ROTE — простая библиотека на языке C, служащая для эмуляции терминала VT100. Она создает терминал и предоставляет доступ к его состоянию в виде структуры языка C. В терминале можно запустить дочерний процесс, «нажимать» в нем клавиши и смотреть, что он рисует на терминале. Кроме того, есть функция для отрисовки состояния терминала в окне curses.
Зачем на практике может потребоваться эмулировать терминал и взаимодействовать через него с дочерним процессом? В первую очередь это нужно для автоматического тестирования программ, рисующих что-то на терминале с помощью curses, по моему мнению. Как иначе написать тесты для программы, которая ждёт, что пользователь нажмёт клавишу, и выводит результаты в определенное место экрана средствами curses?
Несмотря на всё удобство и внутреннюю красоту ROTE, использовать её напрямую в тестах было бы громоздко. Поэтому я решил упростить задачу, привязав ROTE к языку Lua, который я очень люблю и знаю, как писать тесты. Так и родилась библиотека lua-rote, о которой я хочу рассказать.
Установка
Потребуется Linux, curses, Lua версии от 5.1 до 5.3 или LuaJIT, пакетный менеджер luarocks с установленными пакетом luaposix и, собственно, сама библиотека ROTE.
ROTE устанавливается простым ./configure && make && make install. Надо отследить, чтобы она установилась туда, где её увидит система сборки. Я использую для этого ./configure --prefix=/usr. Чтобы не замусоривать систему бесхозными файлами, можно сделать пакет, для этого подойдёт программа checkinstall.
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:
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. К сожалению, эти константы появляются в модуле только после инициализации curses, которую часто производить нежелательно (например, в коде тестов). Чтобы как-то жить с этим, в другом проекте был сделан костыль, запускающий 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
Пример использования
А ещё я занимаюсь биоинформатикой :)
Давно хотелось иметь программу для просмотра выравниваний вроде Jalview, но прямо в терминале, так как часто файлы находятся на сервере, к которому я подключён через ssh. В таких случаях нужно что-то вроде less для fasta-файлов. Всё, что мне удалось найти на эту тему, — программа tview для просмотра ридов, но это немного не то.
В результате я написал программу alnbox, которая именно это и делает: показывает выравнивание ДНК в curses, позволяет «ходить» по нему стрелочками, перемещаться в начало и в конец. Названия последовательностей отображаются слева, номера позиций — сверху, консенсус — снизу. Код написан несколько шире, поэтому может пригодиться не только для выравниваний, но и любых less-подобных программ с заголовками вдоль всех 4-ех сторон терминала. Весь код программы написан на Lua, без использования C.
С помощью lua-rote и busted написаны тесты для alnbox, в которых проигрываются все возможные варианты работы с программой. За основу кода интеграции тестов в Travis CI взят костяк lua-travis-example от moteus.
Проект пока незавершён, но смотреть выравнивания уже можно. Зависимости те же + сам lua-rote. Для установки наберите команду luarocks make.
Ещё один пример использования
Вместе с библиотекой ROTE распространяется файл demo/boxshell.c. Это по сути терминал в терминале: bash запускается внутри ROTE, а состояние ROTE рисуется в curses при помощи метода draw(). Этот пример я перенёс в Lua. В начале статьи показан пример работы в этом терминале.
В Lua-версию boxshell внесено несколько исправлений. Во-первых, можно запустить в качестве дочернего процесса любую команду, а не только bash. Во-вторых, переделано чтение нажатых клавиш от пользователя: вместо nodelay используется halfdelay, то есть ожидание нажатия клавиши с таймаутом. Благодаря этому нагрузка на процессор со стороны boxshell снижена с 100% до менее чем 1%.
Баги
- Нет поддержки юникода.
- Метод draw() может чудить при запуске в Travis CI. Воспроизвести этот баг у себя не удаётся. Точной причины я не знаю, но подозреваю, что дело в особенностях терминала, который предоставляет Travis CI.
- Возвращает неправильные данные, если у терминала мало столбцов (пример: терминал 1x2).
Исходный код ROTE был написан в 2004 году Бруно Т. К. де Оливейра (Bruno T. C. de Oliveira) и опубликован под лицензией GNU Lesser General Public License 2.1. Исходный код lua-rote опубликован под той же лицензией. Автор ROTE пишет, что разработка библиотеки завершена и обновления стоит искать в библиотеке libvterm, которая основана на ROTE. Есть ещё один проект с названием libvterm, который развивается активнее и есть модификация для проекта NeoVim. Для моих текущих целей ROTE хватило, и она выглядит более простой, поэтому пока я остановился именно на ней. Возможно, потом перейду к одному из libvterm.
Ссылки
- GitHub
- Home page
- Сообщить о баге
- Сообщение в списке рассылки lua-l
- Ветка обсуждения на Reddit
- ROTE
- Busted, фреймворк для тестирования кода на Lua
- lua-travis-example, костяк для интеграции Lua-проекта в Travis CI
Автор: starius