- PVSM.RU - https://www.pvsm.ru -

ROTE [1] — простая библиотека на языке C, служащая для эмуляции терминала VT100 [2]. Она создает терминал и предоставляет доступ к его состоянию в виде структуры языка C. В терминале можно запустить дочерний процесс, «нажимать» в нем клавиши и смотреть, что он рисует на терминале. Кроме того, есть функция для отрисовки состояния терминала в окне curses.
Зачем на практике может потребоваться эмулировать терминал и взаимодействовать через него с дочерним процессом? В первую очередь это нужно для автоматического тестирования программ, рисующих что-то на терминале с помощью curses, по моему мнению. Как иначе написать тесты для программы, которая ждёт, что пользователь нажмёт клавишу, и выводит результаты в определенное место экрана средствами curses?
Несмотря на всё удобство и внутреннюю красоту ROTE, использовать её напрямую в тестах было бы громоздко. Поэтому я решил упростить задачу, привязав ROTE к языку Lua, который я очень люблю и знаю, как [3] писать тесты. Так и родилась библиотека lua-rote [4], о которой я хочу рассказать.
Потребуется Linux, curses, Lua версии от 5.1 до 5.3 или LuaJIT, пакетный менеджер luarocks с установленными пакетом luaposix и, собственно, сама библиотека ROTE [1].
ROTE устанавливается простым ./configure && make && make install. Надо отследить, чтобы она установилась туда, где её увидит система сборки. Я использую для этого ./configure --prefix=/usr. Чтобы не замусоривать систему бесхозными файлами, можно сделать пакет, для этого подойдёт программа checkinstall [5].
lua-rote добавлен в luarocks, поэтому для его установки достаточно набрать следующую команду:
$ sudo luarocks install lua-rote
Если ROTE установили в /usr/local, то об этом надо сообщить luarocks'у посредством опции:
$ sudo luarocks install lua-rote ROTE_DIR=/usr/local
Чтобы установить версию с GitHub, введите следующие команды:
$ git clone https://github.com/starius/lua-rote.git
$ cd lua-rote
$ sudo luarocks make
Чтобы устанавливать пакеты в luarocks локально (то есть в домашнюю папку пользователя, а не в системные папки), добавьте опцию --local. В таком случае потребуется изменить кое-какие переменные окружения, чтобы Lua увидел эти пакеты:
$ luarocks make --local
$ luarocks path > paths
$ echo 'PATH=$PATH:~/.luarocks/bin' >> paths
$ . paths
Вся библиотека lua-rote находится в модуле rote, так что для начала подключим его:
rote = require 'rote'
Основная часть библиотеки — класс RoteTerm, представляющий терминал.
Создадим терминал из 24 строк и 80 столбцов:
rt = rote.RoteTerm(24, 80)
Чтобы удалить терминал, надо просто удалить переменную, в которой он живёт. В Lua работает сборщик мусора, который при очередном проходе сделает всю работу по удалению.
Запустим дочерний процесс:
pid = rt:forkPty('less /some/file')
Команда запускается при помощи '/bin/sh -c'. В переменную pid попадает идентификатор дочернего процесса. Позже его можно выяснить с помощью метода childPid(). В случае ошибки метод возвращает -1. Если попытаться запустить неправильную команду, то ошибка не будет отловлена на этом уровне: shell попытается запустить её и завершится со статусом 127. Чтобы перехватывать подобные ошибки, надо устанавливать обработчик сигнала SIGCHLD. Чтобы игнорировать завершение дочерних процессов, надо установить обработчик SIGCHLD в значение SIG_IGN. В Lua всё это можно сделать с помощью библиотеки luaposix [6]:
signal = require 'posix.signal'
signal.signal(signal.SIGCHLD, function(signo)
-- do smth
end)
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
Взаимодействие с терминалом, в котором дочерний процесс завершился, не является ошибкой, хотя вряд ли имеет смысл. Тем не менее, стоит уведомить ROTE о завершении дочернего процесса, вызвав метод forsakeChild().
У терминала есть ряд методов, возвращающих его параметры и состояние:
Ещё есть метод draw для рисования содержимого терминала в окне curses:
curses = require 'posix.curses'
-- инициализация curses, см. ниже demo/boxshell.lua
window = ...
rt = ...
start_row = 0
start_col = 0
rt:draw(window, start_row, start_col)
Есть несколько методов, позволяющих менять состояние терминала напрямую:
Более важны методы, посылающие данные в дочерний процесс:
-- Отправляет последовательность ':wqn' в терминал
-- Если есть дочерний процесс, данные передаются ему.
-- Иначе данные напрямую вставляются в терминал при помощи inject()
rt:write(':wqn') -- сохраняем документ и закрываем vim
-- Отправляет нажатие клавиши дочернему процессу через write()
local keycode = string.byte('n') -- число
rt:keyPress(keycode)
Коллекцию кодов клавиш для keyPress можно найти в curses [7]. К сожалению, эти константы появляются в модуле только после инициализации curses, которую часто производить нежелательно (например, в коде тестов). Чтобы как-то жить с этим, в другом проекте был сделан костыль [8], запускающий curses в дочернем процессе через ROTE и возвращающий все константы.
Метод rt:takeSnapshot() возвращает объект-снимок, а метод rt:restoreSnapshot(snapshot) восстанавливает состояние терминала согласно снимку. Объект-снимок также удаляется автоматически сборщиком мусора.
Атрибут — это 8-битное число, в котором хранится цвет букв, цвет фона, бит полужирного текста (bold bit) и бит мигающего текста (blink bit). Порядок битов следующий:
бит: 7 6 5 4 3 2 1 0
содержимое: S F F F H B B B
| `-,-' | `-,-'
| | | |
| | | `----- 3 бита цвета фона (0 - 7)
| | `--------- бит мигающего текста
| `------------- 3 бита цвета букв (0 - 7)
`----------------- бит полужирного текста
Есть пара функций для упаковки и распаковки значения атрибута:
foreground, background, bold, blink = rote.fromAttr(attr)
attr = rote.toAttr(foreground, background, bold, blink)
-- foreground и background - числа (0 - 7)
-- bold и blink - логические переменные
Коды цветов:
В модуле rote есть таблицы перевода между кодами цветов и названиями цветов:
rote.color2name[2] -- возвращает "green"
rote.name2color.green -- возвращает 2

