aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGerd Hoffmann <kraxel@redhat.com>2019-01-30 16:04:57 +0100
committerGerd Hoffmann <kraxel@redhat.com>2019-01-30 16:04:57 +0100
commitc1da53fd1611719b6daee6ab217c9ffaac9dbc00 (patch)
tree892cd0ae40dfb512de6ea0862db355b5ddd8b5d8
parentac9005bf0bbf50f14dc1b368be5084c8e0510a5d (diff)
downloadfbida-c1da53fd1611719b6daee6ab217c9ffaac9dbc00.tar.gz
add simple, experimental terminal emulator
-rw-r--r--README.tmt637
-rw-r--r--fbcon.c304
-rw-r--r--meson.build15
-rw-r--r--tmt.c500
-rw-r--r--tmt.h140
5 files changed, 1595 insertions, 1 deletions
diff --git a/README.tmt b/README.tmt
new file mode 100644
index 0000000..f3819df
--- /dev/null
+++ b/README.tmt
@@ -0,0 +1,637 @@
+
+============================================
+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.
+
+.. _`mtm`: https://github.com/deadpixi/mtm
+.. _`libtsm`: https://www.freedesktop.org/wiki/Software/kmscon/libtsm/
+
+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:
+
+.. _`mtm`: https://github.com/deadpixi/mtm
+
+.. code:: c
+
+ #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
+---------------------------
+
+.. code:: c
+
+ /* 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).
+
+.. _`apt`: https://wiki.debian.org/Apt
+
+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.
diff --git a/fbcon.c b/fbcon.c
new file mode 100644
index 0000000..1b08ca8
--- /dev/null
+++ b/fbcon.c
@@ -0,0 +1,304 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <termios.h>
+#include <math.h>
+#include <signal.h>
+#include <inttypes.h>
+#include <ctype.h>
+#include <locale.h>
+#include <wchar.h>
+#include <setjmp.h>
+#include <pty.h>
+
+#include <sys/time.h>
+#include <sys/ioctl.h>
+
+#include <linux/input.h>
+
+#include <cairo.h>
+#include <libudev.h>
+#include <libinput.h>
+#include <xkbcommon/xkbcommon.h>
+
+#include "fbtools.h"
+#include "drmtools.h"
+#include "tmt.h"
+
+static gfxstate *gfx;
+static cairo_surface_t *surface1;
+static cairo_surface_t *surface2;
+static TMT *vt;
+static int dirty;
+static struct udev *udev;
+static struct libinput *kbd;
+
+static struct xkb_context *xkb;
+static struct xkb_keymap *map;
+static struct xkb_state *state;
+static struct xkb_rule_names layout = {
+ .rules = NULL,
+ .model = "pc105",
+ .layout = "us",
+ .variant = NULL,
+ .options = NULL,
+};
+
+/* ---------------------------------------------------------------------- */
+
+static jmp_buf fb_fatal_cleanup;
+
+static void catch_exit_signal(int signal)
+{
+ siglongjmp(fb_fatal_cleanup,signal);
+}
+
+static void exit_signals_init(void)
+{
+ struct sigaction act,old;
+ int termsig;
+
+ memset(&act,0,sizeof(act));
+ act.sa_handler = catch_exit_signal;
+ sigemptyset(&act.sa_mask);
+ sigaction(SIGINT, &act,&old);
+ sigaction(SIGQUIT,&act,&old);
+ sigaction(SIGTERM,&act,&old);
+
+ sigaction(SIGABRT,&act,&old);
+ sigaction(SIGTSTP,&act,&old);
+
+ sigaction(SIGBUS, &act,&old);
+ sigaction(SIGILL, &act,&old);
+ sigaction(SIGSEGV,&act,&old);
+
+ if (0 == (termsig = sigsetjmp(fb_fatal_cleanup,0)))
+ return;
+
+ /* cleanup */
+ gfx->cleanup_display();
+ fprintf(stderr,"Oops: %s\n",strsignal(termsig));
+ exit(42);
+}
+
+static void cleanup_and_exit(int code)
+{
+ gfx->cleanup_display();
+ exit(code);
+}
+
+/* ---------------------------------------------------------------------- */
+
+static int open_restricted(const char *path, int flags, void *user_data)
+{
+ int fd;
+
+ fd = open(path, flags);
+ if (fd < 0) {
+ fprintf(stderr, "open %s: %s\n", path, strerror(errno));
+ return fd;
+ }
+
+ ioctl(fd, EVIOCGRAB, 1);
+ return fd;
+}
+
+static void close_restricted(int fd, void *user_data)
+{
+ ioctl(fd, EVIOCGRAB, 0);
+ close(fd);
+}
+
+static const struct libinput_interface interface = {
+ .open_restricted = open_restricted,
+ .close_restricted = close_restricted,
+};
+
+/* ---------------------------------------------------------------------- */
+
+static void render(void)
+{
+ static bool second;
+ const TMTSCREEN *s = tmt_screen(vt);
+// const TMTPOINT *c = tmt_cursor(vt);
+ cairo_t *context;
+ cairo_font_extents_t extents;
+ int line, col, tx, ty;
+ wchar_t ws[2];
+ char utf8[10];
+
+ if (surface2)
+ second = !second;
+ context = cairo_create(second ? surface2 : surface1);
+
+ cairo_select_font_face(context, "monospace",
+ CAIRO_FONT_SLANT_NORMAL,
+ CAIRO_FONT_WEIGHT_NORMAL);
+ cairo_set_font_size(context, 16);
+ cairo_font_extents(context, &extents);
+ tx = (gfx->hdisplay - (extents.max_x_advance * s->ncol)) / 2;
+ ty = (gfx->vdisplay - (extents.height * s->nline)) / 2;
+
+ cairo_set_source_rgb(context, 0, 0, 0);
+ cairo_paint(context);
+
+ cairo_set_source_rgb(context, 1, 1, 1);
+ for (line = 0; line < s->nline; line++) {
+ for (col = 0; col < s->ncol; col++) {
+ TMTCHAR *c = &s->lines[line]->chars[col];
+ ws[0] = c->c;
+ ws[1] = 0;
+ wcstombs(utf8, ws, sizeof(utf8));
+ cairo_move_to(context,
+ tx + col * extents.max_x_advance,
+ ty + line * extents.height + extents.ascent);
+ cairo_show_text(context, utf8);
+ }
+ }
+
+ cairo_show_page(context);
+ cairo_destroy(context);
+
+ if (gfx->flush_display)
+ gfx->flush_display(second);
+}
+
+void tmt_callback(tmt_msg_t m, TMT *vt, const void *a, void *p)
+{
+ switch (m) {
+ case TMT_MSG_UPDATE:
+ dirty++;
+ break;
+ default:
+ break;
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ pid_t child;
+ int pty, input;
+
+ setlocale(LC_ALL,"");
+
+ /* init drm */
+ gfx = drm_init(NULL, NULL, NULL, true);
+ if (!gfx) {
+ fprintf(stderr, "drm init failed\n");
+ exit(1);
+ }
+ exit_signals_init();
+ signal(SIGTSTP,SIG_IGN);
+
+ /* init cairo */
+ surface1 = cairo_image_surface_create_for_data(gfx->mem,
+ CAIRO_FORMAT_ARGB32,
+ gfx->hdisplay,
+ gfx->vdisplay,
+ gfx->stride);
+ if (gfx->mem2) {
+ surface2 = cairo_image_surface_create_for_data(gfx->mem2,
+ CAIRO_FORMAT_ARGB32,
+ gfx->hdisplay,
+ gfx->vdisplay,
+ gfx->stride);
+ }
+
+ /* init udev + libinput */
+ udev = udev_new();
+ kbd = libinput_udev_create_context(&interface, NULL, udev);
+ libinput_udev_assign_seat(kbd, "seat0");
+ input = libinput_get_fd(kbd);
+
+ /* init udev + xkbcommon */
+ xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+ map = xkb_keymap_new_from_names(xkb, &layout, XKB_KEYMAP_COMPILE_NO_FLAGS);
+ state = xkb_state_new(map);
+
+ vt = tmt_open(25, 80, tmt_callback, NULL, NULL);
+ child = forkpty(&pty, NULL, NULL, NULL);
+ if (0 == child) {
+ /* child */
+ setenv("TERM", "ansi", true);
+ setenv("LINES", "25", true);
+ setenv("COLUMNS", "80", true);
+ execl("/bin/sh", "-sh", NULL);
+ fprintf(stderr, "failed to exec /bin/sh: %s\n", strerror(errno));
+ sleep(3);
+ exit(0);
+ }
+ dirty++;
+
+ for (;;) {
+ fd_set set;
+ int rc, max;
+
+ if (dirty)
+ render();
+
+ max = 0;
+ FD_ZERO(&set);
+ FD_SET(pty, &set);
+ if (max < pty)
+ max = pty;
+ FD_SET(input, &set);
+ if (max < input)
+ max = input;
+
+ rc = select(max+ 1, &set, NULL, NULL, NULL);
+ if (rc < 0)
+ break;
+
+ if (FD_ISSET(pty, &set)) {
+ char buf[1024];
+ rc = read(pty, buf, sizeof(buf));
+ if (rc < 0 && errno != EAGAIN && errno != EINTR)
+ break; /* read error */
+ if (rc == 0)
+ break; /* no data -> EOF */
+ if (rc > 0)
+ tmt_write(vt, buf, rc);
+ }
+
+ if (FD_ISSET(input, &set)) {
+ struct libinput_event *evt;
+ struct libinput_event_keyboard *kevt;
+ xkb_keycode_t key;
+ bool down;
+ char buf[32];
+
+ rc = libinput_dispatch(kbd);
+ if (rc < 0)
+ break;
+ while ((evt = libinput_get_event(kbd)) != NULL) {
+ switch (libinput_event_get_type(evt)) {
+ case LIBINPUT_EVENT_KEYBOARD_KEY:
+ kevt = libinput_event_get_keyboard_event(evt);
+ key = libinput_event_keyboard_get_key(kevt) + 8;
+ down = libinput_event_keyboard_get_key_state(kevt);
+ xkb_state_update_key(state, key, down);
+ if (down) {
+ rc = xkb_state_key_get_utf8(state, key,
+ buf, sizeof(buf));
+ if (rc > 0)
+ write(pty, buf, rc);
+ }
+ break;
+ default:
+ /* ignore event */
+ break;
+ }
+ libinput_event_destroy(evt);
+ }
+ }
+ }
+
+ cleanup_and_exit(0);
+ return 0;
+}
diff --git a/meson.build b/meson.build
index cbed982..8b5728a 100644
--- a/meson.build
+++ b/meson.build
@@ -1,8 +1,10 @@
# -*-python-*-
-project('fbida', 'c')
+project('fbida', 'c', default_options : [ 'c_std=gnu99' ])
# tweak warnings
add_global_arguments('-Wno-pointer-sign', language : 'c')
+add_global_arguments('-D_POSIX_SOURCE=1', language : 'c')
+add_global_arguments('-D_GNU_SOURCE=1', language : 'c')
# init configuration
config = configuration_data()
@@ -26,10 +28,12 @@ tiff_dep = dependency('libtiff-4')
webp_dep = dependency('libwebp', required : false)
udev_dep = dependency('libudev')
input_dep = dependency('libinput')
+xkb_dep = dependency('xkbcommon')
# other library deps
cc = meson.get_compiler('c')
jpeg_dep = cc.find_library('jpeg')
+util_dep = cc.find_library('util')
math_dep = cc.find_library('m', required : false)
pcd_dep = cc.find_library('pcd', required : false)
gif_dep = cc.find_library('gif', required : false)
@@ -125,6 +129,15 @@ executable('fbpdf',
dependencies : fbpdf_deps,
install : true)
+# build fbcon
+fbcon_srcs = [ 'fbcon.c', 'drmtools.c', 'tmt.c' ]
+fbcon_deps = [ drm_dep, cairo_dep, util_dep, udev_dep, input_dep, xkb_dep ]
+
+executable('fbcon',
+ sources : fbcon_srcs,
+ dependencies : fbcon_deps,
+ install : true)
+
# build kbdtest
executable('kbdtest',
sources : [ 'kbdtest.c', 'kbd.c' ],
diff --git a/tmt.c b/tmt.c
new file mode 100644
index 0000000..26c122e
--- /dev/null
+++ b/tmt.c
@@ -0,0 +1,500 @@
+/* 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 HOLDER 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.
+ */
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "tmt.h"
+
+#define BUF_MAX 100
+#define PAR_MAX 8
+#define TAB 8
+#define MAX(x, y) (((size_t)(x) > (size_t)(y)) ? (size_t)(x) : (size_t)(y))
+#define MIN(x, y) (((size_t)(x) < (size_t)(y)) ? (size_t)(x) : (size_t)(y))
+#define CLINE(vt) (vt)->screen.lines[MIN((vt)->curs.r, (vt)->screen.nline - 1)]
+
+#define P0(x) (vt->pars[x])
+#define P1(x) (vt->pars[x]? vt->pars[x] : 1)
+#define CB(vt, m, a) ((vt)->cb? (vt)->cb(m, vt, a, (vt)->p) : (void)0)
+#define INESC ((vt)->state)
+
+#define COMMON_VARS \
+ TMTSCREEN *s = &vt->screen; \
+ TMTPOINT *c = &vt->curs; \
+ TMTLINE *l = CLINE(vt); \
+ TMTCHAR *t = vt->tabs->chars
+
+#define HANDLER(name) static void name (TMT *vt) { COMMON_VARS;
+
+struct TMT{
+ TMTPOINT curs, oldcurs;
+ TMTATTRS attrs, oldattrs;
+
+ bool dirty, acs, ignored;
+ TMTSCREEN screen;
+ TMTLINE *tabs;
+
+ TMTCALLBACK cb;
+ void *p;
+ const wchar_t *acschars;
+
+ mbstate_t ms;
+ size_t nmb;
+ char mb[BUF_MAX + 1];
+
+ size_t pars[PAR_MAX];
+ size_t npar;
+ size_t arg;
+ enum {S_NUL, S_ESC, S_ARG} state;
+};
+
+static TMTATTRS defattrs = {.fg = TMT_COLOR_DEFAULT, .bg = TMT_COLOR_DEFAULT};
+static void writecharatcurs(TMT *vt, wchar_t w);
+
+static wchar_t
+tacs(const TMT *vt, unsigned char c)
+{
+ /* The terminfo alternate character set for ANSI. */
+ static unsigned char map[] = {0020U, 0021U, 0030U, 0031U, 0333U, 0004U,
+ 0261U, 0370U, 0361U, 0260U, 0331U, 0277U,
+ 0332U, 0300U, 0305U, 0176U, 0304U, 0304U,
+ 0304U, 0137U, 0303U, 0264U, 0301U, 0302U,
+ 0263U, 0363U, 0362U, 0343U, 0330U, 0234U,
+ 0376U};
+ for (size_t i = 0; i < sizeof(map); i++) if (map[i] == c)
+ return vt->acschars[i];
+ return (wchar_t)c;
+}
+
+static void
+dirtylines(TMT *vt, size_t s, size_t e)
+{
+ vt->dirty = true;
+ for (size_t i = s; i < e; i++)
+ vt->screen.lines[i]->dirty = true;
+}
+
+static void
+clearline(TMT *vt, TMTLINE *l, size_t s, size_t e)
+{
+ vt->dirty = l->dirty = true;
+ for (size_t i = s; i < e && i < vt->screen.ncol; i++){
+ l->chars[i].a = defattrs;
+ l->chars[i].c = L' ';
+ }
+}
+
+static void
+clearlines(TMT *vt, size_t r, size_t n)
+{
+ for (size_t i = r; i < r + n && i < vt->screen.nline; i++)
+ clearline(vt, vt->screen.lines[i], 0, vt->screen.ncol);
+}
+
+static void
+scrup(TMT *vt, size_t r, size_t n)
+{
+ n = MIN(n, vt->screen.nline - 1 - r);
+
+ if (n){
+ TMTLINE *buf[n];
+
+ memcpy(buf, vt->screen.lines + r, n * sizeof(TMTLINE *));
+ memmove(vt->screen.lines + r, vt->screen.lines + r + n,
+ (vt->screen.nline - n - r) * sizeof(TMTLINE *));
+ memcpy(vt->screen.lines + (vt->screen.nline - n),
+ buf, n * sizeof(TMTLINE *));
+
+ clearlines(vt, vt->screen.nline - n, n);
+ dirtylines(vt, r, vt->screen.nline);
+ }
+}
+
+static void
+scrdn(TMT *vt, size_t r, size_t n)
+{
+ n = MIN(n, vt->screen.nline - 1 - r);
+
+ if (n){
+ TMTLINE *buf[n];
+
+ memcpy(buf, vt->screen.lines + (vt->screen.nline - n),
+ n * sizeof(TMTLINE *));
+ memmove(vt->screen.lines + r + n, vt->screen.lines + r,
+ (vt->screen.nline - n - r) * sizeof(TMTLINE *));
+ memcpy(vt->screen.lines + r, buf, n * sizeof(TMTLINE *));
+
+ clearlines(vt, r, n);
+ dirtylines(vt, r, vt->screen.nline);
+ }
+}
+
+HANDLER(ed)
+ size_t b = 0;
+ size_t e = s->nline;
+
+ switch (P0(0)){
+ case 0: b = c->r + 1; clearline(vt, l, c->c, vt->screen.ncol); break;
+ case 1: e = c->r - 1; clearline(vt, l, 0, c->c); break;
+ case 2: /* use defaults */ break;
+ default: /* do nothing */ return;
+ }
+
+ clearlines(vt, b, e - b);
+}
+
+HANDLER(ich)
+ size_t n = P1(0); /* XXX use MAX */
+ if (n > s->ncol - c->c - 1) n = s->ncol - c->c - 1;
+
+ memmove(l->chars + c->c + n, l->chars + c->c,
+ MIN(s->ncol - 1 - c->c,
+ (s->ncol - c->c - n - 1)) * sizeof(TMTCHAR));
+ clearline(vt, l, c->c, n);
+}
+
+HANDLER(dch)
+ size_t n = P1(0); /* XXX use MAX */
+ if (n > s->ncol - c->c) n = s->ncol - c->c;
+
+ memmove(l->chars + c->c, l->chars + c->c + n,
+ (s->ncol - c->c - n) * sizeof(TMTCHAR));
+
+ clearline(vt, l, s->ncol - c->c - n, s->ncol);
+}
+
+HANDLER(el)
+ switch (P0(0)){
+ case 0: clearline(vt, l, c->c, vt->screen.ncol); break;
+ case 1: clearline(vt, l, 0, MIN(c->c + 1, s->ncol - 1)); break;
+ case 2: clearline(vt, l, 0, vt->screen.ncol); break;
+ }
+}
+
+HANDLER(sgr)
+ #define FGBG(c) *(P0(i) < 40? &vt->attrs.fg : &vt->attrs.bg) = c
+ for (size_t i = 0; i < vt->npar; i++) switch (P0(i)){
+ case 0: vt->attrs = defattrs; break;
+ case 1: case 22: vt->attrs.bold = P0(0) < 20; break;
+ case 2: case 23: vt->attrs.dim = P0(0) < 20; break;
+ case 4: case 24: vt->attrs.underline = P0(0) < 20; break;
+ case 5: case 25: vt->attrs.blink = P0(0) < 20; break;
+ case 7: case 27: vt->attrs.reverse = P0(0) < 20; break;
+ case 8: case 28: vt->attrs.invisible = P0(0) < 20; break;
+ case 10: case 11: vt->acs = P0(0) > 10; break;
+ case 30: case 40: FGBG(TMT_COLOR_BLACK); break;
+ case 31: case 41: FGBG(TMT_COLOR_RED); break;
+ case 32: case 42: FGBG(TMT_COLOR_GREEN); break;
+ case 33: case 43: FGBG(TMT_COLOR_YELLOW); break;
+ case 34: case 44: FGBG(TMT_COLOR_BLUE); break;
+ case 35: case 45: FGBG(TMT_COLOR_MAGENTA); break;
+ case 36: case 46: FGBG(TMT_COLOR_CYAN); break;
+ case 37: case 47: FGBG(TMT_COLOR_WHITE); break;
+ case 39: case 49: FGBG(TMT_COLOR_DEFAULT); break;
+ }
+}
+
+HANDLER(rep)
+ if (!c->c) return;
+ wchar_t r = l->chars[c->c - 1].c;
+ for (size_t i = 0; i < P1(0); i++)
+ writecharatcurs(vt, r);
+}
+
+HANDLER(dsr)
+ char r[BUF_MAX + 1] = {0};
+ snprintf(r, BUF_MAX, "\033[%zd;%zdR", c->r, c->c);
+ CB(vt, TMT_MSG_ANSWER, (const char *)r);
+}
+
+HANDLER(resetparser)
+ memset(vt->pars, 0, sizeof(vt->pars));
+ vt->state = vt->npar = vt->arg = vt->ignored = (bool)0;
+}
+
+HANDLER(consumearg)
+ if (vt->npar < PAR_MAX)
+ vt->pars[vt->npar++] = vt->arg;
+ vt->arg = 0;
+}
+
+HANDLER(fixcursor)
+ c->r = MIN(c->r, s->nline - 1);
+ c->c = MIN(c->c, s->ncol - 1);
+}
+
+static bool
+handlechar(TMT *vt, char i)
+{
+ COMMON_VARS;
+
+ char cs[] = {i, 0};
+ #define ON(S, C, A) if (vt->state == (S) && strchr(C, i)){ A; return true;}
+ #define DO(S, C, A) ON(S, C, consumearg(vt); if (!vt->ignored) {A;} \
+ fixcursor(vt); resetparser(vt););
+
+ DO(S_NUL, "\x07", CB(vt, TMT_MSG_BELL, NULL))
+ DO(S_NUL, "\x08", if (c->c) c->c--)
+ DO(S_NUL, "\x09", while (++c->c < s->ncol - 1 && t[c->c].c != L'*'))
+ DO(S_NUL, "\x0a", c->r < s->nline - 1? (void)c->r++ : scrup(vt, 0, 1))
+ DO(S_NUL, "\x0d", c->c = 0)
+ ON(S_NUL, "\x1b", vt->state = S_ESC)
+ ON(S_ESC, "\x1b", vt->state = S_ESC)
+ DO(S_ESC, "H", t[c->c].c = L'*')
+ DO(S_ESC, "7", vt->oldcurs = vt->curs; vt->oldattrs = vt->attrs)
+ DO(S_ESC, "8", vt->curs = vt->oldcurs; vt->attrs = vt->oldattrs)
+ ON(S_ESC, "+*()", vt->ignored = true; vt->state = S_ARG)
+ DO(S_ESC, "c", tmt_reset(vt))
+ ON(S_ESC, "[", vt->state = S_ARG)
+ ON(S_ARG, "\x1b", vt->state = S_ESC)
+ ON(S_ARG, ";", consumearg(vt))
+ ON(S_ARG, "?", (void)0)
+ ON(S_ARG, "0123456789", vt->arg = vt->arg * 10 + atoi(cs))
+ DO(S_ARG, "A", c->r = MAX(c->r - P1(0), 0))
+ DO(S_ARG, "B", c->r = MIN(c->r + P1(0), s->nline - 1))
+ DO(S_ARG, "C", c->c = MIN(c->c + P1(0), s->ncol - 1))
+ DO(S_ARG, "D", c->c = MIN(c->c - P1(0), c->c))
+ DO(S_ARG, "E", c->c = 0; c->r = MIN(c->r + P1(0), s->nline - 1))
+ DO(S_ARG, "F", c->c = 0; c->r = MAX(c->r - P1(0), 0))
+ DO(S_ARG, "G", c->c = MIN(P1(0) - 1, s->ncol - 1))
+ DO(S_ARG, "d", c->r = MIN(P1(0) - 1, s->nline - 1))
+ DO(S_ARG, "Hf", c->r = P1(0) - 1; c->c = P1(1) - 1)
+ DO(S_ARG, "I", while (++c->c < s->ncol - 1 && t[c->c].c != L'*'))
+ DO(S_ARG, "J", ed(vt))
+ DO(S_ARG, "K", el(vt))
+ DO(S_ARG, "L", scrdn(vt, c->r, P1(0)))
+ DO(S_ARG, "M", scrup(vt, c->r, P1(0)))
+ DO(S_ARG, "P", dch(vt))
+ DO(S_ARG, "S", scrup(vt, 0, P1(0)))
+ DO(S_ARG, "T", scrdn(vt, 0, P1(0)))
+ DO(S_ARG, "X", clearline(vt, l, c->c, P1(0)))
+ DO(S_ARG, "Z", while (c->c && t[--c->c].c != L'*'))
+ DO(S_ARG, "b", rep(vt));
+ DO(S_ARG, "c", CB(vt, TMT_MSG_ANSWER, "\033[?6c"))
+ DO(S_ARG, "g", if (P0(0) == 3) clearline(vt, vt->tabs, 0, s->ncol))
+ DO(S_ARG, "m", sgr(vt))
+ DO(S_ARG, "n", if (P0(0) == 6) dsr(vt))
+ DO(S_ARG, "h", if (P0(0) == 25) CB(vt, TMT_MSG_CURSOR, "t"))
+ DO(S_ARG, "i", (void)0)
+ DO(S_ARG, "l", if (P0(0) == 25) CB(vt, TMT_MSG_CURSOR, "f"))
+ DO(S_ARG, "s", vt->oldcurs = vt->curs; vt->oldattrs = vt->attrs)
+ DO(S_ARG, "u", vt->curs = vt->oldcurs; vt->attrs = vt->oldattrs)
+ DO(S_ARG, "@", ich(vt))
+
+ return resetparser(vt), false;
+}
+
+static void
+notify(TMT *vt, bool update, bool moved)
+{
+ if (update) CB(vt, TMT_MSG_UPDATE, &vt->screen);
+ if (moved) CB(vt, TMT_MSG_MOVED, &vt->curs);
+}
+
+static TMTLINE *
+allocline(TMT *vt, TMTLINE *o, size_t n, size_t pc)
+{
+ TMTLINE *l = realloc(o, sizeof(TMTLINE) + n * sizeof(TMTCHAR));
+ if (!l) return NULL;
+
+ clearline(vt, l, pc, n);
+ return l;
+}
+
+static void
+freelines(TMT *vt, size_t s, size_t n, bool screen)
+{
+ for (size_t i = s; vt->screen.lines && i < s + n; i++){
+ free(vt->screen.lines[i]);
+ vt->screen.lines[i] = NULL;
+ }
+ if (screen) free(vt->screen.lines);
+}
+
+TMT *
+tmt_open(size_t nline, size_t ncol, TMTCALLBACK cb, void *p,
+ const wchar_t *acs)
+{
+ TMT *vt = calloc(1, sizeof(TMT));
+ if (!nline || !ncol || !vt) return free(vt), NULL;
+
+ /* ASCII-safe defaults for box-drawing characters. */
+ vt->acschars = acs? acs : L"><^v#+:o##+++++~---_++++|<>*!fo";
+ vt->cb = cb;
+ vt->p = p;
+
+ if (!tmt_resize(vt, nline, ncol)) return tmt_close(vt), NULL;
+ return vt;
+}
+
+void
+tmt_close(TMT *vt)
+{
+ free(vt->tabs);
+ freelines(vt, 0, vt->screen.nline, true);
+ free(vt);
+}
+
+bool
+tmt_resize(TMT *vt, size_t nline, size_t ncol)
+{
+ if (nline < 2 || ncol < 2) return false;
+ if (nline < vt->screen.nline)
+ freelines(vt, nline, vt->screen.nline - nline, false);
+
+ TMTLINE **l = realloc(vt->screen.lines, nline * sizeof(TMTLINE *));
+ if (!l) return false;
+
+ size_t pc = vt->screen.ncol;
+ vt->screen.lines = l;
+ vt->screen.ncol = ncol;
+ for (size_t i = 0; i < nline; i++){
+ TMTLINE *nl = NULL;
+ if (i >= vt->screen.nline)
+ nl = vt->screen.lines[i] = allocline(vt, NULL, ncol, 0);
+ else
+ nl = allocline(vt, vt->screen.lines[i], ncol, pc);
+
+ if (!nl) return false;
+ vt->screen.lines[i] = nl;
+ }
+ vt->screen.nline = nline;
+
+ vt->tabs = allocline(vt, vt->tabs, ncol, 0);
+ if (!vt->tabs) return free(l), false;
+ vt->tabs->chars[0].c = vt->tabs->chars[ncol - 1].c = L'*';
+ for (size_t i = 0; i < ncol; i++) if (i % TAB == 0)
+ vt->tabs->chars[i].c = L'*';
+
+ fixcursor(vt);
+ dirtylines(vt, 0, nline);
+ notify(vt, true, true);
+ return true;
+}
+
+static void
+writecharatcurs(TMT *vt, wchar_t w)
+{
+ COMMON_VARS;
+
+ #ifdef TMT_HAS_WCWIDTH
+ extern int wcwidth(wchar_t c);
+ if (wcwidth(w) > 1) w = TMT_INVALID_CHAR;
+ if (wcwidth(w) < 0) return;
+ #endif
+
+ CLINE(vt)->chars[vt->curs.c].c = w;
+ CLINE(vt)->chars[vt->curs.c].a = vt->attrs;
+ CLINE(vt)->dirty = vt->dirty = true;
+
+ if (c->c < s->ncol - 1)
+ c->c++;
+ else{
+ c->c = 0;
+ c->r++;
+ }
+
+ if (c->r >= s->nline){
+ c->r = s->nline - 1;
+ scrup(vt, 0, 1);
+ }
+}
+
+static inline size_t
+testmbchar(TMT *vt)
+{
+ mbstate_t ts = vt->ms;
+ return vt->nmb? mbrtowc(NULL, vt->mb, vt->nmb, &ts) : (size_t)-2;
+}
+
+static inline wchar_t
+getmbchar(TMT *vt)
+{
+ wchar_t c = 0;
+ size_t n = mbrtowc(&c, vt->mb, vt->nmb, &vt->ms);
+ vt->nmb = 0;
+ return (n == (size_t)-1 || n == (size_t)-2)? TMT_INVALID_CHAR : c;
+}
+
+void
+tmt_write(TMT *vt, const char *s, size_t n)
+{
+ TMTPOINT oc = vt->curs;
+ n = n? n : strlen(s);
+
+ for (size_t p = 0; p < n; p++){
+ if (handlechar(vt, s[p]))
+ continue;
+ else if (vt->acs)
+ writecharatcurs(vt, tacs(vt, (unsigned char)s[p]));
+ else if (vt->nmb >= BUF_MAX)
+ writecharatcurs(vt, getmbchar(vt));
+ else{
+ switch (testmbchar(vt)){
+ case (size_t)-1: writecharatcurs(vt, getmbchar(vt)); break;
+ case (size_t)-2: vt->mb[vt->nmb++] = s[p]; break;
+ }
+
+ if (testmbchar(vt) <= MB_LEN_MAX)
+ writecharatcurs(vt, getmbchar(vt));
+ }
+ }
+
+ notify(vt, vt->dirty, memcmp(&oc, &vt->curs, sizeof(oc)) != 0);
+}
+
+const TMTSCREEN *
+tmt_screen(const TMT *vt)
+{
+ return &vt->screen;
+}
+
+const TMTPOINT *
+tmt_cursor(const TMT *vt)
+{
+ return &vt->curs;
+}
+
+void
+tmt_clean(TMT *vt)
+{
+ for (size_t i = 0; i < vt->screen.nline; i++)
+ vt->dirty = vt->screen.lines[i]->dirty = false;
+}
+
+void
+tmt_reset(TMT *vt)
+{
+ vt->curs.r = vt->curs.c = vt->oldcurs.r = vt->oldcurs.c = vt->acs = (bool)0;
+ resetparser(vt);
+ vt->attrs = vt->oldattrs = defattrs;
+ memset(&vt->ms, 0, sizeof(vt->ms));
+ clearlines(vt, 0, vt->screen.nline);
+ CB(vt, TMT_MSG_CURSOR, "t");
+ notify(vt, true, true);
+}
diff --git a/tmt.h b/tmt.h
new file mode 100644
index 0000000..ae0ddbb
--- /dev/null
+++ b/tmt.h
@@ -0,0 +1,140 @@
+/* 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 HOLDER 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.
+ */
+
+#ifndef TMT_H
+#define TMT_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <wchar.h>
+
+/**** INVALID WIDE CHARACTER */
+#ifndef TMT_INVALID_CHAR
+#define TMT_INVALID_CHAR ((wchar_t)0xfffd)
+#endif
+
+/**** INPUT SEQUENCES */
+#define TMT_KEY_UP "\033[A"
+#define TMT_KEY_DOWN "\033[B"
+#define TMT_KEY_RIGHT "\033[C"
+#define TMT_KEY_LEFT "\033[D"
+#define TMT_KEY_HOME "\033[H"
+#define TMT_KEY_END "\033[Y"
+#define TMT_KEY_INSERT "\033[L"
+#define TMT_KEY_BACKSPACE "\x08"
+#define TMT_KEY_ESCAPE "\x1b"
+#define TMT_KEY_BACK_TAB "\033[Z"
+#define TMT_KEY_PAGE_UP "\033[V"
+#define TMT_KEY_PAGE_DOWN "\033[U"
+#define TMT_KEY_F1 "\033OP"
+#define TMT_KEY_F2 "\033OQ"
+#define TMT_KEY_F3 "\033OR"
+#define TMT_KEY_F4 "\033OS"
+#define TMT_KEY_F5 "\033OT"
+#define TMT_KEY_F6 "\033OU"
+#define TMT_KEY_F7 "\033OV"
+#define TMT_KEY_F8 "\033OW"
+#define TMT_KEY_F9 "\033OX"
+#define TMT_KEY_F10 "\033OY"
+
+/**** BASIC DATA STRUCTURES */
+typedef struct TMT TMT;
+
+typedef enum{
+ TMT_COLOR_DEFAULT = -1,
+ TMT_COLOR_BLACK = 1,
+ TMT_COLOR_RED,
+ TMT_COLOR_GREEN,
+ TMT_COLOR_YELLOW,
+ TMT_COLOR_BLUE,
+ TMT_COLOR_MAGENTA,
+ TMT_COLOR_CYAN,
+ TMT_COLOR_WHITE,
+ TMT_COLOR_MAX
+} tmt_color_t;
+
+typedef struct TMTATTRS TMTATTRS;
+struct TMTATTRS{
+ bool bold;
+ bool dim;
+ bool underline;
+ bool blink;
+ bool reverse;
+ bool invisible;
+ tmt_color_t fg;
+ tmt_color_t bg;
+};
+
+typedef struct TMTCHAR TMTCHAR;
+struct TMTCHAR{
+ wchar_t c;
+ TMTATTRS a;
+};
+
+typedef struct TMTPOINT TMTPOINT;
+struct TMTPOINT{
+ size_t r;
+ size_t c;
+};
+
+typedef struct TMTLINE TMTLINE;
+struct TMTLINE{
+ bool dirty;
+ TMTCHAR chars[];
+};
+
+typedef struct TMTSCREEN TMTSCREEN;
+struct TMTSCREEN{
+ size_t nline;
+ size_t ncol;
+
+ TMTLINE **lines;
+};
+
+/**** CALLBACK SUPPORT */
+typedef enum{
+ TMT_MSG_MOVED,
+ TMT_MSG_UPDATE,
+ TMT_MSG_ANSWER,
+ TMT_MSG_BELL,
+ TMT_MSG_CURSOR
+} tmt_msg_t;
+
+typedef void (*TMTCALLBACK)(tmt_msg_t m, struct TMT *v, const void *r, void *p);
+
+/**** PUBLIC FUNCTIONS */
+TMT *tmt_open(size_t nline, size_t ncol, TMTCALLBACK cb, void *p,
+ const wchar_t *acs);
+void tmt_close(TMT *vt);
+bool tmt_resize(TMT *vt, size_t nline, size_t ncol);
+void tmt_write(TMT *vt, const char *s, size_t n);
+const TMTSCREEN *tmt_screen(const TMT *vt);
+const TMTPOINT *tmt_cursor(const TMT *vt);
+void tmt_clean(TMT *vt);
+void tmt_reset(TMT *vt);
+
+#endif