Pull to refresh

Теплый ламповый текстовый интерфейс. Просто о простом

Reading time 5 min
Views 58K
Периодически просматривая топики на хабре, постоянно ловлю себя на мысли, что ещё чуть-чуть и какой-нибудь нейроинтерфейс в ноутбуке станет реальностью. В работе постоянно натыкаюсь на то, что современные люди не очень понимают и любят простую командную строку. А читать мануалы им тем более лень.
Но в моей практике часто случается так, что нужна небольшая утилита, выполняющая одну или две функции. А где именно она будет выполняться — неизвестно. Это может быть 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);
}



Ну и в результате



Думал описать больше, но судя по всему, это был бы слишком большой и скучный пост, я же лишь хотел обратить внимание на эту «древнюю технологию», у которой достаточно возможностей. За кадром остались манипуляции с текстом, с атрибутами и прочим. К примеру, возможно скопировать строку текста из любого окна, узнать его цвет и режим. И многое другое.
Надеюсь, это не было слишком скучным.
Tags:
Hubs:
+90
Comments 42
Comments Comments 42

Articles