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

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



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



    Думал описать больше, но судя по всему, это был бы слишком большой и скучный пост, я же лишь хотел обратить внимание на эту «древнюю технологию», у которой достаточно возможностей. За кадром остались манипуляции с текстом, с атрибутами и прочим. К примеру, возможно скопировать строку текста из любого окна, узнать его цвет и режим. И многое другое.
    Надеюсь, это не было слишком скучным.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 42

      +14
      Кнопки и обработка ввода остались за кадром. Очень жаль. Было бы интересно почитать еще и про них
        +1
        Для обработки пользовательского ввода можно использовать удобную библиотеку libtermkey. А для создания цикла обработки ввода, который бы поддерживал возобновление по разным сигналам (не только по завершении getch()) использовать eventfd.
          +7
          Раз такая поддержка, я подготовлю второй пост на эту тему. Когда писал, я вообще не ожидал комментариев к этому посту. Раз тема интересна, с большим удовольсвтием продолжу
          +4
          Так как обычно ширина букв в 2 раза меньше высоты, то тень от окон лучше рисовать как: 1 символ снизу, 2 справа.
            +3
            На первом курсе университета писал что-то подобное, но на ncurses под линух.

            Прочитал статью.
              0
              Все действительно тепло и лампово, но, увы, современный пользователь вряд ли осилит подобный интерфейс. В нем нет большой красивой кнопки «сделать все как надо».
                +4
                Этот интерфейс не предназначен для конечного пользователя-потребителя. Зато он отлично подходит для упрощения работы админам. Всё же проще запустить такую программу и быстро всё настроить, чем разбираться в синтаксисе и допустимых комбинациях параметров различных конфигурационных файлов. Тем более такие программы можно спокойно запускать удалённо по ssh.
                  +4
                  Всё-таки непонятна целевая аудитория. Админам и олдскульным программистам (типа меня) намного удобнее консоль. А остальным лучше привычный графический интерфейс. Имхо, конечно

                  P.S. Исправьте, пожалуйста «симпотично» — режет глаз не меньше чем «правельно» :) Я не наци, ноэто «режущие глаз» ошибки :)
                    0
                    Так она и будет запускаться в консоли :)

                    Про «симпотично», наверное, к автору.
                      +1
                      Я имел ввиду чистая консоль. С ключиками
                        +5
                        Некоторые программы имеют сложные конфиги. Например, ядро Linux. Обычно его конфигурируют из menuconfig. Думается, что и для других сложноконфигурируемых вещей (веб-сервера, почтовые службы) такие штуки найдут применение.
                          0
                          Да, возможно вы правы. Спасибо
                            0
                            Как раз ядро показывает что нужны оба варианта: ncurses чтобы что-то аккуратно выбрать из меню и консольная программа с сотней опций (или конфиг-файлом) чтобы интегрировать её ещё куда-то.

                            Практически проще всего написать пару таких программ где ncurses'ная генерирует ключики для «настоящей».

                            Конечно если процесс интерактивен (выбрали одну команду, посмотрели результат, выбрали другую, что-то ещё сделали), то так не получится, но тут уже уровень сложности начинает приближаться к «настоящей» программе, которую нужно разрабатывать уже не в одиночку и, возможно, даже иметь красивую кнопку «сделать всё как надо»…
                              0
                              Просто все гуевые конфигураторы там оказались менее удобными. Впрочем, инсталляторы и подобные вещи единственное исключение, где подобный интерфейс может быть удобен. Хотя не очень представляю как делать make menuconfig с планшета по ssh, например. :)
                    +9
                    Смотрели на turbovision?
                    Например такой tvision.sourceforge.net/
                      +3
                      Прекрасная библиотека.
                      Именно она привила мне «ООП головного мозга» ;)
                        +2
                        Морально устаревшее API. Но для С/С++ наверное пока лучше всё равно ничего нет. Для C# я потихоньку пишу удобную библиотеку, там можно будет описывать интерфейсы в XAML, и компоновать контролы как в WPF. Но пока ещё не готово (хотя для маленьких программ уже можно использовать). Описание окна будет выглядеть примерно так:

                        <Window Name="window2" Title="Очень длинное название окна">
                          <GroupBox Title="Title">
                            <ScrollViewer VerticalAlignment="Stretch">
                              <ScrollViewer.HorizontalScrollEnabled>false</ScrollViewer.HorizontalScrollEnabled>
                              <ListBox>
                                <ListBox.Items>
                                  <item>Длинный элемент 2</item>
                                  <item>Длинный элемент 3</item>
                                  <item>Длинный элемент 4</item>
                                  <item>Длинный элемент 5</item>
                                  <item>Длинный элемент 6</item>
                                  <item>Длинный элемент 700</item>
                                </ListBox.Items>
                              </ListBox>
                            </ScrollViewer>
                          </GroupBox>
                        </Window>
                        


                        А само окно так:


                        Data binding, нормальная система layout'ов, поддержка Win32 и Linux (с использованием Mono) — всё будет в комплекте.
                          +2
                          Декларативность — это, конечно, хорошо, но поддержки не windows систем у вас по сути не будет. Я еще не видел системы, на которую бы устанавливали mono для запуска какой-нибудь программы и эта программа при этом нормально работала.
                            +1
                            Моно занимает немного, мегабайт 20 всего в «базовой комплектации». Плюс в некоторых дистрибутивах (если я ничего не путаю) оно уже установлено по умолчанию. Gnome Do, например — это же моно-приложение.
                              +1
                              Я не говорю про его размеры, я говорю про то, что оно еще не допилено до конца и редкая программа работает на нем без глюков. И это перманентное состояние, потому что мс регулярно выпускает новые версии фреймворков, не совместимые со старыми.

                              Писать программы на C# для не windows систем — очень странное решение по тем же самым причинам.
                                +1
                                По-моему, новые версии .NET от Microsoft не особо влияют на стабильность моно ) Да и ядро моно давно уже стабилизировалось, в частности, я знаю программистов, которые доверяют ему и делают на нём даже веб-приложения. Вообще изначально я писал это для Windows. Но потом решил, что нельзя пренебрегать важным Linux-направлением, и добавил поддержку Mono. Тем более что такого рода инструменты в nix-среде хотя бы могут быть востребованы по сравнению с Windows, где это вообще никому не нужно. А вариантов нет, можно конечно попробовать написать на пайтоне, но я его не знаю ) По-моему, моно для этого вполне адекватный вариант. Поставить моно для работы нужной программы — это вообще не проблема для прагматически настроенных людей. Попробовали — работает — все довольны. Не работает — почистили, удалили моно, ждём фиксов.
                          +1
                          Удивительно, что не видел этого раньше. Обязательно посмотрю, ибо тот же pdcurses не умеет меню, я их велосипедил руками под винду
                            0
                            Ух, оно еще живо? Сколько лет прошло :) В свое время было очень круто.
                              0
                              Насколько я помню, TurboVision была переписана под «современные» реалии 10 лет назад под (в том числе) и Win32 платформу. Переписана автором Ida Pro. В принципе тогда я эту библиотеку и пользовал в крайний раз.
                              +1
                              Вспомнилась молодость. Лет этак 20 назад активно разбирался с TVision, писал тогда на Паскале. Уже в 2000 клиенту потребовалось написать фронтэнд к базе под текстовые терминалы на Unix-e. Как ни странно нашел билд TVision под юникс который без проблем компилился и работал. Юзеры хрюкали от счастья.

                              Помню к TVision кстати был то ли add-on то ли сторонняя разработка, которая добавляла полезных виджетов.
                              0
                              Для консольного UI есть плюсовая библиотека cwidget. Работает поверх curses.
                                0
                                "… современные люди не очень понимают и любят простую командную строку. А читать мануалы им тем более лень."
                                А в основном у современных людей для этого просто нет времени. Хорошо когда раньше программ было 2-3 десятка и ты знал все команды наизусть, теперь все иначе.
                                Немножко побуду капитаном, но интерфейс как раз предназначен для того, чтобы не читая мануалов работать с программой (даже неизвестной) и чем интерфейс лучше, тем эта работа будет проще.
                                Командная строка незаменима для процессов автоматизации, но для общения с живым человеком уже есть более человечные, простите за тавтологию, подходы.
                                  0
                                  Конечно, более того, сложилось мнение, что «красноглазить» за консолью должны админы и прочие системнщики. И все бы хорошо, но за три года работы в поддержке одной фирмы я пришел к печальному выводу — «админы уже не те». И чем крупнее фирма, тем хуже. Они покупают поддержку и просто звонят — делайте. Поэтому чем «человечней лицо» командлайновой программы, тем меньше издержки на поддержку и меньше ложных багов будет заводится. Тем меньше ошибок будет. Такая банальная вещь, как парсинг параметров почему-то у многих вызывает проблему. Жесткая последовательность там, где она нелогичная и так далее.
                                  Из современных программ мне очень нравится xe в составе Xen Cloud. И автодополнение, и «подстрочные» подсказки и приятный разбор параметров, с гибким синтаксисом.
                                  • UFO just landed and posted this here
                                  +2
                                  Недавно я решил поэксперементировать и сверстать TUI на HTML. Используя блочную модель и привычные border, background-color можно сделать интерфейс, который будет транслироваться в текст. Пока проект существует как proof-of-concept. Можно запускать в браузере или в консоли через phantomjs. Те мы фактически за даром получаем движок(CSS Box Model) и используя привычные многим языки(HTML+CSS+JS) можем рендерить текстовые интерфейсы. Живет на GH.
                                    0
                                    Вот не понимал никогда причем тут командная строка и подобная псевдографика, она куда ближе к gui, чем к интерфейсу командной строки. Поэтому я никогда особо не видел смысла во всех этих midnight commander'ах, проще уж графическими ФМами пользоваться, а в командной строке пользоваться командами и скриптами, а не окошками на псевдографике.
                                      0
                                      Иногда графической оболочки просто нет (например, на серверах она просто не нужна) или пользователь выполняет операции удалённо (например, через ssh) и ему доступна только консоль. В таком случае полезно иметь псевдогуёвое представление для упрощения работы.
                                        0
                                        Все очень просто. MC полезен, когда значительная часть работы связана с командной строкой, но при этом приходится очень часто передавать файлы в качестве аргументов. Просто такая удобная форма представления списка файлов прямо в консоли вместе со средствами управления этими файлами. Все же нормально скрещивать консоль с GUI в рамках самого GUI все еще не научились и проще делать это посредством отрисовки псевдо-GUI в консоли, чем наоборот. Кстати, когда я пробовал GUI-вые файловые менеджеры, то ощущение было такое, как будто все хорошо, но руки связаны за спиной.
                                        0
                                        Года три назад пытался писать консольный торрент-клиент на жаве, там же и нашел jcurses, а потом энтузиазм закончился. Спасибо, что напомнили.
                                          0
                                          Сделать свои гуи к rtorrent — практически одна из первых мыслей была в свое время, ибо штатные не устраивали. А потмо прикрутил вебморду…
                                            +1
                                            Ненене, в rtorrent меня не устраивало то, что он смотрит файлы только по имени — в utorrent в папке с раздачей можно переименовать файлы и папки, он их не потеряет, т. к. действует по другому принципу: если размер файла совпадает до байта с описанным в .torrent-файле, то начинает проверять хеш. Хеш проверяется — файл найден. В никсах можно конечно симлинков наделать, но это костыль, не избавляющий от срача в папках с раздачами.
                                            Насколько помню, написал либу по парсингу bencode и созданию .torrent-файлов из указанных папок. Потом написал разбиралку ответов трекера, нашел jcurses для консольной морды, нарисовал пару окошек и менюшек, на том и забросил. Т.к. сейчас в rtorrent у меня висит ~300 раздач и он грузит систему, судя по top, на 5-10%, то даже боюсь представить, что творилось бы с загрузкой системы java-поделием, будь оно дописано.
                                              0
                                              Если что, к трансмишну есть патч с переименованием раздач. Для того чтобы собрать его на моем NAS пришлось повозиться, но в конце-концов получилось навести с раздачами порядок да и загрузка вполне скромная. Минусы — переименование только в консоли, а также в специально патченом transmission remote .NET, вебинтерфейс патчем не затронут.
                                          0
                                          После статьи создалось впечатление, что вы только что начали читать книгу Петера Нортона о разработке его Коммандера, название, увы, вылетело из головы.
                                            0
                                            А было бы интересно узнать, ибо быстрое гугление ничего не дало. И вроде бы автором был John Socha?
                                              0
                                              Да, я тоже прошерстил библиографию Нортона, но, увы, не нашёл. Год примерно 98й. Увлекательная и доступная. По её мотивам написал свой просмотрщик-редактор памяти с псевдографическими окнами, мышью.
                                                +2
                                                Это наверное
                                                Нортон П., Соухэ Д. Язык ассемблера для IBM PC. М.: Компьютер, 1992.
                                            0
                                            Немного запоздало, но, всё же, спрошу.
                                            Ваш тестовый образец корректно воспринимал одиночные нажатия Escape и не гадил в консоль разными символами вроде ~:#] при нажатии сочетаний клавиш вроде Shift+F1 или Ctrl+A?

                                            Only users with full accounts can post comments. Log in, please.