Introducing lua-rote, Lua binding to ROTE, Terminal Emulation library
28 Mar 2015ROTE is a simple C library for VT102 terminal emulation. It allows the programmer to set up virtual ‘screens’ and send them data. The virtual screens will emulate the behavior of a VT102 terminal, interpreting escape sequences, control characters and such. The library supports ncurses as well so that you may render the virtual screen to the real screen when you need to.
There are several programs that do terminal emulation, such as xterm, rxvt, screen and even the Linux console driver itself. However, it is not easy to isolate their terminal emulation logic and put it in a module that can be easily reused in other programs. That’s where the ROTE library comes in.
The goal of the lua-rote library is to provide terminal emulation support for Lua applications, making it possible to write programs that display terminals in embedded windows within them, or even monitor the display produced by other programs. The lua-rote library depend only on Lua, ROTE itself, ncurses and luaposix.
The ROTE library is able to render the virtual screens to the physical screen (actually any ncurses window) and can also translate ncurses key codes to the escape sequences the Linux console would have produced (and feed them into the terminal). Using ncurses is not mandatory however, and ROTE will work fine without it, but in that case the application must take care of drawing the terminal to the screen in whichever way it sees fit.
ROTE also encapsulates the functionality needed to execute a child process using the virtual screen as the controlling terminal. It will handle the creation of the pseudo-terminal and the child process. All the application has to do is tell it the command to run in the terminal and call an update function at regular intervals to allow the terminal to update itself.
ROTE is extremely useful to programmatically interact with curses applications (e.g., for unit testing).
Prerequisites
- Lua 5.1, 5.2, 5.3 or LuaJIT
- curses (binary + headers)
- luaposix (install after installing curses headers!)
- ROTE (install after installing curses headers!)
Curses and luaposix are needed for drawing state of ROTE terminal on curses’ WINDOW object (method RoteTerm:draw()). If you do not need this feature and want to exclude these two dependencies, then remove CURSES and luaposix from file
lua-rote-*.rockspec
.
See shell script with installation commands for Debian Wheezy.
Installation
This library is built using LuaRocks.
Option 1: install from LuaRocks server
$ luarocks install lua-rote
If you have installed ROTE to prefix other than “/usr”, you have to provide this path to LuaRocks. For example, if you have installed ROTE to “/usr/local”, use the following command:
$ luarocks install lua-rote ROTE_DIR=/usr/local
Option 2: install from local source tree
$ git clone https://github.com/starius/lua-rote.git
$ cd lua-rote
$ luarocks make
Running unit tests
Unit tests are written using unit testing framework busted. Unit tests can serve as reference documentation and code examples.
To run unit tests, install busted from LuaRocks:
$ luarocks install busted
Go to the source folder of lua-rote and run command busted
:
$ busted
++++++++++++++++++++++++++++++++
32 successes / 0 failures / 0 errors / 0 pending : 1.5 seconds
Running the demo
Program boxshell.lua is a clone of ROTE’s example program “boxshell.c” (file “demo/boxshell.c” in ROTE’s source tree). Both programs include the following steps:
- start curses,
- fill the screen with blue,
- create curses window in the middle of the screen,
- start ROTE terminal, fork bash inside,
- do in a loop until child process dies:
- redraw curses window accorsing to ROTE terminal,
getch()
, results of which are passed to ROTE terminal.
Run lua demo/boxshell.lua
, ls
, busted
:
Currently lua-rote does not support unicode characters, that is why busted was changed to produce “+” instead of “●”.
There are some differences between boxshell.c and boxshell.lua. Program boxshell.lua can fork other commands as well as bash. boxshell.c uses
nodelay
mode repeating draw-getch cycle without a delay, while boxshell.lua useshalfdelay
mode repeating draw-getch cycle 10 times a second. That is why boxshell.c constantly consumes 100% CPU, while boxshell.lua consumes almost no CPU when inactive.
Reference
Module rote
Library lua-rote is loaded from module “rote”:
rote = require 'rote'
All code of the library “lives” inside this module.
Class RoteTerm
The main part of the library is class RoteTerm. It wraps C structure RoteTerm, declared in library ROTE. RoteTerm represents terminal emulator.
Create a new virtual terminal with the given dimensions. (Height is 24 rows, width is 80 columns.)
rt = rote.RoteTerm(24, 80)
Instance of RoteTerm is destroyed automatically when the corresponding Lua object is collected.
Start child process
Start a forked process in the terminal:
pid = rt:forkPty('less /some/file')
The command will be interpreted by ‘/bin/sh -c’.
Returns PID of the child process.
On error returns -1
.
Notice that passing an invalid command will not cause
an error at this level: the shell will try to execute
the command and will exit with status 127. You can catch
that by installing a SIGCHLD
handler if you want.
If you want to be notified when child processes exits, you should handle the
SIGCHLD
signal. If, on the other hand, you want to ignore exitting child processes, you should set theSIGCHLD
handler toSIG_IGN
to prevent child processes from hanging around the system as ‘zombie processes’.You can use luaposix to manage child processes as described above. See file demo/boxshell.lua.
Continuing to write to a RoteTerm whose child process has died does not accomplish a lot, but is not an error and should not cause your program to crash or block indefinitely or anything of that sort :-)
If, however, you want to be tidy and inform the RoteTerm that its child has died, call method
forsakeChild
when appropriate.
You can get the PID later by calling rt:childPid()
.
Disconnect the RoteTerm from its forked child process:
rt:forsakeChild()
Getting contents of the terminal
You can get number of rows and columns of the terminal:
print(rt:rows()) -- integer
print(rt:cols()) -- integer
Get cursor coordinates:
print(rt:row()) -- integer
print(rt:col()) -- integer
Before getting any output from the child process, call method
rt:update()
to update internal state of RoteTerm.
You can get value of character and attribute of any cell:
row = 0
col = 0
print(rt:cellChar(row, col)) -- string of length 1
attr = rt:cellAttr(row, col) -- integer
lua-rote provides several functions to handle attribute values.
Get current attribute, that is the attribute that will be used for newly characters:
print(rt:attr()) -- integer
Get a row as a string (not terminated with \n
):
row = 0
print(rt:rowText(row)) -- string
Get whole terminal as a string (rows are terminated with \n
):
print(rt:termText()) -- string
Draw contents of ROTE terminal on curses WINDOW:
curses = require 'posix.curses'
-- setup curses, see demo/boxshell.lua
window = ...
rt = ...
start_row = 0
start_col = 0
rt:draw(window, start_row, start_col)
Changing the terminal state
You can directly change internal state of RoteTerm by calling the following methods:
rt:setCellChar(row, col, character) -- character at (row, col)
rt:setCellAttr(row, col, attr) -- attribute at (row, col)
rt:setAttr(attr) -- current attribute
You can pass data to the child process or to the terminal:
-- Puts data ':wq\n' into the terminal.
-- If there is a forked process, the data will be sent to it.
-- If there is no forked process, the data will simply
-- be injected into the terminal (as in inject()).
rt:write(':wq\n')
-- Inject data directly into the terminal.
rt:inject(':wq\n')
-- Indicates to the terminal that the key has been pressed.
-- Appropriate escape sequence is passed to method write().
local keycode = string.byte('\n') -- integer
rt:keyPress(keycode)
You can get values of keycodes from posix.curses.
Snapshots
-- take a snapshot of the current contents of the terminal
snapshot = rt:takeSnapshot()
-- ... do something ...
-- restore a snapshot previously taken
rt:restoreSnapshot(snapshot)
Snapshot object is deleted automatically when the corresponding Lua object is collected.
Handling attributes
An ‘attribute’ as used in this library means an 8-bit value that conveys a foreground color code, a background color code, and the bold and blink bits. Each cell in the virtual terminal screen is associated with an attribute that specifies its appearance.
The bits of an attribute, from most significant to least significant, are
bit: 7 6 5 4 3 2 1 0
content: S F F F H B B B
| `-,-' | `-,-'
| | | |
| | | `----- 3-bit background color (0 - 7)
| | `--------- blink bit
| `------------- 3-bit foreground color (0 - 7)
`----------------- bold bit
Color codes:
- 0 = black,
- 1 = red,
- 2 = green,
- 3 = yellow,
- 4 = blue,
- 5 = magenta,
- 6 = cyan,
- 7 = white.
There are functions provided to “pack” and “unpack” attribute bits:
foreground, background, bold, blink = rote.fromAttr(attr)
attr = rote.toAttr(foreground, background, bold, blink)
-- foreground and background are integers (0 - 7)
-- bold and blink are booleans
The library provides tables converting color codes to and from human readable names:
print(rote.color2name[2]) -- prints "green"
print(rote.name2color.green) -- prints "2"
Bugs
- Unicode characters are printed and read with errors.
- Method
RoteTerm:draw()
is unreliable. - ROTE can’t read cell 0x0 in 1x2 window when reads second time. It seems to be related to low number of columns.
Author
Corresponding author: Boris Nagaev, email: bnagaev@gmail.com
Copyright (C) 2015 Boris Nagaev
See the LICENSE file for terms of use.
ROTE was written by Bruno T. C. de Oliveira, see rote.sourceforge.net for more information.