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

starius 27 марта 2015 в 15:50 5,9k
boxshell

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) — вводит данные в терминал

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

-- Отправляет последовательность ':wq\n' в терминал
-- Если есть дочерний процесс, данные передаются ему.
-- Иначе данные напрямую вставляются в терминал при помощи inject()
rt:write(':wq\n') -- сохраняем документ и закрываем vim

-- Отправляет нажатие клавиши дочернему процессу через write()
local keycode = string.byte('\n') -- число
rt:keyPress(keycode)

Коллекцию кодов клавиш для keyPress можно найти в curses. К сожалению, эти константы появляются в модуле только после инициализации curses, которую часто производить нежелательно (например, в коде тестов). Чтобы как-то жить с этим, был сделан модуль rote.cursesConsts, запускающий 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, но прямо в терминале, так как часто файлы находятся на сервере, к которому я подключён через 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.

Ссылки


Проголосовать:
+10
Сохранить: