Boris Nagaev · Home page | About | Contact | Github | Code | FBB files

Introducing lua-rote, Lua binding to ROTE, Terminal Emulation library

ROTE 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:

boxshell.lua

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 uses halfdelay 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 the SIGCHLD handler to SIG_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.

Report a bug

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.