ROTE and Lua binding terminal emulation library
ROTE is a simple C library used to emulate the VT100 terminal . She creates a terminal and provides access to its state in the form of the C language structure. In the terminal, you can start a child process, “press” keys in it and watch what it draws on the terminal. In addition, there is a function for displaying the state of the terminal in the curses window.
Why in practice may you need to emulate a terminal and interact through it with a child process? This is primarily necessary for automatic testing of programs that draw something on the terminal using curses, in my opinion. How else to write tests for a program that expects the user to press a key and displays the results in a specific place on the screen using curses?
Despite all the convenience and inner beauty of ROTE, using it directly in tests would be cumbersome. So I decided to simplify the task by linking ROTE to the Lua language, which I really love and know how to write tests. And so the lua-rote library was born , which I want to talk about.
You will need Linux, curses, Lua versions 5.1 to 5.3, or LuaJIT, the luarocks package manager with the luaposix package installed, and, in fact, the ROTE library itself .
ROTE installs simple ./configure && make && make install. It must be tracked so that it is installed where the assembly system sees it. I use ./configure --prefix = / usr for this. In order not to litter the system with orphaned files, you can make a package, the checkinstall program is suitable for this .
lua-rote has been added to luarocks, so to install it, just type the following command:
$ sudo luarocks install lua-rote
If ROTE is installed in / usr / local, then this should be reported to luarocks via the option:
$ sudo luarocks install lua-rote ROTE_DIR=/usr/local
To install the version from GitHub, enter the following commands:
$ git clone https://github.com/starius/lua-rote.git $ cd lua-rote $ sudo luarocks make
To install packages in luarocks locally (that is, in the user's home folder, and not in the system folders), add the --local option. In this case, you need to change some environment variables so that Lua sees these packages:
$ luarocks make --local $ luarocks path > paths $ echo 'PATH=$PATH:~/.luarocks/bin' >> paths $ . paths
The entire lua-rote library is located in the rote module, so for starters we’ll plug it in:
rote = require 'rote'
The main part of the library is the RoteTerm class, which represents the terminal.
Create a terminal with 24 rows and 80 columns:
rt = rote.RoteTerm(24, 80)
To delete a terminal, you just need to delete the variable in which it lives. Lua has a garbage collector that will do all the removal work on the next pass.
Run the child process:
pid = rt:forkPty('less /some/file')
The command is launched using '/ bin / sh -c'. The identifier of the child process falls into the pid variable. You can find out later using the childPid () method. In case of an error, the method returns -1. If you try to run the wrong command, the error will not be caught at this level: the shell will try to run it and exit with status 127. To catch such errors, you must install the SIGCHLD signal handler. To ignore the termination of child processes, you must set the SIGCHLD handler to SIG_IGN. In Lua, all this can be done using the luaposix library :
signal = require 'posix.signal' signal.signal(signal.SIGCHLD, function(signo) -- do smth end) signal.signal(signal.SIGCHLD, signal.SIG_IGN)
Interaction with the terminal where the child process terminated is not an error, although it hardly makes sense. However, it is worth notifying ROTE of the completion of the child process by calling the forsakeChild () method.
Reading terminal contents
The terminal has a number of methods that return its parameters and state:
- rt: rows () and rt: cols () - the number of rows and columns of the terminal
- rt: row () and rt: col () are the current coordinates of the cursor
- rt: update () - applies the changes that came from the child process; call before reading the contents of the terminal
- rt: cellChar (row, col) - cell symbol (row, col) in the form of a string of length 1
- rt: cellAttr (row, col) - attributes of the cell (row, col) in the form of a number (see below what to do with it)
- rt: attr () - current attributes that apply to new characters
- rt: rowText (row) - terminal line number row, without "\ n" at the end
- rt: termText () - a string representing the entire terminal; rows end with "\ n"
There is also a draw method for drawing the contents of the terminal in the curses window:
curses = require 'posix.curses' -- инициализация curses, см. ниже demo/boxshell.lua window = ... rt = ... start_row = 0 start_col = 0 rt:draw(window, start_row, start_col)
Writing to the terminal
There are several methods that allow you to change the status of the terminal directly:
- rt: setCellChar (row, col, character) - replaces the character of the cell (row, col)
- rt: setCellAttr (row, col, attr) - replaces the attributes of the cell (row, col)
- rt: setAttr (attr) - changes the current attributes that apply to new characters
- rt: inject (data) - enters data into the terminal
More important are the methods that send data to the child process:
-- Отправляет последовательность ':wq\n' в терминал -- Если есть дочерний процесс, данные передаются ему. -- Иначе данные напрямую вставляются в терминал при помощи inject() rt:write(':wq\n') -- сохраняем документ и закрываем vim -- Отправляет нажатие клавиши дочернему процессу через write() local keycode = string.byte('\n') -- число rt:keyPress(keycode)
A collection of key codes for keyPress can be found in curses . Unfortunately, these constants appear in the module only after curses initialization, which is often undesirable (for example, in test code). To somehow live with this, the rote.cursesConsts module was made , which starts curses in a child process via ROTE and returns all constants.
The rt: takeSnapshot () method returns a snapshot object, and the rt: restoreSnapshot (snapshot) method restores the terminal state according to the snapshot. The snapshot object is also deleted automatically by the garbage collector.
Attributes and colors
An attribute is an 8-bit number that stores letter color, background color, bold bit and blink bit. The order of the bits is as follows:
бит: 7 6 5 4 3 2 1 0 содержимое: S F F F H B B B | `-,-' | `-,-' | | | | | | | `----- 3 бита цвета фона (0 - 7) | | `--------- бит мигающего текста | `------------- 3 бита цвета букв (0 - 7) `----------------- бит полужирного текста
There are a couple of functions for packing and unpacking an attribute value:
foreground, background, bold, blink = rote.fromAttr(attr) attr = rote.toAttr(foreground, background, bold, blink) -- foreground и background - числа (0 - 7) -- bold и blink - логические переменные
- 0 = black
- 1 = red
- 2 = green
- 3 = yellow
- 4 = blue
- 5 = purple
- 6 = blue
- 7 = white
The rote module has translation tables between color codes and color names:
rote.color2name -- возвращает "green" rote.name2color.green -- возвращает 2
And I also do bioinformatics :)
For a long time I wanted to have a program for viewing alignments like Jalview , but right in the terminal, because often the files are on the server to which I am connected via ssh. In such cases, you need something like less for fasta files. All I could find on this topic was the tview program for viewing reads, but that’s a bit different.
As a result, I wrote the alnbox program, which does just that: it shows the alignment of DNA in curses, allows you to "walk" along it with arrows, move to the beginning and end. The names of the sequences are displayed on the left, the position numbers on the top, the consensus on the bottom. The code is written a little wider, so it can be useful not only for alignments, but also for any less-like programs with headers along all 4 sides of the terminal. All program code is written in Lua, without using C.
With lua-rote and busted , tests for alnbox are written , in which all possible variants of working with the program are played. The basis of the test integration code in Travis CI is the backbone of lua-travis-example from moteus .
The project is still incomplete, but you can already see the alignment. Dependencies are the same + lua-rote itself. To install, type the luarocks make command.
Another use case
Together with the ROTE library, the demo / boxshell.c file is distributed . This is essentially a terminal in the terminal: bash runs inside ROTE, and the ROTE state is drawn in curses using the draw () method. I moved this example to Lua. At the beginning of the article an example of work in this terminal is shown.
Several corrections have been made to the Lua version of boxshell. First, you can run any command as a child process, not just bash. Secondly, the reading of keystrokes from the user has been redone: instead of nodelay, halfdelay is used, that is, waiting for a keystroke with a timeout. Thanks to this, the processor load from boxshell is reduced from 100% to less than 1%.
- No unicode support.
- The draw () method can get weird when launched in Travis CI. It is not possible to reproduce this bug at home. I don’t know the exact reason, but I suspect that this is due to the features of the terminal that Travis CI provides.
- Returns incorrect data if the terminal has few columns (example: terminal 1x2).
Report a bug
The ROTE source code was written in 2004 by Bruno TC de Oliveira and published under the GNU Lesser General Public License 2.1. The source code for lua-rote is published under the same license. The author of ROTE writes that the development of the library is completed and updates are worth looking for in the libvterm library , which is based on ROTE. There is another project called libvterm, which is developing more actively and there is a modification for the NeoVim project. For my current purposes, ROTE was enough, and it looks simpler, so for now I have focused on it. Maybe then I will move on to one of libvterm.