Выпуск 8. Октябрь 2013
← Разворачивание PSGI/Plack приложения | Содержание | Миграция веб-приложений с Dancer на Dancer2 →Консольные приложения на Curses
Приложения с текстовым интерфейсом, работающие в терминале, по-прежнему очень популярны и отлично конкурируют с приложениями с графическим интерфейсом. Mutt
, irssi
, vim
, tmux
и многие другие являются незаменимыми в повседневной работе. На CPAN есть модули, позволяющие создавать приложения с текстовым интерфейсом, в том числе модуль Curses
, являющийся обвязкой к распространённой C-библиотеке ncurses
.
История
Для самых ранних ЭВМ в качестве ввода/вывода информации использовались электромеханические телетайпы (Teletype, или сокращённо TTY), которые позволяли вводить текст с клавиатуры и печатать на бумаге символ за символом, полученные от ЭВМ. Позже телетайп был заменён терминалом, где принтер заменил ЭЛТ-экран, на который на фиксированные позиции можно было выводить символы. Самым первым компьютерным терминалом стал DataPoint 3300, который имел экран, позволяющий выводить 25 рядов по 72 символа. Примечательно, что этот терминал был создан на основе обычных TTL-микросхем, а дальнейшие попытки упростить логику и уменьшить размер внутренней начинки терминала стали одной из побуждающих причин разработки микропроцессоров.
Как правило, к одному мейнфрейму под управлением какой-либо из UNIX-систем подключалось множество терминалов. ЭВМ запускала программу командной оболочки, ввод и вывод которой был связан с устройством терминала. Оболочка обрабатывала вводимые команды и выводила результаты их работы на терминал.
Стандартом де-факто на долгие времена стал терминал VT100, созданный компанией DEC в 1978 г. Терминал подключался к вычислительной машине через последовательный интерфейс, позволял выводить 80 или 132 символа в ряд и использовал для кодировки символов стандарт ASCII, а также набор управляющих последовательностей (escape-последовательностей), которые были стандартизованы ANSI в виде стандарта ECMA-48. Escape-последовательности получили своё название от кода клавиши Escape, который предварял набор кодов, определяющих управляющую последовательность. Управляющие последовательности позволяли передвигать курсор, очищать экран, задавать толщину символов или даже делать мигающую строку, на цветных терминалах появилась возможность задавать цвет фона и символа. Например, последовательность кодов ESC[1m
задаёт жирный шрифт для последующих выводимых символов, а ESC[0m
сбрасывает все установленные атрибуты.
Возможность манипулировать расположением символов и их атрибутами на экране открыло путь к созданию программ с текстовым пользовательским интерфейсом. В отличие от командного интерфейса, в текстовом интерфейсе с помощью ASCII
-символов можно было отображать границы окон, диалоговые окна, меню или таблицы, а также кнопки и поля ввода.
С ростом числа терминалов стала возникать проблема с поддержкой всех их видов внутри программ. В 1978 г. Билл Джой, при работе над текстовым радактором vi для Berkeley Unix, выделил код для поддержки разных типов терминалов в отдельную библиотеку под названием termcap, которая стала базой данных описаний существующих терминалов и позволяла реализовывать программы, независимые от типа терминала. Termcap давал возможность программе узнать ширину терминала, правильную последовательность escape-кодов для перемещения курсора и т.д. Вслед за termcap появилась библиотека terminfo, которая являлась улучшенной реализацией termcap с более быстрым доступом к описанию терминала. Именно terminfo получила максимальное распространение в UNIX-системах.
В 1980 г. большую популярность получила игра Rogue, которая дала старт целому жанру rogue-подобных игр, в которой игровой персонаж исследует подземелья, сражается с монстрами и ищет сокровища. Игра для вывода использовала текстовую консоль, монстры обозначались заглавными буквами, коридоры и границы подземелий прорисовывались ASCII-символами |
и -
. Один из разработчиков, Кен Арнольд, специально для игры создал библиотеку, которая абстрагировалась от работы с конкретным типом терминала, вводила абстрактные понятия окон как матриц символов, забирая на себя все заботы по выводу и обновлению экрана. Библиотека получила название curses («проклятие»), что является каламбуром на словосочетание «cursor optimization». Первоначально curses была основана на termcap. В отличие от termcap, которая фактически являлась текстовой базой данных о свойствах терминалов, curses предоставляла довольно внятное C API.
Позже появились библиотеки pcurses и PDcurses, которые являлись свободными альтернативами BSD curses. В 1991 г. работа над pcurses была продолжена, а в 1993 г. она была переименована в ncurses, что было сокращением от new curses (новая curses), и стала развиваться в рамках проекта GNU. Со временем к ncurses появились обертки для других языков программирования, в том числе и Perl-модуль Curses, который позволяет использовать библиотеку в Perl-программах.
Широкое использование аппаратных терминалов прекратилось после появления видеодисплеев, но тем не менее простота интерфейса и количество существующих для него программ привело к тому, что были созданы эмуляторы терминала — программы, которые эмулировали видеотерминал внутри другой видео системы, например, X Window. Это позволяло продолжать использовать программы, заточенные для работы в терминале, и в других системах. Наиболее популярные графические эмуляторы терминала — xterm, rxvt, а также современные gnome-terminal и konsole.
Использование консольных приложений с текстовым интерфейсом актуально и на сегодняшний день. Такие приложения как top, vim, emacs, mutt, irssi, moc, midnight commander используют многие и просто не представляют себе другой альтернативы. Терминальное приложение может работать поверх последовательных линий и поверх сети на очень низких скоростях, которые недоступны VNC или RDP. А в отличие от веб-интерфейсов, не требуют от клиента наличия гигабайтов оперативной памяти и многоядерных процессоров для того, чтобы удовлетворить системным требованиям современных браузеров.
Устройство терминала
Поскольку исторически терминалы являлись внешними устройствами с посимвольным обменом информации по последовательным линиям, то в UNIX-системах каждому терминалу соответствовал файл устройства вида /dev/tty*
. Системные консоли были доступны на /dev/tty[0..NN]
, последовательные порты на /dev/ttyS[0..NN]
и т.д. (вариация названия зависела от конкретной реализации UNIX-системы). Для каждого такого устройства существует возможность установки скорости обмена, управляющих последовательностей и других настроек. Текущие настройки любого такого устройства можно увидеть с помощью утилиты stty
:
# stty -a --file=/dev/tty0
speed 38400 baud; rows 64; columns 160; line = 0;
...
Для процессов, которым необходима работа с терминалом, не связанным с каким-то физическим устройством, создаются так называемые псевдотерминалы. В современных UNIX-системах для эмулятора терминала по запросу создаётся файл устройства псевдотерминала в файловой системе devpts, например /dev/pts/0
, c которым ассоциируются стандартные файловые дескрипторы ввода/вывода STDIN
, STDOUT
и STDERR
запускаемых эмулятором дочерних процессов. Эмулятор терминала определяет, каким будет тип терминала, каков будет размер экрана (количество строк и символов в строке) и другие параметры. Для запущеного же в псевдотерминале приложения нет никакого отличия работает ли он в псевдотерминале или на любом другом реальном терминале.
Perl Curses
Perl-модуль Curses
является низкоуровневой обвязкой к С-библиотеке ncurses. Из существующей POD-документации практически невозможно понять, как программировать приложения на Curses
, поскольку подразумевается, что вы будете использовать документацию по самой C-библиотеке ncurses.
Помимо Curses
, на CPAN существуют ещё несколько дистрибутивов, которые позволяют разрабатывать приложения с текстовым интерфейсом. Часть их используют Curses
, но имеют более высокоуровневый интерфейс, удобный для разработки. Это такие модули как Curses::UI
и Curses::Toolkit
. Есть также модули, которые вообще не привязаны к ncurses, например, Tickit
. Если вы планируете создание сложных консольных приложений, где будут активно использоваться примитивы окон, диалогов, меню, форм, функции обратного вызова на события, то эти модули, вероятно, наиболее лучший выбор для использования. Если же стоит задача создания тривиального позиционного вывода информации, как, например, в приложении top
, то использование Curses
может быть целесообразнее.
Библиотека Curses
работает с экраном, который представляется в виде прямоугольной матрицы, каждый элемент которой связан с позицией на экране для вывода символа. Начало координат, позиция (0,0), задаёт левый верхний угол экрана. На экране могут быть заданы одно или несколько окон и субокон — прямоугольных областей, для которых определены свои свойства вывода.
Старт с Curses
Несколько функций библиотеки инициализируют работу приложения в текстовом режиме:
initscr
— функция инициирует режим curses, создаёт окно по умолчанию, производит очистку экрана и устанавливает текущую позицию курсора в левый верхний угол (0,0);cbreak
иraw
— функции отключают буферизованный ввод, и приложение сразу получает информацию о нажатых клавишах, при этомraw
также перехватывает и специальные сочетания клавиш, такие какCtrl+C
иCtrl+Z
;echo
иnoecho
— включают и отключают режим «эха», т.е. вывод на экран вводимых с клавиатуры символов.
Библиотека Curses
имеет ОО-интерфейс, в этом случае вместо инициализации окна по умолчанию через initscr
можно использовать вызов new
:
my $win = Curses->new();
Многие функции С-библиотеки ncurses имеют различные вариации одной и той же функции с префиксами w, mv, и wmv. Префикс подразумевает, что появляется какой-либо дополнительный параметр: w — объект окна, mv — координаты y, x позиции курсора. В Perl-библиотеке Curses
такие вариации функций были объединены в одну, но с опциональными параметрами окна и координат:
function( [win], [y, x], args );
Такие функции получили название унифицированных, и их можно использовать как методы при программировании с использованием ОО-интерфейса. В документации Curses
приведён полный список функций и указано, какие из них являются унифицированными, а какие — нет. Во всех последующих примерах будет использоваться ОО-интерфейс, поэтому если используется прямой вызов функций, то это явно подразумевает, что функция не является унифицированной.
Рассмотрим пример приложения, которое выводит сообщение в центр экрана, ожидает ввод пользователя и завершает работу:
use strict;
use warnings;
use Curses;
my $win = Curses->new();
raw();
noecho();
$win->keypad(1);
$win->getmaxyx(my $row, my $col);
my $str = "Your terminal size: ${row}x$col";
$win->addstr( $row/2, ( $col - length $str )/2 , $str );
$win->refresh();
my $ch = $win->getch();
endwin();
Приложение создаёт окно. Метод keypad
со значением истины в параметре включает обработку специальных клавиш, таких как F1
, F2
и т.д. Метод getmaxyx
позволяет определить максимальные значения координат на экране, т.е. размер текущего терминала. Метод addstr
позволяет вывести строку по заданным начальным координатам. Метод refresh
отображает всё то, что мы вывели на экран. Метод getch
ожидает нажатия клавиши и возвращает введённый символ. Функция endwin
завершает режим curses, возвращая предыдущее содержимое экрана.
Окна и субокна
Окна и субокна используются для того, чтобы установить ограничение для контента, который будет выводиться внутри окна, а также для того, чтобы управлять атрибутами внутри окна.
Для создания нового окна используется функция newwin
:
my $win = newwin($rows, $cols, $y, $x);
где $rows
, $cols
— определяют высоту и ширину окна, а $y
, $x
— положение окна на экране.
Окна могут перекрываться, при этом в месте пересечения будет отображаться контент того окна, которое было обновлено последним. Если требуется создать окно внутри другого окна, то можно использовать функцию создания субокна subwin
или derwin
:
my $subwin = $win->subwin($rows, $cols, $y, $x)
my $subwin = $win->derwin($rows, $cols, $dy, $dx)
Отличие subwin
и derwin
в том, что в derwin
координаты субокна задаются относительно верхнего левого угла родительского окна.
Для удаления окна или субокна используется функция delwin
$win->delwin();
Окно можно перемещать по экрану с помощью функции mvwin
(mvderwin
— для субкона):
$win->mvwin($newY, $newX)
$subwin->mvderwin($newDX, $newDY);
Очистить содержимое окна можно с помощью функции clear
:
$win->clear();
Существует возможность отобразить границы окна с помощью функции box
:
$win->box("|","-");
$win->box(0,0);
где первый параметр определяет символ для вертикальных линий, а второй — горизонтальных. Если используется значение 0
(или undef
), то используются символы границ по умолчанию.
Если требуется задать различные символы для всех сторон и углов окна, то можно воспользоваться функцией border
:
$win->border(
$left, $right, $top, $bottom,
$top_left, $top_right, $bottom_left, $bottom_right
);
Атрибуты
Для каждого выводимого символа может быть установлены атрибуты, в том числе:
A_BOLD
— режим повышенной яркости;A_NORMAL
— нормальный режим яркости;A_DIM
— режим пониженной яркости;A_UNDERLINE
— подчёркнутый текст;A_REVERSE
— инверсный текст;A_BLINK
— мерцающий текст;A_INVIS
— невидимый текст.
Не все терминалы поддерживают указанные атрибуты (как правило, A_NORMAL
совпадает с A_DIM
, а некоторые атрибуты могут заменяться цветовым выделением).
Установить конкретный атрибут или набор атрибутов можно функцией attron
:
$win->attron(A_BOLD | A_UNDERLINE);
После чего при выводе текста в данное окно начинают применяться указанные атрибуты. Также для установки атрибутов можно использовать функцию attrset
, но данная функция сбросит все другие атрибуты, которые были до этого установлены:
$win->attron(A_BOLD); # attributes now A_BOLD
$win->attron(A_REVERSE); # attributes now A_BOLD + A_REVERSE
$win->attrset(A_UNDERLINE); # attributes set to A_UNDERLINE
Для отключения определённых атрибутов используется функция attroff
:
$win->attroff(A_BOLD);
Цвета
Многие терминалы имеют поддержку цвета. Для того, чтобы определить, поддерживает ли терминал цвет, можно использовать функцию has_colors
. Если поддержка цветов в терминале присутствует, то перед работой с цветовой палитрой требуется инициализировать цвета вызовом функции start_color
:
die "Your terminal does not support color\n" unless has_colors;
start_color();
Для создания палитры из двух цветов (цвет фона и цвет текста) используется функция init_pair
:
init_pair(1, COLOR_BLUE, COLOR_BLACK);
Установить указанную палитру цветов для вывода можно всё той же функцией attron
:
$win->attron(COLOR_PAIR(1));
Цветовая палитра ограничена 16 цветами, причём базовых цветов только 8, а дополнительные цвета получаются за счёт использования повышенной яркости (A_BOLD
):
COLOR_BLACK 0
COLOR_RED 1
COLOR_GREEN 2
COLOR_YELLOW 3
COLOR_BLUE 4
COLOR_MAGENTA 5
COLOR_CYAN 6
COLOR_WHITE 7
Существует возможность переопределить любой базовый цвет с помощью функции init_color
, которая для заданного базового цвета устанавливает значения интенсивности соответственно красного, зелёного и синего цветов в диапазоне от 0 до 1000:
init_color(COLOR_BLACK, 100,100,100); # black -> dark gray
Не все терминалы поддерживают такую возможность, поэтому перед изменением цветов палитры следует выяснить, присутствует ли такая возможность в текущем терминале, с помощью функции can_change_color
.
Обработка ввода
Для получения значения введённого символа используется метод getch
. В случае если была активирована обработка специальных клавиш (F1
, PgUp
, PgDown
и т.д.) с помощью keypad(1)
, то getch
возвращает числовой код специальной клавиши, например, значение 338 для PgDown
. Если же обработка специальных клавиш отключена, то getch()
вернёт лишь первый код из escape-последовательности клавиши.
Если на вводе оказывается символ Unicode, то getch
воспринимает лишь первый байт последовательности. Соответственно для обработки Unicode-символа, имеющего внутреннее представление из двух байт, потребуется два последовательных вызова getch
:
$win->keypad(1);
$ch = $win->getch();
if (length $ch > 1 && $ch == KEY_F(1)) {
# looks like a F1 key
$win->addstr(1, 1, "F1 KEY!");
}
elsif (ord($ch) < 128) {
# looks like an ASCII
$win->addstr(1, 1, $ch);
}
elsif (ord($ch) >> 5 == 0b110) {
# hack: looks like a first byte of 2bytes UTF-8 unicode code point
$ch .= $win->getch();
$win->addstr(1, 1, decode_utf8($ch));
}
В Curses
определены константы для функциональных клавиш, а также функция KEY_F
, возвращающая код F*
-клавиш. Названия констант можно найти в документации Curses
, все они имеют префикс KEY_
.
В С-библиотеке ncursesw, есть функция get_wch
, которая может обрабатывать ввод «широких» символов, но, к сожалению, она не перенесена в Perl-обвязку Curses
.
С помощью функции halfdelay
можно указать, какое время (в десятых долях секунды) getch
должен ожидать ввода символа. Если после истечения таймаута клавиша не нажата, getch
вернёт значение ERR (-1).
halfdelay(10)
$ch = $win->getch();
if ($ch == ERR) {
# Timeout
...
}
Для получения строки символов до символа переноса строки (или EOF
) может использоваться функция getstr
:
$win->getstr(my $str);
$win->addstr(1,1,"You type: $str");
Управление курсором
Получить текущие координаты курсора можно с помощью getyx
:
$win->getyx(my $y, my $x);
Если вызывается какая-либо функция вывода на экран без указания координат, то вывод производится начиная с текущей позиции курсора. Курсор можно передвигать по экрану с помощью функции move
:
$win->move($y, $x);
Очистка экрана
Существует возможность очистки экрана или его части. Под очисткой понимается заполнение его пробельными символами.
Функция clear
очищает окно целиком:
$win->clear();
Функции clrtoeol
и clrtobot
очищают экран, начиная с текущей позиции курсора до конца строки и конца окна соответственно:
$win->ctrloeol();
$win->ctrlobot();
Функции deleteln
и delch
удаляют строку и символ соответственно на текущей позиции курсора:
$win->deleteln();
$win->delch();
Функция insdelln
вставляет указанное число пустых строк, начиная с текущей позиции курсора (или удаляет, если аргумент отрицательный).
$win->insdelln(10);
Обработка событий мыши
Curses
позволяет обрабатывать события, возникающие при использовании мыши, такие как нажатие клавиш и перемещение указателя. Перед началом обработки действий мыши необходимо задать маску событий, которые будет фиксировать программа. Делается это с помощью функции mousemask
:
mousemask(ALL_MOUSE_EVENTS, my $old_mask);
Фиксировать наличие действий, произведённых мышью, можно всё той же функцией getch
. Функция getmouse
позволяет получить структуру произошедшего события, куда входит информация об идентификаторе устройства мыши (их может быть несколько), координатах мыши в трёх измерениях и непосредственно типе события.
my $key = $win->getch();
if ($key == KEY_MOUSE) {
getmouse(my $ev);
# fix align of short in C-structure
my ($id, $x, $y, $z, $state) = unpack("s x![i] i3 L",$ev);
if ($state & BUTTON1_CLICKED) {
# left button clicked
}
}
Изменение размера экрана
Поскольку зачастую консольные приложения запускаются внутри графического окна эмулятора терминала, то во время работы приложения может происходить изменение размеров экрана, когда пользователь растягивает окно терминала или наоборот уменьшает. Для этих случаев существует сигнал SIGWINCH
, которым эмулятор терминала оповещает запущенные приложения об изменении размера экрана.
Библиотека Curses
устанавливает обработчик этого сигнала и пытается адаптировать существующие окна под новый размер экрана. Конечно не всегда это может быть выполнено корректно, поэтому для пользовательского приложения реализована возможность получить оповещение об произошедшем изменении размеров экрана — функция getch
возвращает значение KEY_RESIZE
:
my $key = $win->getch();
if ($key == KEY_RESIZE) {
# get new window size
$win->getmaxyx(my $row, my $col);
}
Не стоит переопределять обработчик сигнала SIGWINCH
в коде приложения, поскольку это приведёт к завершению работы приложения после получения сигнала.
Панели, меню и формы
В рамках проекта ncurses существуют дополнительные библиотеки, расширяющие возможности ncurses:
- Панели. Представляют собой окна, которые могут перекрывать друг друга, при этом можно указать в каком порядке следуют панели, т.е. указывается третье измерение — глубина. Панели можно скрывать, отображать, перемещать наверх в стопке.
- Меню. Позволяет реализовывать внутри ваших приложений привычные меню с поддержкой вложенности.
- Формы. Позволяют создавать привычные элементы для ввода данных в виде текстовых полей, которые можно заполнять и редактировать.
Подробное описание этих расширений может стать темой для ещё одной статьи, но вполне возможно, если приложение требует подобного функционала, стоит посмотреть на более мощные альтернативы на CPAN, например, Curses::UI
или Tickit
.
← Разворачивание PSGI/Plack приложения | Содержание | Миграция веб-приложений с Dancer на Dancer2 →