А ещё я занимаюсь биоинформатикой :)
Давно хотелось иметь программу для просмотра выравниваний вроде Jalview [9], но прямо в терминале, так как часто файлы находятся на сервере, к которому я подключён через ssh. В таких случаях нужно что-то вроде less для fasta-файлов. Всё, что мне удалось найти на эту тему, — программа tview [10] для просмотра ридов, но это немного не то.
В результате я написал программу alnbox [11], которая именно это и делает: показывает выравнивание ДНК в curses, позволяет «ходить» по нему стрелочками, перемещаться в начало и в конец. Названия последовательностей отображаются слева, номера позиций — сверху, консенсус — снизу. Код написан несколько шире, поэтому может пригодиться не только для выравниваний, но и любых less-подобных программ с заголовками вдоль всех 4-ех сторон терминала. Весь код программы написан на Lua, без использования C.
С помощью lua-rote и busted [3] написаны тесты для alnbox [12], в которых проигрываются все возможные варианты работы с программой. За основу кода интеграции тестов в Travis CI взят костяк lua-travis-example [13] от moteus [14].
Проект пока незавершён, но смотреть выравнивания уже можно. Зависимости те же + сам lua-rote. Для установки наберите команду luarocks make.
Вместе с библиотекой ROTE распространяется файл demo/boxshell.c [15]. Это по сути терминал в терминале: bash запускается внутри ROTE, а состояние ROTE рисуется в curses при помощи метода draw(). Этот пример я перенёс [16] в Lua. В начале статьи показан пример работы в этом терминале.
В Lua-версию boxshell внесено несколько исправлений. Во-первых, можно запустить в качестве дочернего процесса любую команду, а не только bash. Во-вторых, переделано чтение нажатых клавиш от пользователя: вместо nodelay используется halfdelay, то есть ожидание нажатия клавиши с таймаутом. Благодаря этому нагрузка на процессор со стороны boxshell снижена с 100% до менее чем 1%.
Сообщить о баге [18]
Исходный код ROTE [1] был написан в 2004 году Бруно Т. К. де Оливейра (Bruno T. C. de Oliveira) и опубликован под лицензией GNU Lesser General Public License 2.1. Исходный код lua-rote [19] опубликован под той же лицензией. Автор ROTE пишет, что разработка библиотеки завершена и обновления стоит искать в библиотеке libvterm [20], которая основана на ROTE. Есть ещё один проект [21] с названием libvterm, который развивается активнее и есть модификация [22] для проекта NeoVim. Для моих текущих целей ROTE хватило, и она выглядит более простой, поэтому пока я остановился именно на ней. Возможно, потом перейду к одному из libvterm.
Автор: starius
Источник [27]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/bioinformatika/87202
Ссылки в тексте:
[1] ROTE: http://rote.sourceforge.net/
[2] терминала VT100: https://en.wikipedia.org/wiki/VT100
[3] как: http://olivinelabs.com/busted/
[4] lua-rote: https://starius.github.io/lua-rote/
[5] checkinstall: https://wiki.debian.org/CheckInstall
[6] luaposix: https://luaposix.github.io/luaposix/modules/posix.signal.html
[7] curses: https://luaposix.github.io/luaposix/modules/posix.curses.html#Constants
[8] костыль: https://github.com/starius/alnbox/blob/master/src/alnbox/cursesConsts.lua
[9] Jalview: http://www.jalview.org/
[10] tview: http://samtools.sourceforge.net/tview.shtml
[11] alnbox: https://github.com/starius/alnbox
[12] тесты для alnbox: https://github.com/starius/alnbox/tree/master/spec
[13] lua-travis-example: https://github.com/moteus/lua-travis-example
[14] moteus: https://github.com/moteus
[15] demo/boxshell.c: https://gist.github.com/starius/c57f0f352fa0775cb91f
[16] перенёс: https://github.com/starius/lua-rote/blob/master/demo/boxshell.lua
[17] чудить: https://travis-ci.org/starius/lua-rote/jobs/54479120#L1160
[18] Сообщить о баге: https://github.com/starius/lua-rote/issues/new
[19] Исходный код lua-rote: https://github.com/starius/lua-rote
[20] libvterm: https://sourceforge.net/projects/libvterm//
[21] ещё один проект: http://www.leonerd.org.uk/code/libvterm
[22] модификация: https://github.com/neovim/libvterm
[23] Home page: https://starius.github.io/lua-rote
[24] Сообщение в списке рассылки lua-l: http://lua-users.org/lists/lua-l/2015-03/msg00325.html
[25] Ветка обсуждения на Reddit: https://www.reddit.com/r/lua/comments/30ast4/ann_luarote_lua_binding_to_rote_terminal/
[26] ROTE: http://rote.sourceforge.net
[27] Источник: http://habrahabr.ru/post/254089/
Нажмите здесь для печати.