Периодически просматривая топики на хабре, постоянно лювлю себя на мысли, что ещё чуть-чуть и какой-нибудь нейроинтерфейс в ноутбуке станет реальностью. В работе постоянно натыкаюсь на то, что современные люди не очень понимают и любят простую командную строку. А читать мануалы им тем более лень.
Но в моей практике часто случается так, что нужна небольшая утилита, выполняющая одну или две функции. А где именно она будет выполняться — неизвестно. Это может быть Windows, это может быть исключительно терминальный линукс, загрузочная медия — что угодно. Я не программист, но иногда бывает нужно облегчить жизнь себе или другим. И желательно как можно более наглядно. Сначала я пробовал делать просто консольные утилиты. Собственно, с этого, наверное, начинают все. Но очень быстро оказалось, что средствами printf/sprintf/puts и прочими (а пишу я на С) не очень удобно форматировать текст, выводить какую-то информацию. Скролящиеся окно выглядит не очень симпотично, и если информации много — абсолютно нечитаемо. Тогда я вспомнил про ncurses.
Обычно curses/ncurses ассоциируется с линуксом, хотя на самом деле совместимые реализации есть для многих платформ, в частности и под Windows. Изначально большая часть утилит нужна была под Win, а никаких графических фреймворков я не знал и отчаянно искал способы нормально оформить текст, сделать красиво и наглядно. Вот тогда я и наткнулся на Public Domain Curses. Созданный с целью быть совместимым с ncurses, он позволяет писать кроссплатформенные приложения, используя большую часть функционала оригинального curses/ncurses. Но, к сожалению, без багов и ограничений не обходится. Но это не так страшно, как казалось по началу. Я хочу показать, что создать симпотичное консольное псевдооконное приложение не так сложно; а на выходе мы получаем теплый ламповый TUI. Хочется, чтобы люди не забывали о таких методах работы с пользователем.
В данном посте, я буду описывать работу, совместимую с PDcurses, такчто данные примеры должны без проблем собираться и под Windows и под Linux.
Начало
Так как мы работаем с текстовым интерфейсом, то единицей размерности у нас будет один символ. Работать можно как с обычным ASCII, так и с Wide символами. Следует помнить, что отобразить curses может только то, что поддерживает терминал. К сожалению, лично у меня 80% псевдографики не выводится адекватно. Чуть лучше на линуксе, совсем плохо на Windows. к счастью, простые линии выводятся нормально.
Работать мы можем с окнами, панелями, цветами и текстом (включая скроллинг, копирование и прочее).
Перед началом работы, нам необходимо подготовиться к работе, в(ы)ключить (не)нужные опции.
initscr(); //инициализируем библиотекц
cbreak(); //Не использовать буфер для функции getch()
raw();
nonl();
noecho(); //Не печатать на экране то, что набирает пользователь на клавиатуре
curs_set(0); //Убрать курсор
keypad(stdscr, TRUE); //Активировать специальные клавиши клавиатуры (например, если хотим использовать горячие клавиши)
if (has_colors() == FALSE) //На практике столкнулся с линуксом, на котором не было поддержки цвета.
{
endwin();
puts("nYour terminal does not support color");
return (1);
}
start_color(); //Активируем поддержку цвета
use_default_colors(); //Фон stscr будет "прозрачным"
init_pair(1, COLOR_WHITE, COLOR_BLUE); //Все цветовые пары (background-foreground) должны быть заданы прежде,ч ем их используют
init_pair(2, COLOR_WHITE, COLOR_RED);
......
Сначала было окно
Когда мы запускаем эмулятор/экземпляр терминала, мы оказываемся в stdscr. Это наш базис, начальное окно. Работать мы можем в нем, либо насоздавать своих окон.
Хватит слов, давайте к делу. Создадим окно. Сразу хочу заметить важный нюанс — везде, во всем функциях, сначала идет Y, потом X
WINDOW *win = newwin(height, width, y, x);
Каждое новое окно имеет свои собственные относительные координаты, которыми вы будете оперировать в дальнейшем. Это важно и удобно.
Окно создано, но в консоли ничего не появилось. Потому что окно унаследовало атрибуты родителя — stdscr в нашем случае.
Сразу покажу, как делаю я. Имеется структура, которая описывает «виртуальное окно», о панелях расскажу попозже
struct cursed_window
{
WINDOW *background;
WINDOW *decoration;
WINDOW *overlay;
PANEL *panel;
};
typedef struct cursed_window curw;
Я так делаю для того, чтобы сначала сделать оформление, которое не будет меняться и которое статично. Меняем только рабочие данные, при этом не затирая оформление.
Окно background — прозрачный фон и тень от окна.
decoration — рамка, она рисуется автоматически
overlay — собственно, рабочее поле. Начало координат у неё будет 0,0, так как это новое окно, не нужно вносить поправки на рамку и тень.
про панель — позже.
curw *tui_new_win(int sy, int sx, int h, int w, char *label)
{
curw *new = malloc (sizeof *new);
new->background = newwin(h, w, sy, sx);//Создаем самую нижнюю часть нашего бутерброда
wattron(new->background, COLOR_PAIR(7));//Черная тень, яркий цвет. Атрибуты можно объединять
//И рисуем тень черным пробелом
for (int i= w*0.1; i<w;i++)
mvwaddch(new->background, h-1, i, ' ');
for (int i= h*0.2; i<h;i++)
mvwaddch(new->background, i, w-1, ' ');
wattroff(new->background, COLOR_PAIR(7));
//Создаем окно для рамки, это уже дочернее окно для фона. Поэтому координаты указываются
//Относительно родительского окна
new->decoration = derwin(new->background, h-2, w-2, 1, 1);
wbkgd(new->decoration, COLOR_PAIR(1));
//Рисуем рамку
box(new->decoration, 0, 0);
int x, y;
getmaxyx(new->decoration, y, x);
new->overlay = derwin(new->decoration, y-4, x-2, 3, 1);//рабочее дочернее окно
wbkgd(new->overlay, COLOR_PAIR(1));
new->panel = new_panel(new->background);
tui_win_label(new->decoration, label, 0);
//Даем команду обновить все это на экране
update_panels();
doupdate();
return new;
}
На самом деле, если содать второе окно поверх этого, то наш фон «наедет» на нижнее окно. Это некрасиво. Но устранимо. Но это уже тема отдельного разговора. Уберем тени для простоты и создадим несколько окон
А вот теперь можно сказать про панели. Панель это контейнер-стек, вмещающий в себя окно и все его дочерние окна. С панелью можно проводить множество интересных манипуляций.
Панели
Теперь можно продемонстрировать возможности панелей на практике. Самое верхнее окно в стеке доступно для работы по умолчанию. Мы так же можем обращаться к любым окнам и панелям в стеке снизу, писать в них, при этом никак не влияя на окна в стеке выше. Мы можем сами сортировать окна как угодно, перемещать, изменять их размеры. Уж простите за примитивный код, но старался делать нагляднее.
int x, y;
getmaxyx(stdscr, y, x);
curw *wins[3];
//Создадим несколько окон
wins[0] = tui_new_win(0, 0, y - 5, x - 5, "-=Hello Habr=-", 1);
wins[1] = tui_new_win(y / 3, x / 2, 15, 30, "-=Data=-", 4);
wins[2] = tui_new_win(5, 5, 10, 20, "-=Memo=-", 5);
PANEL *TOP = wins[0]->panel;
int panel_counter = 0;
do
{
switch ( user_key )
{
case 0x9: //TAB
if(++panel_counter > 2)
{
panel_counter=0;
}
TOP = wins[panel_counter]->panel;
break;
case KEY_UP:
case KEY_DOWN:
case KEY_LEFT:
case KEY_RIGHT:
tui_move_panel(wins[panel_counter], user_key);
default:
if(isalpha(user_key))
waddch(wins[panel_counter]->overlay, user_key);
break;
}
//Ставим текущее выбранное окно на вершину стека и обновляем
top_panel(TOP);
touchwin(panel_window(TOP));
update_panels( );
doupdate( );
}
while (( user_key = getch( )) != KEY_F(12));
void tui_move_panel(curw *win, int ch)
{
int begy, begx, maxx, maxy, x, y;
getbegyx(panel_window(win->panel), begy, begx);
getmaxyx(panel_window(win->panel), maxy, maxx);
getmaxyx(stdscr, y, x);
switch (ch)
{
case KEY_UP:
if ((begy - 1) >= 0)
begy--;
break;
case KEY_DOWN:
if (((begy + 1) + maxy) <= y)
begy++;
break;
case KEY_LEFT:
if ((begx - 1) >= 0)
begx--;
break;
case KEY_RIGHT:
if (((begx + 1) + maxx) <= x)
begx++;
break;
}
move_panel(win->panel, begy, begx);
}
Ну и в результате
Думал описать больше, но судя по всему, это был бы слишком большой и скучный пост, я же лишь хотел обратить внимание на эту «древнюю технологию», у которой достаточно возможностей. За кадром остались манипуляции с текстом, с атрибутами и прочим. К примеру, возможно скопировать строку текста из любого окна, узнать его цвет и режим. И многое другое.
Надеюсь, это не было слишком скучным.
Автор: Pugnator