From 42b780fd3aeb8ad6541de05eacb4e7202ccfca5e Mon Sep 17 00:00:00 2001 From: Zach Potter Date: Sun, 20 Aug 2023 23:26:55 -0700 Subject: Rework keyboard and implement global terminal --- .gitignore | 1 + pi/KeyCodes.tcl | 266 +++++++++-------- pi/Keyboard.tcl | 33 +-- pi/pi.tcl | 34 ++- vendor/libtmt/README.rst | 637 +++++++++++++++++++++++++++++++++++++++++ vendor/libtmt/tmt.c | 506 ++++++++++++++++++++++++++++++++ vendor/libtmt/tmt.h | 140 +++++++++ virtual-programs/terminal.folk | 170 +++++++++++ 8 files changed, 1641 insertions(+), 146 deletions(-) create mode 100644 vendor/libtmt/README.rst create mode 100644 vendor/libtmt/tmt.c create mode 100644 vendor/libtmt/tmt.h create mode 100644 virtual-programs/terminal.folk diff --git a/.gitignore b/.gitignore index 281d7b1e..36c1aab4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ statements.dot trie.pdf trie.dot virtual-programs/program-positions.tcl +capture*.jpg diff --git a/pi/KeyCodes.tcl b/pi/KeyCodes.tcl index 4ca8d4ad..880a2e65 100644 --- a/pi/KeyCodes.tcl +++ b/pi/KeyCodes.tcl @@ -1,129 +1,147 @@ set KeyCodes [dict create] -proc define {name code} { - upvar KeyCodes KeyCodes - dict set KeyCodes $code $name + +proc keydef {code val {shiftVal ""}} { + upvar KeyCodes KeyCodes + if {$shiftVal == ""} { + set shiftVal $val + } + dict set KeyCodes $code [list $val $shiftVal] +} + +proc keyFromCode {code {shift false}} { + upvar KeyCodes KeyCodes + if {[dict exists $KeyCodes $code]} { + set vals [dict get $KeyCodes $code] + return [lindex $vals [expr {$shift ? 1 : 0}]] + } + puts "WARNING: unknown key code \"$code\"" + return "?" } -# from https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h -define KEY_RESERVED 0 -define KEY_ESC 1 -define KEY_1 2 -define KEY_2 3 -define KEY_3 4 -define KEY_4 5 -define KEY_5 6 -define KEY_6 7 -define KEY_7 8 -define KEY_8 9 -define KEY_9 10 -define KEY_0 11 -define KEY_MINUS 12 -define KEY_EQUAL 13 -define KEY_BACKSPACE 14 -define KEY_TAB 15 -define KEY_Q 16 -define KEY_W 17 -define KEY_E 18 -define KEY_R 19 -define KEY_T 20 -define KEY_Y 21 -define KEY_U 22 -define KEY_I 23 -define KEY_O 24 -define KEY_P 25 -define KEY_LEFTBRACE 26 -define KEY_RIGHTBRACE 27 -define KEY_ENTER 28 -define KEY_LEFTCTRL 29 -define KEY_A 30 -define KEY_S 31 -define KEY_D 32 -define KEY_F 33 -define KEY_G 34 -define KEY_H 35 -define KEY_J 36 -define KEY_K 37 -define KEY_L 38 -define KEY_SEMICOLON 39 -define KEY_APOSTROPHE 40 -define KEY_GRAVE 41 -define KEY_LEFTSHIFT 42 -define KEY_BACKSLASH 43 -define KEY_Z 44 -define KEY_X 45 -define KEY_C 46 -define KEY_V 47 -define KEY_B 48 -define KEY_N 49 -define KEY_M 50 -define KEY_COMMA 51 -define KEY_DOT 52 -define KEY_SLASH 53 -define KEY_RIGHTSHIFT 54 -define KEY_KPASTERISK 55 -define KEY_LEFTALT 56 -define KEY_SPACE 57 -define KEY_CAPSLOCK 58 -define KEY_F1 59 -define KEY_F2 60 -define KEY_F3 61 -define KEY_F4 62 -define KEY_F5 63 -define KEY_F6 64 -define KEY_F7 65 -define KEY_F8 66 -define KEY_F9 67 -define KEY_F10 68 -define KEY_NUMLOCK 69 -define KEY_SCROLLLOCK 70 -define KEY_KP7 71 -define KEY_KP8 72 -define KEY_KP9 73 -define KEY_KPMINUS 74 -define KEY_KP4 75 -define KEY_KP5 76 -define KEY_KP6 77 -define KEY_KPPLUS 78 -define KEY_KP1 79 -define KEY_KP2 80 -define KEY_KP3 81 -define KEY_KP0 82 -define KEY_KPDOT 83 +# Keycodes from https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h + +# In the future, probably could make use of KDB's libkeymap to support more than basic ANSI +# See: http://kbd-project.org/ + +keydef 0 {RESERVED} +keydef 1 {ESC} +keydef 2 {1} {!} +keydef 3 {2} {@} +keydef 4 {3} {#} +keydef 5 {4} {$} +keydef 6 {5} {%} +keydef 7 {6} {^} +keydef 8 {7} {&} +keydef 9 {8} {*} +keydef 10 {9} {(} +keydef 11 {0} {)} +keydef 12 {-} {_} +keydef 13 {=} {+} +keydef 14 {BACKSPACE} +keydef 15 {TAB} +keydef 16 {q} {Q} +keydef 17 {w} {W} +keydef 18 {e} {E} +keydef 19 {r} {R} +keydef 20 {t} {T} +keydef 21 {y} {Y} +keydef 22 {u} {U} +keydef 23 {i} {I} +keydef 24 {o} {O} +keydef 25 {p} {P} +keydef 26 {[} "\{" +keydef 27 {]} "\}" +keydef 28 {ENTER} +keydef 29 {LEFTCTRL} +keydef 30 {a} {A} +keydef 31 {s} {S} +keydef 32 {d} {D} +keydef 33 {f} {F} +keydef 34 {g} {G} +keydef 35 {h} {H} +keydef 36 {j} {J} +keydef 37 {k} {K} +keydef 38 {l} {L} +keydef 39 {;} {:} +keydef 40 {'} "\"" +keydef 41 {`} {~} +keydef 42 {LEFTSHIFT} +keydef 43 "\\" {|} +keydef 44 {z} {Z} +keydef 45 {x} {X} +keydef 46 {c} {C} +keydef 47 {v} {V} +keydef 48 {b} {B} +keydef 49 {n} {N} +keydef 50 {m} {M} +keydef 51 {,} {<} +keydef 52 {.} {>} +keydef 53 {/} {?} +keydef 54 {RIGHTSHIFT} +keydef 55 {KPASTERISK} +keydef 56 {LEFTALT} +keydef 57 { } ;# SPACE +keydef 58 {CAPSLOCK} +keydef 59 {F1} +keydef 60 {F2} +keydef 61 {F3} +keydef 62 {F4} +keydef 63 {F5} +keydef 64 {F6} +keydef 65 {F7} +keydef 66 {F8} +keydef 67 {F9} +keydef 68 {F10} +keydef 69 {NUMLOCK} +keydef 70 {SCROLLLOCK} +keydef 71 {KP7} +keydef 72 {KP8} +keydef 73 {KP9} +keydef 74 {KPMINUS} +keydef 75 {KP4} +keydef 76 {KP5} +keydef 77 {KP6} +keydef 78 {KPPLUS} +keydef 79 {KP1} +keydef 80 {KP2} +keydef 81 {KP3} +keydef 82 {KP0} +keydef 83 {KPDOT} -define KEY_ZENKAKUHANKAKU 85 -define KEY_102ND 86 -define KEY_F11 87 -define KEY_F12 88 -define KEY_RO 89 -define KEY_KATAKANA 90 -define KEY_HIRAGANA 91 -define KEY_HENKAN 92 -define KEY_KATAKANAHIRAGANA 93 -define KEY_MUHENKAN 94 -define KEY_KPJPCOMMA 95 -define KEY_KPENTER 96 -define KEY_RIGHTCTRL 97 -define KEY_KPSLASH 98 -define KEY_SYSRQ 99 -define KEY_RIGHTALT 100 -define KEY_LINEFEED 101 -define KEY_HOME 102 -define KEY_UP 103 -define KEY_PAGEUP 104 -define KEY_LEFT 105 -define KEY_RIGHT 106 -define KEY_END 107 -define KEY_DOWN 108 -define KEY_PAGEDOWN 109 -define KEY_INSERT 110 -define KEY_DELETE 111 -define KEY_MACRO 112 -define KEY_MUTE 113 -define KEY_VOLUMEDOWN 114 -define KEY_VOLUMEUP 115 -define KEY_POWER 116 -define KEY_KPEQUAL 117 -define KEY_KPPLUSMINUS 118 -define KEY_PAUSE 119 -define KEY_SCALE 120 +keydef 85 {ZENKAKUHANKAKU} +keydef 86 {102ND} +keydef 87 {F11} +keydef 88 {F12} +keydef 89 {RO} +keydef 90 {KATAKANA} +keydef 91 {HIRAGANA} +keydef 92 {HENKAN} +keydef 93 {KATAKANAHIRAGANA} +keydef 94 {MUHENKAN} +keydef 95 {KPJPCOMMA} +keydef 96 {KPENTER} +keydef 97 {RIGHTCTRL} +keydef 98 {KPSLASH} +keydef 99 {SYSRQ} +keydef 100 {RIGHTALT} +keydef 101 {LINEFEED} +keydef 102 {HOME} +keydef 103 {UP} +keydef 104 {PAGEUP} +keydef 105 {LEFT} +keydef 106 {RIGHT} +keydef 107 {END} +keydef 108 {DOWN} +keydef 109 {PAGEDOWN} +keydef 110 {INSERT} +keydef 111 {DELETE} +keydef 112 {MACRO} +keydef 113 {MUTE} +keydef 114 {VOLUMEDOWN} +keydef 115 {VOLUMEUP} +keydef 116 {POWER} +keydef 117 {KPEQUAL} +keydef 118 {KPPLUSMINUS} +keydef 119 {PAUSE} +keydef 120 {SCALE} diff --git a/pi/Keyboard.tcl b/pi/Keyboard.tcl index d578e0b5..3ac0d803 100644 --- a/pi/Keyboard.tcl +++ b/pi/Keyboard.tcl @@ -33,7 +33,8 @@ namespace eval Keyboard { fconfigure $kb -translation binary } - proc getChar {} { + # Returns a tuple of [keycode up/down/repeated] + proc getKeyEvent {} { # See https://www.kernel.org/doc/Documentation/input/input.txt # https://www.kernel.org/doc/Documentation/input/event-codes.txt # https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h @@ -42,27 +43,23 @@ namespace eval Keyboard { # struct timeval time; # unsigned short type; (should be EV_KEY = 0x01) # unsigned short code; (scancode; for example, 16 = q) - # unsigned int value; (should be 1 for keypress) + # unsigned int value; (0 for key release, 1 for press, 2 for repeat) # }; # while 1 { - binary scan [read $Keyboard::kb 16] nntutunu tvSec tvUsec type code value - if {$type == 0x01 && $value == 1} break + # TODO: is this CPU dependant? Originally was 16 bytes with 32-bit longs + # Could check [getconf LONG_BIT] for 32 or 64... + binary scan [read $Keyboard::kb 24] wwsss tvSec tvUsec type code value + if {$type == 0x01} { + return [list $code $value] + } } - # TODO: Should properly catch this error - # e.g. Jan 28 04:11:28 folk0 make[1991]: Thread error: tid0x7f45fd2b0640 can't\ read\ \"name\":\ no\ such\ variable ... - - # scancode name, like KEY_A - catch { set name [dict get $Keyboard::KeyCodes $code] } err - set ch [string tolower [string range $name 4 end]] - # puts "type $type code $code value $value ($ch)" - return $ch } } -if {[info exists ::argv0] && $::argv0 eq [info script]} { - Keyboard::init - puts [Keyboard::getChar] - puts [Keyboard::getChar] - puts [Keyboard::getChar] -} +# if {[info exists ::argv0] && $::argv0 eq [info script]} { +# Keyboard::init +# puts [Keyboard::getChar] +# puts [Keyboard::getChar] +# puts [Keyboard::getChar] +# } diff --git a/pi/pi.tcl b/pi/pi.tcl index a3b73281..80d6a5b1 100644 --- a/pi/pi.tcl +++ b/pi/pi.tcl @@ -5,18 +5,44 @@ thread::errorproc errorproc try { set keyboardThread [thread::create [format { source "pi/Keyboard.tcl" + source "pi/KeyCodes.tcl" source "lib/c.tcl" source "pi/cUtils.tcl" Keyboard::init puts "Keyboard tid: [getTid]" - set chs [list] + set keyStates [list up down repeat] + set modifiers [dict create \ + shift 0 \ + ctrl 0 \ + alt 0 \ + ] + while true { - lappend chs [Keyboard::getChar] + lassign [Keyboard::getKeyEvent] keyCode eventType + + set shift [dict get $modifiers shift] + set key [keyFromCode $keyCode $shift] + set keyState [lindex $keyStates $eventType] + + set isDown [expr {$keyState != "up"}] + if {[string match *SHIFT $key]} { + dict set modifiers shift $isDown + } + if {[string match *CTRL $key]} { + dict set modifiers ctrl $isDown + } + if {[string match *ALT $key]} { + dict set modifiers alt $isDown + } + + set heldModifiers [dict keys [dict filter $modifiers value 1]] + if {[llength $heldModifiers] == 0} {set heldModifiers " "} thread::send -async "%s" [subst { - Retract keyboard claims the keyboard character log is /something/ - Assert keyboard claims the keyboard character log is "$chs" + # TODO: can't use open-square-bracket or backslash keys + Retract keyboard claims key /k/ is /t/ with modifiers /m/ + Assert keyboard claims key "$key" is $keyState with modifiers "$heldModifiers" }] } } [thread::id]]] diff --git a/vendor/libtmt/README.rst b/vendor/libtmt/README.rst new file mode 100644 index 00000000..f3819df9 --- /dev/null +++ b/vendor/libtmt/README.rst @@ -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 + #include + #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/vendor/libtmt/tmt.c b/vendor/libtmt/tmt.c new file mode 100644 index 00000000..73be670d --- /dev/null +++ b/vendor/libtmt/tmt.c @@ -0,0 +1,506 @@ +/* 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 +#include +#include +#include +#include +#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; \ + /* ignore -Wunused-variable */ (void)t;(void)l;(void)c;(void)s + +#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; + else if (n == 0) return; + + memmove(l->chars + c->c, l->chars + c->c + n, + (s->ncol - c->c - n) * sizeof(TMTCHAR)); + + clearline(vt, l, s->ncol - n, s->ncol); + /* VT102 manual says the attribute for the newly empty characters + * should be the same as the last character moved left, which isn't + * what clearline() currently does. + */ +} + +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 + 1, c->c + 1); + 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/vendor/libtmt/tmt.h b/vendor/libtmt/tmt.h new file mode 100644 index 00000000..ae0ddbb9 --- /dev/null +++ b/vendor/libtmt/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 +#include +#include + +/**** 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 diff --git a/virtual-programs/terminal.folk b/virtual-programs/terminal.folk new file mode 100644 index 00000000..df333d28 --- /dev/null +++ b/virtual-programs/terminal.folk @@ -0,0 +1,170 @@ +# Terminal +# +# Claim $this is a terminal +# + +set cc [c create] +$cc cflags -I./vendor/libtmt ./vendor/libtmt/tmt.c + +# TODO: find the right libutil.so for the system +c loadlib /lib/aarch64-linux-gnu/libutil.so +$cc cflags -lutil + +$cc include +$cc include +$cc include +$cc include +$cc include +$cc include +$cc include +$cc include ;# For gettimeofday() + +$cc code { + #include "tmt.h" + + #define SHELL "/bin/bash" + + #define ROWS 12 + #define COLS 43 + char screen[ROWS][COLS + 1]; + int curs_r = 0; + int curs_c = 0; + + + #define PTYBUF 4096 + char iobuf[PTYBUF]; + + TMT *vt; + int pty_fd; + + void callback(tmt_msg_t m, TMT *vt, const void *a, void *p) { + const TMTSCREEN *s = tmt_screen(vt); + + if (m == TMT_MSG_UPDATE) { + for (size_t r = 0; r < s->nline; r++){ + if (s->lines[r]->dirty){ + for (size_t c = 0; c < s->ncol; c++){ + screen[r][c] = s->lines[r]->chars[c].c; + } + } + } + tmt_clean(vt); + } + } + + void initScreen() { + for (int r = 0; r < ROWS - 1; r++) { + screen[r][COLS] = '\n'; + } + screen[ROWS - 1][COLS] = '\0'; + } + + void updateCursor() { + // Restore char under old cursor + const TMTSCREEN *s = tmt_screen(vt); + screen[curs_r][curs_c] = s->lines[curs_r]->chars[curs_c].c; + + // Update new cursor + const TMTPOINT *c = tmt_cursor(vt); + curs_r = c->r; + curs_c = c->c; + + // Replace char with cursor every other second + struct timeval tv; + gettimeofday(&tv, NULL); + if (tv.tv_sec % 2 == 0) { + screen[curs_r][curs_c] = 0xDB; // block char: █ + } + } +} + +$cc proc termInit {} bool { + if (vt != NULL) { + return false; + } + + initScreen(); + vt = tmt_open(ROWS, COLS, callback, NULL, NULL); + + struct winsize ws = {.ws_row = ROWS, .ws_col = COLS}; + pid_t pid = forkpty(&pty_fd, NULL, NULL, &ws); + if (pid < 0){ + return false; + } else if (pid == 0){ + setenv("TERM", "ansi", 1); + execl(SHELL, SHELL, NULL); + return true; + } + + fcntl(pty_fd, F_SETFL, O_NONBLOCK); + return true; +} + +$cc proc termRead {} char* { + ssize_t r = read(pty_fd, iobuf, PTYBUF); + if (r > 0) { + tmt_write(vt, iobuf, r); + } + + updateCursor(); + return (char*)screen; +} + +$cc proc termWrite {char* key} void { + write(pty_fd, key, strlen(key)); +} + +$cc compile + +# Folk stuff... + +# From `man console_codes` +set keymap [dict create \ + ENTER "\x0d" \ + TAB "\x09" \ + BACKSPACE "\x08" \ + DELETE "\x7f" \ + ESC "\x1b" \ + UP "\x1b\[A" \ + DOWN "\x1b\[B" \ + RIGHT "\x1b\[C" \ + LEFT "\x1b\[D" \ +] + +proc remap {key modifiers} { + upvar keymap keymap + if {[string length $key] == 1} { + # Convert ctrl-A through ctrl-Z and others to terminal control characters + if {"ctrl" in $modifiers} { + set charCode [scan [string toupper $key] %c] + if {$charCode >= 64 && $charCode <= 95} { + set charCode [expr {$charCode - 64}] + return [format %c $charCode] + } + } + # All other single char keys can be passed through + return $key + } + if {[dict exists $keymap $key]} { + return [dict get $keymap $key] + } + return "" +} + +When /anyone/ claims /thing/ is a terminal { + termInit + + When /anyone/ claims key /key/ is /direction/ with modifiers /modifiers/ { + if {$direction != "up"} { + set key [remap $key $modifiers] + if {[string length $key] > 0} { + termWrite $key + } + } + } + + When $thing has region /r/ & /node/ has step count /c/ { + set display [region move $r up 180px] + Wish region $display is labelled [termRead] + } +} -- cgit v1.2.3