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

Библиотека эмуляции терминала ROTE и Lua привязки

boxshell

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().

Чтение содержимого терминала

У терминала есть ряд методов, возвращающих его параметры и состояние:

  • rt:rows() и rt:cols() — число строк и столбцов терминала
  • rt:row() и rt:col() — текущие координаты курсора
  • rt:update() — применяет изменения, пришедшие от дочернего процесса; вызывать перед чтением содержимого терминала
  • rt:cellChar(row, col) — символ ячейки (row, col) в форме строки длины 1
  • rt:cellAttr(row, col) — атрибуты ячейки (row, col) в форме числа (см. ниже, что с ним делать)
  • rt:attr() — текущие атрибуты, которые применяются к новым символам
  • rt:rowText(row) — строка терминала номер row, без "n" на конце
  • rt:termText() — строка, представляющая весь терминал; ряды завершаются "n"

Ещё есть метод 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)

Запись в терминал

Есть несколько методов, позволяющих менять состояние терминала напрямую:

  • rt:setCellChar(row, col, character) — заменяет символ ячейки (row, col)
  • rt:setCellAttr(row, col, attr) — заменяет атрибуты ячейки (row, col)
  • rt:setAttr(attr) — меняет текущие атрибуты, которые применяются к новым символам
  • rt:inject(data) — вводит данные в терминал

Более важны методы, посылающие данные в дочерний процесс:

-- Отправляет последовательность ':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 - логические переменные

Коды цветов:

  • 0 = черный
  • 1 = красный
  • 2 = зеленый
  • 3 = желтый
  • 4 = синий
  • 5 = фиолетовый
  • 6 = голубой
  • 7 = белый

В модуле rote есть таблицы перевода между кодами цветов и названиями цветов:

rote.color2name[2] -- возвращает "green"
rote.name2color.green -- возвращает 2

Пример использования

DNA alignment

А ещё я занимаюсь биоинформатикой :)

Давно хотелось иметь программу для просмотра выравниваний вроде 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%.

Баги

  • Нет поддержки юникода.
  • Метод draw() может чудить [17] при запуске в Travis CI. Воспроизвести этот баг у себя не удаётся. Точной причины я не знаю, но подозреваю, что дело в особенностях терминала, который предоставляет Travis CI.
  • Возвращает неправильные данные, если у терминала мало столбцов (пример: терминал 1x2).

Сообщить о баге [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/