libtmt is the Tiny Mock Terminal Library. It provides emulation of a text terminal by maintaining an in-memory screen image. Sending text and command sequences to libtmt causes it to update this in-memory image, which can then be examined and rendered however the user sees fit.
The imagined primary use cases for libtmt are terminal emulators and multiplexers. Other uses include screen-scraping and automated test harnesses.
Note! libtmt was originally written by Rob King. This is an updated fork.
- Works Out-of-the-Box
- While libtmt does not currently emulate any other terminal with 100% fidelity, it does a pretty decent job of emulating the well-known linux terminal type. A definition of this terminal has been in the terminfo database since 2000 or before, so there is no need to install a custom terminfo entry. In addition, libtmt supports a number of terminal control sequences from other popular terminals (e.g., xterm, vt102). The emulation is good enough to work with vim, nano, dialog, and tmux, for example.
- Portable
- Written in pure C99. Optionally, the POSIX-mandated wcwidth function can be used, which provides minimal support for combining characters.
- Small
- Less than 1000 lines of C, including comments and whitespace.
- Free
- Released under a BSD-style license, free for commercial and non-commerical use, with no restrictions on source code release or redistribution.
- Simple
- Only 9 functions to learn, and really you can get by with 6!
- International
- libtmt internally uses wide characters exclusively, and uses your C library's multibyte encoding functions. This means that the library automatically supports any encoding that your operating system does.
libtmt is a single C file and a single header. Just include these files in your project and you should be good to go.
By default, libtmt uses only ISO standard C99 features, but see Compile-Time Options below.
Below is a simple program fragment giving the flavor of libtmt. Note that another good example is the lilt terminal emulator:
#include <stdio.h>
#include <stdlib.h>
#include "tmt.h"
/* Forward declaration of a callback.
* libtmt will call this function when the terminal's state changes.
*/
void callback(tmt_msg_t m, TMT *vt, const void *a, void *p);
int
main(void)
{
/* Open a virtual terminal with 2 lines and 10 columns.
* The first NULL is just a pointer that will be provided to the
* callback; it can be anything. The second NULL specifies that
* we want to use the default Alternate Character Set; this
* could be a pointer to a wide string that has the desired
* characters to be displayed when in ACS mode.
*/
TMT *vt = tmt_open(2, 10, callback, NULL, NULL);
if (!vt)
return perror("could not allocate terminal"), EXIT_FAILURE;
/* Write some text to the terminal, using escape sequences to
* use a bold rendition.
*
* The final argument is the length of the input; 0 means that
* libtmt will determine the length dynamically using strlen.
*/
tmt_write(vt, "\033[1mhello, world (in bold!)\033[0m", 0);
/* Writing input to the virtual terminal can (and in this case, did)
* call the callback letting us know the screen was updated. See the
* callback below to see how that works.
*/
tmt_close(vt);
return EXIT_SUCCESS;
}
void
callback(tmt_msg_t m, TMT *vt, const void *a, void *p)
{
/* grab a pointer to the virtual screen */
const TMTSCREEN *s = tmt_screen(vt);
const TMTPOINT *c = tmt_cursor(vt);
switch (m){
case TMT_MSG_BELL:
/* the terminal is requesting that we ring the bell/flash the
* screen/do whatever ^G is supposed to do; a is NULL
*/
printf("bing!\n");
break;
case TMT_MSG_UPDATE:
/* the screen image changed; a is a pointer to the TMTSCREEN */
for (size_t r = 0; r < s->nline; r++){
if (s->lines[r]->dirty){
for (size_t c = 0; c < s->ncol; c++){
printf("contents of %zd,%zd: %lc (%s bold)\n", r, c,
s->lines[r]->chars[c].c,
s->lines[r]->chars[c].a.bold? "is" : "is not");
}
}
}
/* let tmt know we've redrawn the screen */
tmt_clean(vt);
break;
case TMT_MSG_ANSWER:
/* the terminal has a response to give to the program; a is a
* pointer to a string */
printf("terminal answered %s\n", (const char *)a);
break;
case TMT_MSG_MOVED:
/* the cursor moved; a is a pointer to the cursor's TMTPOINT */
printf("cursor is now at %zd,%zd\n", c->r, c->c);
break;
}
}
/* an opaque structure */
typedef struct TMT TMT;
/* possible messages sent to the callback */
typedef enum{
TMT_MSG_MOVED, /* the cursor changed position */
TMT_MSG_UPDATE, /* the screen image changed */
TMT_MSG_ANSWER, /* the terminal responded to a query */
TMT_MSG_BELL /* the terminal bell was rung */
TMT_MSG_CURSOR, /* cursor visibility changed */
TMT_MSG_SETMODE, /* a terminal mode is being enabled */
TMT_MSG_UNSETMODE, /* a terminal mode is being disabled */
} tmt_msg_T;
/* a callback for the library
* m is one of the message constants above
* vt is a pointer to the vt structure
* r is NULL for TMT_MSG_BELL
* is a pointer to the cursor's TMTPOINT for TMT_MSG_MOVED
* is a pointer to the terminal's TMTSCREEN for TMT_MSG_UPDATE
* is a pointer to a string for TMT_MSG_ANSWER
* is a pointer to "t" or "f" for TMT_MSG_CURSOR
* is a pointer to args of the mode for TMT_MSG_UNSETMODE
* p is whatever was passed to tmt_open (see below).
*/
typedef void (*TMTCALLBACK)(tmt_msg_t m, struct TMT *vt,
const void *r, void *p);
/* color definitions */
typedef enum{
TMT_COLOR_BLACK,
TMT_COLOR_RED,
TMT_COLOR_GREEN,
TMT_COLOR_YELLOW,
TMT_COLOR_BLUE,
TMT_COLOR_MAGENTA,
TMT_COLOR_CYAN,
TMT_COLOR_WHITE,
TMT_COLOR_DEFAULT /* whatever the host terminal wants it to mean */
} tmt_color_t;
/* graphical rendition */
typedef struct TMTATTRS TMTATTRS;
struct TMTATTRS{
bool bold; /* character is bold */
bool dim; /* character is half-bright */
bool underline; /* character is underlined */
bool blink; /* character is blinking */
bool reverse; /* character is in reverse video */
bool invisible; /* character is invisible */
tmt_color_t fg; /* character foreground color */
tmt_color_t bg; /* character background color */
};
/* characters */
typedef struct TMTCHAR TMTCHAR;
struct TMTCHAR{
wchar_t c; /* the character */
TMTATTRS a; /* its rendition */
};
/* a position on the screen; upper left corner is 0,0 */
typedef struct TMTPOINT TMTPOINT;
struct TMTPOINT{
size_t r; /* row */
size_t c; /* column */
};
/* a line of characters on the screen;
* every line is always as wide as the screen
*/
typedef struct TMTLINE TMTLINE;
struct TMTLINE{
bool dirty; /* line has changed since it was last drawn */
TMTCHAR chars[]; /* the contents of the line */
};
/* a virtual terminal screen image */
typedef struct TMTSCREEN TMTSCREEN;
struct TMTSCREEN{
size_t nline; /* number of rows */
size_t ncol; /* number of columns */
TMTLINE **lines; /* the lines on the screen */
};
- TMT *tmt_open(size_t nrows, size_t ncols, TMTCALLBACK cb, VOID *p, const wchar *acs);
Creates a new virtual terminal, with nrows rows and ncols columns. The callback cb will be called on updates, and passed p as a final argument. See the definition of tmt_msg_t above for possible values of each argument to the callback.
Terminals must have a size of at least two rows and two columns.
acs specifies the characters to use when in Alternate Character Set (ACS) mode and for printing DEC Special Graphics characters (there is a large amount of crossover). The default string (used if NULL is specified) is:
L"><^v#+:o##+++++~---_++++|<>*!fo"
See Alternate Character Set for more information.
Note that the callback must be ready to be called immediately, as it will be called after initialization of the terminal is done, but before the call to tmt_open returns.
- void tmt_close(TMT *vt)
- Close and free all resources associated with vt.
- bool tmt_resize(TMT *vt, size_t nrows, size_t ncols)
Resize the virtual terminal to have nrows rows and ncols columns. The contents of the area in common between the two sizes will be preserved.
Terminals must have a size of at least two rows and two columns.
If this function returns false, the resize failed (only possible in out-of-memory conditions or invalid sizes). If this happens, the terminal is trashed and the only valid operation is the close the terminal.
- void tmt_write(TMT *vt, const char *s, size_t n);
Write the provided string to the terminal, interpreting any escape sequences contained threin, and update the screen image. The last argument is the length of the input. If set to 0, the length is determined using strlen.
The terminal's callback function may be invoked one or more times before a call to this function returns.
The string is converted internally to a wide-character string using the system's current multibyte encoding. Each terminal maintains a private multibyte decoding state, and correctly handles mulitbyte characters that span multiple calls to this function (that is, the final byte(s) of s may be a partial mulitbyte character to be completed on the next call).
- const TMTSCREEN *tmt_screen(const TMT *vt);
- Returns a pointer to the terminal's screen image.
- const TMTPOINT *tmt_cursor(cosnt TMT *vt);
- Returns a pointer to the terminal's cursor position.
- void tmt_clean(TMT *vt);
- Call this after receiving a TMT_MSG_UPDATE or TMT_MSG_MOVED callback to let the library know that the program has handled all reported changes to the screen image.
- void tmt_reset(TMT *vt);
- Resets the virtual terminal to its default state (colors, multibyte decoding state, rendition, etc).
- bool tmt_set_unicode_decode(TMT *vt, bool v);
- Enables to disables Unicode mapping. If true, recognized Unicode characters will be remapped to the equivalent ACS characters.
To send special keys to a program that is using libtmt for its display, write one of the TMT_KEY_* strings to that program's standard input (not to libtmt; it makes no sense to send any of these constants to libtmt itself).
The following macros are defined, and are all constant strings:
- TMT_KEY_UP
- TMT_KEY_DOWN
- TMT_KEY_RIGHT
- TMT_KEY_LEFT
- TMT_KEY_HOME
- TMT_KEY_END
- TMT_KEY_INSERT
- TMT_KEY_BACKSPACE
- TMT_KEY_ESCAPE
- TMT_KEY_BACK_TAB
- TMT_KEY_PAGE_UP
- TMT_KEY_PAGE_DOWN
- TMT_KEY_F1 through TMT_KEY_F10
Note also that the classic PC console sent the enter key as a carriage return, not a linefeed. Many programs don't care, but some do.
There are two preprocessor macros that affect libtmt:
- TMT_INVALID_CHAR
Define this to a wide-character. This character will be added to the virtual display when an invalid multibyte character sequence is encountered.
By default (if you don't define it as something else before compiling), this is ((wchar_t)0xfffd), which is the codepoint for the Unicode 'REPLACEMENT CHARACTER'. Note that your system might not use Unicode, and its wide-character type might not be able to store a constant as large as 0xfffd, in which case you'll want to use an alternative.
- TMT_HAS_WCWIDTH
By default, libtmt uses only standard C99 features. If you define TMT_HAS_WCWIDTH before compiling, libtmt will use the POSIX wcwidth function to detect combining characters.
Note that combining characters are still not handled particularly well, regardless of whether this was defined. Also note that what your C library's wcwidth considers a combining character and what the written language in question considers one could be different.
The terminal can be switched to and from its "Alternate Character Set" (ACS) using escape sequences. The ACS traditionally contained box-drawing and other semigraphic characters.
The characters in the ACS are configurable at runtime, by passing a wide string to tmt_open. The default if none is provided (i.e. the argument is NULL) uses ASCII characters to approximate the traditional characters.
The string passed to tmt_open must be 31 characters long. The characters, and their default ASCII-safe values, are in order:
- RIGHT ARROW ">"
- LEFT ARROW "<"
- UP ARROW "^"
- DOWN ARROW "v"
- BLOCK "#"
- DIAMOND "+"
- CHECKERBOARD "#"
- DEGREE "o"
- PLUS/MINUS "+"
- BOARD ":"
- LOWER RIGHT CORNER "+"
- UPPER RIGHT CORNER "+"
- UPPER LEFT CORNER "+"
- LOWER LEFT CORNER "+"
- CROSS "+"
- SCAN LINE 1 "~"
- SCAN LINE 3 "-"
- HORIZONTAL LINE "-"
- SCAN LINE 7 "-"
- SCAN LINE 9 "_"
- LEFT TEE "+"
- RIGHT TEE "+"
- BOTTOM TEE "+"
- TOP TEE "+"
- VERTICAL LINE "|"
- LESS THAN OR EQUAL "<"
- GREATER THAN OR EQUAL ">"
- PI "*"
- NOT EQUAL "!"
- POUND STERLING "f"
- BULLET "o"
If your system's wide character type's character set corresponds to the Universal Character Set (UCS/Unicode), the following wide string is a good option to use:
L"→←↑↓■◆▒°±▒┘┐┌└┼⎺───⎽├┤┴┬│≤≥π≠£•"
Note that multibyte decoding is disabled in ACS mode. The traditional implementations of the "ansi" terminal type (i.e. IBM PCs and compatibles) had no concept of multibyte encodings and used the character codes outside the ASCII range for various special semigraphic characters. (Technically they had an entire alternate character set as well via the code page mechanism, but that's beyond the scope of this explanation.)
The end result is that the terminfo definition of "ansi" sends characters with the high bit set when in ACS mode. This breaks several multibyte encoding schemes (including, most importantly, UTF-8).
As a result, libtmt does not attempt to decode multibyte characters in ACS mode, since that would break the multibyte encoding, the semigraphic characters, or both.
In general this isn't a problem, since programs explicitly switch to and from ACS mode using escape sequences.
When in ACS mode, bytes that are not special members of the alternate character set (that is, bytes not mapped to the string provided to tmt_open) are passed unchanged to the terminal.
Internally libtmt uses your C library's/compiler's idea of a wide character for all characters, so you should be able to use whatever characters you want when writing to the virtual terminal (but see Alternate Character Set).
The following escape sequences are recognized and will be processed specially, though note that this list is not authoritative or exhaustive. At present, the real documentation is the source code.
In the descriptions below, "ESC" means a literal escape character and "Ps" means zero or more decimal numeric arguments separated by semicolons. In descriptions "P1", "P2", etc, refer to the first parameter, second parameter, and so on. If a required parameter is omitted, it defaults to the smallest meaningful value (zero if the command accepts zero as an argument, one otherwise). Any number of parameters may be passed, but any after the first eight are ignored.
Unless explicitly stated below, cursor motions past the edges of the screen are ignored and do not result in scrolling. When characters are moved, the spaces left behind are filled with blanks and any characters moved off the edges of the screen are lost.
Sequence | Action |
---|---|
0x07 (Bell) | Callback with TMT_MSG_BELL |
0x08 (Backspace) | Cursor left one cell |
0x09 (Tab) | Cursor to next tab stop or end of line |
0x0a (Carriage Return) | Cursor to first cell on this line |
0x0d (Linefeed) | Cursor to same column one line down, scroll if needed |
ESC M | Reverse linefeed, scroll if needed |
ESC H | Set a tabstop in this column |
ESC 7 | Save cursor position and current graphical state |
ESC 8 | Restore saved cursor position and current graphical state |
ESC c | Reset terminal to default state |
ESC [ Ps A | Cursor up P1 rows |
ESC [ Ps B | Cursor down P1 rows |
ESC [ Ps C | Cursor right P1 columns |
ESC [ Ps D | Cursor left P1 columns |
ESC [ Ps E | Cursor to first column of line P1 rows down from current |
ESC [ Ps F | Cursor to first column of line P1 rows up from current |
ESC [ Ps G | Cursor to column P1 |
ESC [ Ps d | Cursor to row P1 |
ESC [ Ps H | Cursor to row P1, column P2 |
ESC [ Ps f | Alias for ESC [ Ps H |
ESC [ Ps I | Cursor to next tab stop |
ESC [ Ps J | Clear screen P1 == 0: from cursor to end of screen P1 == 1: from beginning of screen to cursor P1 == 2: entire screen |
ESC [ Ps K | Clear line P1 == 0: from cursor to end of line P1 == 1: from beginning of line to cursor P1 == 2: entire line |
ESC [ Ps L | Insert P1 lines at cursor, scrolling lines below down |
ESC [ Ps M | Delete P1 lines at cursor, scrolling lines below up |
ESC [ Ps P | Delete P1 characters at cursor, moving characters to the right over |
ESC [ Ps S | Scroll screen up P1 lines |
ESC [ Ps T | Scroll screen down P1 lines |
ESC [ Ps X | Erase P1 characters at cursor (overwrite with spaces) |
ESC [ Ps Z | Go to previous tab stop |
ESC [ Ps b | Repeat previous character P1 times |
ESC [ Ps c | Callback with TMT_MSG_ANSWER "033[?6c" |
ESC [ Ps g | If P1 == 3, clear all tabstops |
ESC [ Ps h | If P1 == 25, show the cursor (if it was hidden) |
ESC [ Ps m | Change graphical rendition state; see below |
ESC [ Ps l | If P1 == 25, hide the cursor |
ESC [ Ps n | If P1 == 6, callback with TMT_MSG_ANSWER "033[%d;%dR" with cursor row, column |
ESC [ Ps s | Alias for ESC 7 |
ESC [ Ps u | Alias for ESC 8 |
ESC [ Ps @ | Insert P1 blank spaces at cursor, moving characters to the right over |
For the ESC [ Ps m escape sequence above ("Set Graphic Rendition"), up to eight parameters may be passed; the results are cumulative:
Rendition Code | Meaning |
---|---|
0 | Reset all graphic rendition attributes to default |
1 | Bold |
2 | Dim (half bright) |
4 | Underline |
5 | Blink |
7 | Reverse video |
8 | Invisible |
10 | Leave ACS mode |
11 | Enter ACS mode |
22 | Bold off |
23 | Dim (half bright) off |
24 | Underline off |
25 | Blink off |
27 | Reverse video off |
28 | Invisible off |
30 | Foreground black |
31 | Foreground red |
32 | Foreground green |
33 | Foreground yellow |
34 | Foreground blue |
35 | Foreground magenta |
36 | Foreground cyan |
37 | Foreground white |
39 | Foreground default color |
40 | Background black |
41 | Background red |
42 | Background green |
43 | Background yellow |
44 | Background blue |
45 | Background magenta |
46 | Background cyan |
47 | Background white |
49 | Background default color |
Other escape sequences are recognized but ignored. This includes escape sequences for switching out codesets (officially, all code sets are defined as equivalent in libtmt), and the various "Media Copy" escape sequences used to print output on paper (officially, there is no printer attached to libtmt).
- Combining characters are "handled" by ignoring them (when compiled with TMT_HAS_WCWIDTH) or by printing them separately.
- Double-width characters are rendered as single-width invalid characters.
- The documentation and error messages are available only in English.
Pretty much all of them. Any program that doesn't assume what terminal it's running under should work without problem; this includes any program that uses the terminfo, termcap, or (pd|n)?curses libraries. Any program that assumes it's running under some specific terminal might fail if its assumption is wrong, and not just under libtmt.
Historically, quite a few applications have been tested and found to work work, including vim, GNU emacs, nano, cmus, mc (Midnight Commander), and others. More recently, things that actually get tested decently are: vim, nano, dialog, readline, and tmux (and all of the above inside tmux).
As of this writing, there are no programs known to not work with libtmt. This almost certainly just means nobody has noticed or reported the issue -- not that such issues don't exist. Note that the correct operation of libtmt depends also on the terminfo entry being used. Changes to the terminfo can certainly break things.
Copyright (c) 2023 Murphy McCauley Copyright (c) 2017 Rob King All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of the copyright holder nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS, COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.