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

boxshell

ROTE — проста бібліотека на мові C, що служить для емуляції терміналу VT100. Вона створює термінал і надає доступ до його стану у вигляді структури мови C. У терміналі можна запустити дочірній процес, «натискати» в ньому клавіші і дивитися, що він малює на терміналі. Крім того, є функція для відтворення стану терміналу у вікні curses.

Навіщо на практиці може знадобитися емулювати термінал і взаємодіяти через нього з дочірнім процесом? В першу чергу це потрібно для автоматичного тестування програм, що малюють щось на терміналі за допомогою curses, на мою думку. Як інакше написати тести для програми, яка чекає, що користувач натисне клавішу, і виводить результати в певне місце екрану засобами curses?

Незважаючи на всю зручність і внутрішню красу ROTE, використовувати її безпосередньо в тестах було б громіздко. Тому я вирішив спростити завдання, прив'язавши ROTE до мови Lua, який я дуже люблю і знаю, <a href=«olivinelabs.com/busted/> писати тести. Так і народилася бібліотека 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 make luarocks

Щоб встановлювати пакети в 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(сигнал.SIGCHLD, function(signo)
-- do smth
end)

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, яку часто проводити небажано (наприклад, у коді тестів). Щоб якось жити з цим, в іншому проекті був зроблений милицю, запускає 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.

Посилання

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.