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

в 2:11, , рубрики: Без рубрики

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js