Skip to content

A small, simple, useful terminal emulation library.

Notifications You must be signed in to change notification settings

deadpixi/libtmt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

63 Commits
 
 
 
 
 
 

Repository files navigation

libtmt - a simple terminal emulation library

libtmt is the Tiny Mock Terminal Library. It provides emulation of a classic smart 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 goal for libtmt is to for terminal emulators and multiplexers; it provides the terminal emulation layer for the mtm terminal multiplexer, for example. Other uses include screen-scraping and automated test harnesses.

libtmt is similar in purpose to libtsm, but considerably smaller (500 lines versus 6500 lines). libtmt is also, in this author's humble opinion, considerably easier to use.

Major Features and Advantages

Works Out-of-the-Box
libtmt emulates a well-known terminal type (ansi), the definition of which has been in the terminfo database since at least 1995. There's no need to install a custom terminfo entry. There's no claiming to be an xterm but only emulating a small subset of its features. Any program using terminfo works automatically: this includes vim, emacs, mc, cmus, nano, nethack, ...
Portable
Written in pure C99. Optionally, the POSIX-mandated wcwidth function can be used, which provides minimal support for combining characters.
Small
Less than 500 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 8 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.

How to Use libtmt

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.

Example Code

Below is a simple program fragment giving the flavor of libtmt. Note that another good example is the mtm terminal multiplexer:

#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;
    }
}

Data Types and Enumerations

/* 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_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
 * 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 */
};

Functions

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. 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).

Special Keys

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.

Compile-Time Options

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.

Alternate Character Set

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.

Supported Input and Escape Sequences

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.

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 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).

Additionally, "?" characters are stripped out of escape sequence parameter lists for compatibility purposes.

Known Issues

  • 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.

Frequently Asked Questions

What programs work with libtmt?

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.

I've tested quite a few applications in libtmt and they've worked flawlessly: vim, GNU emacs, nano, cmus, mc (Midnight Commander), and others just work with no changes.

What programs don't work with libtmt?

Breakage with libtmt is of two kinds: breakage due to assuming a terminal type, and reduced functionality.

In all my testing, I only found one program that didn't work correctly by default with libtmt: recent versions of Debian's apt assume a terminal with definable scrolling regions to draw a fancy progress bar during package installation. Using apt in its default configuration in libtmt will result in a corrupted display (that can be fixed by clearing the screen).

In my honest opinion, this is a bug in apt: it shouldn't assume the type of terminal it's running in.

The second kind of breakage is when not all of a program's features are available. The biggest missing feature here is mouse support: libtmt doesn't, and probably never will, support mouse tracking. I know of many programs that can use mouse tracking in a terminal, but I don't know of any that require it. Most (if not all?) programs of this kind would still be completely usable in libtmt.

License

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.

About

A small, simple, useful terminal emulation library.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages