tui: Terminal UI for Racket
| (require tui) | package: racket-tui |
A small terminal UI library for Linux and Android/Termux: raw-mode terminal control, mouse and true-color support, bracketed paste, and window-resize events.
Linux / Android (Termux). This library binds directly to Linux termios, signalfd and ioctl. Loading tui on other operating systems raises an error, because the FFI symbols do not exist. Only xterm / qterminal have been tested.
1 Installation
Install from the package index:
raco pkg install tui |
or directly from GitHub:
raco pkg install https://github.com/lu96-wow/racket-tui.git |
2 Quick Start
(require tui) (with-tui (λ () (screen-clear) (cursor-hide) (put-rgb-fg 255 100 0 "Hello TUI!") (put-at 5 10 "Direct output") (sleep 2)))
3 API Conventions
The whole API follows two naming tables (prefix, suffix) and three rules (put/format symmetry, argument order, three color tiers). Once these are known, most function names and signatures can be derived without looking them up.
3.1 Prefix: behavior
Prefix |
| Meaning |
| Example |
put- |
| Immediate terminal output (flushes by default) |
| |
format- |
| Return bytes without outputting; batch with @racket[put-bytes] |
| |
cursor- screen- line- buffer- |
| Cursor / screen / line / alt-buffer operations (immediate) |
| |
clr- |
| 16-color foreground constructor (shorthand for @racket[(color-fg n)]) |
| clr-red |
bclr- |
| 16-color background constructor |
| bclr-blue |
attr- |
| SGR attribute constructor (thunk that emits the escape sequence) |
| |
color- color256- color-rgb- |
| Color constructors (thunks, for @racket[style-define!]) |
| |
style- |
| Style system |
| |
event- |
| Input-event predicates and accessors |
| |
current- |
| Parameters or tracked cursor variables |
|
3.2 Suffix: side effects
Suffix |
| Meaning |
| Example |
! |
| Side effects: updates the tracked cursor / terminal mode |
| |
? |
| Predicate, returns boolean |
| |
-at |
| Positioned (@racket[row] @racket[col] first); DECSC/DECRC so the tracked cursor is untouched |
| |
-at! |
| Positioned and updates the tracked cursor |
| |
-base |
| Escape sequence only: no content, no reset |
|
3.3 Rule 1: put / format symmetry
Almost every capability has both a put- form (immediate output) and a format- form (returns bytes), with identical arguments:
(put-fg 1 "x") (put-rgb-fg-at 1 1 255 0 0 "x") (put-cursor-save)
Exceptions (by design):
put-at corresponds to format-content-at (the -at name is the content form).
format-styled* exists only in the format- form, for batching where one trailing format-reset is appended.
Output entry points such as put and put-bytes have no format- twin; format-content does the conversion.
3.4 Rule 2: argument order
The content v (string / bytes / char / number) is always the last argument.
Positioned functions take row col first.
Color arguments come in the middle: n for 16/256 colors; r g b for RGB; fr fg fb br bg bb (foreground then background) for foreground+background.
(put-rgb-fg-bg-at row col fr fg fb br bg bb v)
3.5 Rule 3: three color tiers are isomorphic
The 16-color, 256-color and RGB tiers are same-named and same-shaped; only the color argument differs (n / n / r g b):
(put-fg n v) (put-256-fg n v) (put-rgb-fg r g b v) (put-fg-at r c n v) (put-256-fg-at r c n v) (put-rgb-fg-at r c r g b v) (put-fg-base n) (put-256-fg-base n) (put-rgb-fg-base r g b)
Knowing any one tier, the other two can be derived.
3.6 Standard API categories
Category |
| Representative functions |
Lifecycle |
| |
Basic output |
| |
Positioned output |
| |
Cursor |
| |
Screen / line / buffer |
| |
Colors |
| |
Styles |
| |
Format (returns bytes) |
| |
Input |
| read-event (typed event?) build-input loop-input; raw: read-event/raw event-mouse? |
Terminal |
| |
Platform |
| |
Cursor tracking |
| |
Config constants |
|
4 Lifecycle
All three with-tui entry points are plain functions taking a thunk. They use dynamic-wind internally, so the terminal is always restored whether the body returns normally or raises an exception. Exceptions from the body propagate outward; they are never swallowed.
procedure
(with-tui body) → any
body : (-> any)
procedure
(with-tui-nobuffer body) → any
body : (-> any)
procedure
(with-tui-nobuffer-echo body) → any
body : (-> any)
procedure
(tui-init) → void?
procedure
(tui-exit) → void?
procedure
(tui-init-no-buffer) → void?
procedure
(tui-exit-no-buffer) → void?
procedure
(tui-init-no-buffer-echo) → void?
procedure
(tui-exit-no-buffer-echo) → void?
procedure
(enable-mouse!) → void?
procedure
(disable-mouse!) → void?
procedure
(enable-bracketed-paste!) → void?
procedure
(disable-bracketed-paste!) → void?
Do not call (exit) inside a with-tui body: it terminates the process without running cleanup, leaving the terminal in raw mode. Use a running? flag plus loop-input/stop instead.
5 Output
All put- functions write to the current output port immediately (one flush per call) by default; use set-buffered-mode! to batch.
procedure
(put v) → void?
v : any/c
procedure
(put-string s) → void?
s : string?
procedure
(put-bytes bs) → void?
bs : bytes?
procedure
(put-byte b) → void?
b : byte?
procedure
(put-char c) → void?
c : char?
procedure
(put-newline) → void?
procedure
(put-format-bytes part ...) → void?
part : bytes?
procedure
(format-newline) → bytes?
procedure
(put-at row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-at! row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
5.1 Cursor
procedure
(cursor-up n) → void?
n : exact-nonnegative-integer?
procedure
(cursor-down n) → void?
n : exact-nonnegative-integer?
procedure
(cursor-left n) → void?
n : exact-nonnegative-integer?
procedure
(cursor-right n) → void?
n : exact-nonnegative-integer?
procedure
(cursor-move row col) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer?
procedure
(cursor-col n) → void?
n : exact-nonnegative-integer?
procedure
(cursor-home) → void?
procedure
(cursor-hide) → void?
procedure
(cursor-show) → void?
procedure
(put-cursor-save) → void?
procedure
(put-cursor-restore) → void?
5.2 Screen and lines
procedure
(screen-clear) → void?
procedure
(screen-clear-below) → void?
procedure
(screen-clear-above) → void?
procedure
(line-clear) → void?
procedure
(line-clear-right) → void?
procedure
(line-clear-left) → void?
procedure
(line-clear-row row) → void?
row : exact-nonnegative-integer?
procedure
(buffer-alt-enable) → void?
procedure
(buffer-alt-disable) → void?
5.3 Flush mode
procedure
(set-immediate-mode!) → void?
procedure
(set-buffered-mode!) → void?
procedure
(flush!) → void?
6 Colors
Colors come in three flavors: 16-color ANSI, 256-color, and true color (RGB). The put- variants print content in that color and reset after.
procedure
(put-fg n v) → void?
n : (integer-in 0 15) v : any/c
procedure
(put-bg n v) → void?
n : (integer-in 0 15) v : any/c
procedure
(put-rgb-fg r g b v) → void?
r : byte? g : byte? b : byte? v : any/c
procedure
(put-rgb-bg r g b v) → void?
r : byte? g : byte? b : byte? v : any/c
procedure
(put-rgb-fg-bg fr fg fb br bg bb v) → void?
fr : byte? fg : byte? fb : byte? br : byte? bg : byte? bb : byte? v : any/c
procedure
(put-256-fg n v) → void?
n : (integer-in 0 255) v : any/c
procedure
(put-256-bg n v) → void?
n : (integer-in 0 255) v : any/c
procedure
(put-reset) → void?
6.1 Positioned colors
The -at variants print in color at a fixed position. Like put-at, they use DECSC/DECRC so the tracked cursor is untouched; the -at! variants update the tracked cursor position instead.
procedure
(put-fg-at row col n v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 15) v : any/c
procedure
(put-fg-at! row col n v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 15) v : any/c
procedure
(put-bg-at row col n v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 15) v : any/c
procedure
(put-bg-at! row col n v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 15) v : any/c
procedure
(put-rgb-fg-at row col r g b v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? r : byte? g : byte? b : byte? v : any/c
procedure
(put-rgb-fg-at! row col r g b v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? r : byte? g : byte? b : byte? v : any/c
procedure
(put-rgb-bg-at row col r g b v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? r : byte? g : byte? b : byte? v : any/c
procedure
(put-rgb-bg-at! row col r g b v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? r : byte? g : byte? b : byte? v : any/c
procedure
(put-rgb-fg-bg-at row col fr fg fb br bg bb v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? fr : byte? fg : byte? fb : byte? br : byte? bg : byte? bb : byte? v : any/c
procedure
(put-rgb-fg-bg-at! row col fr fg fb br bg bb v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? fr : byte? fg : byte? fb : byte? br : byte? bg : byte? bb : byte? v : any/c
procedure
(put-256-fg-at row col n v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 255) v : any/c
procedure
(put-256-fg-at! row col n v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 255) v : any/c
procedure
(put-256-bg-at row col n v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 255) v : any/c
procedure
(put-256-bg-at! row col n v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 255) v : any/c
6.2 Escape-only variants
The -base variants emit only the escape sequence, without content or reset; the attribute variants emit the corresponding SGR sequence:
procedure
(put-fg-base n) → void?
n : (integer-in 0 15)
procedure
(put-bg-base n) → void?
n : (integer-in 0 15)
procedure
(put-rgb-fg-base r g b) → void?
r : byte? g : byte? b : byte?
procedure
(put-rgb-bg-base r g b) → void?
r : byte? g : byte? b : byte?
procedure
(put-rgb-fg-bg-base fr fg fb br bg bb) → void?
fr : byte? fg : byte? fb : byte? br : byte? bg : byte? bb : byte?
procedure
(put-256-fg-base n) → void?
n : (integer-in 0 255)
procedure
(put-256-bg-base n) → void?
n : (integer-in 0 255)
procedure
(put-bold) → void?
procedure
(put-dim) → void?
procedure
(put-italic) → void?
procedure
(put-underline) → void?
procedure
(put-blink) → void?
procedure
(put-reverse) → void?
7 Styles
Styles are named bundles of color and attribute thunks. Define them once, then apply by name. style-define! registers every style in both a 256-color and a 16-color registry; the active registry is selected by current-registry (see use-color-auto!).
7.1 Usage
(style-define! 'fancy clr-yellow bclr-blue attr-bold attr-underline) (put-styled 'fancy "Combined style") (put-styled-at 5 10 'fancy "fixed") (put-styled-at! 5 10 'fancy "cursor") (put-format-bytes (format-styled 'title "Title") (format-styled 'info "body text"))
The two registries give automatic 256/16-color fallback: plain color-fg/attr-bold register the same value in both, while color-fg* and color-bg* take separate values for each:
(style-define! 'status (color-fg* 46 2) attr-bold)
style->bytes returns the precomputed escape bytes for a style; it is what format-styled uses internally. An undefined style name is a no-op: it produces empty bytes without raising an error.
procedure
(style-define! name spec ...) → void?
name : symbol? spec : procedure?
procedure
(style-apply! name) → void?
name : symbol?
procedure
(style-reset) → void?
procedure
(style->bytes name) → bytes?
name : symbol?
procedure
(put-styled name v) → void?
name : symbol? v : any/c
procedure
(put-styled-at row col name v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? name : symbol? v : any/c
procedure
(put-styled-at! row col name v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? name : symbol? v : any/c
procedure
(put-styled-bold v) → void?
v : any/c
procedure
(put-styled-bold-at row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-styled-bold-at! row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-styled-dim v) → void?
v : any/c
procedure
(put-styled-dim-at row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-styled-dim-at! row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-styled-italic v) → void?
v : any/c
procedure
(put-styled-italic-at row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-styled-italic-at! row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-styled-underline v) → void?
v : any/c
procedure
(put-styled-underline-at row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-styled-underline-at! row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-styled-blink v) → void?
v : any/c
procedure
(put-styled-blink-at row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-styled-blink-at! row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-styled-reverse v) → void?
v : any/c
procedure
(put-styled-reverse-at row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(put-styled-reverse-at! row col v) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
7.2 Color and attribute constructors
procedure
(color-fg n) → procedure?
n : (integer-in 0 15)
procedure
(color-bg n) → procedure?
n : (integer-in 0 15)
procedure
(color256-fg n) → procedure?
n : (integer-in 0 255)
procedure
(color256-bg n) → procedure?
n : (integer-in 0 255)
procedure
(color-rgb-fg r g b) → procedure?
r : byte? g : byte? b : byte?
procedure
(color-rgb-bg r g b) → procedure?
r : byte? g : byte? b : byte?
procedure
(color-fg* c256 c16) → color-thunk?
c256 : (integer-in 0 255) c16 : (integer-in 0 15)
procedure
(color-bg* c256 c16) → color-thunk?
c256 : (integer-in 0 255) c16 : (integer-in 0 15)
procedure
(attr-bold) → void?
procedure
(attr-dim) → void?
procedure
(attr-italic) → void?
procedure
(attr-underline) → void?
procedure
(attr-blink) → void?
procedure
(attr-reverse) → void?
7.3 Color mode
procedure
(use-256color!) → void?
procedure
(use-16color!) → void?
procedure
(use-color-auto!) → void?
7.4 Built-in styles
The following styles are pre-registered by the library:
Basic: 'red 'green 'blue 'yellow 'cyan 'magenta 'white
Levels: 'error 'warning 'info 'success
Text: 'title 'subtitle 'heading 'border 'border-bold
Widgets: 'button 'button-hover 'button-pressed 'button-disabled
Menus: 'menu-item 'menu-selected 'menu-key 'menu-shortcut
Lists: 'list-item 'list-selected 'list-alternate
Dialogs: 'dialog-title 'dialog-body 'dialog-button 'dialog-highlight
Status: 'status-bar 'status-good 'status-warning 'status-bad
Input: 'input-normal 'input-focus 'input-error
Misc: 'cursor 'selection 'scroll-track 'scroll-thumb
8 Format functions
The format- functions return byte strings without writing anything. They are meant to be collected and written in one batch with put-format-bytes or put-bytes. Each value-taking variant appends content and a style reset, so format-reset is only needed once at the end of a batch.
procedure
(format-cursor-move row col) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer?
procedure
(format-cursor-up n) → bytes?
n : exact-nonnegative-integer?
procedure
(format-cursor-down n) → bytes?
n : exact-nonnegative-integer?
procedure
(format-cursor-left n) → bytes?
n : exact-nonnegative-integer?
procedure
(format-cursor-right n) → bytes?
n : exact-nonnegative-integer?
procedure
(format-cursor-col n) → bytes?
n : exact-nonnegative-integer?
procedure
(format-cursor-home) → bytes?
procedure
(format-cursor-hide) → bytes?
procedure
(format-cursor-show) → bytes?
procedure
(format-cursor-save) → bytes?
procedure
(format-cursor-restore) → bytes?
procedure
(format-screen-clear) → bytes?
procedure
(format-screen-clear-below) → bytes?
procedure
(format-screen-clear-above) → bytes?
procedure
(format-line-clear) → bytes?
procedure
(format-line-clear-right) → bytes?
procedure
(format-line-clear-left) → bytes?
procedure
(format-line-clear-row row) → bytes?
row : exact-nonnegative-integer?
procedure
(format-buffer-alt-enable) → bytes?
procedure
(format-buffer-alt-disable) → bytes?
procedure
(format-reset) → bytes?
procedure
(format-bold) → bytes?
procedure
(format-dim) → bytes?
procedure
(format-italic) → bytes?
procedure
(format-underline) → bytes?
procedure
(format-blink) → bytes?
procedure
(format-reverse) → bytes?
procedure
(format-content v) → bytes?
v : any/c
procedure
(format-fg n v) → bytes?
n : (integer-in 0 15) v : any/c
procedure
(format-bg n v) → bytes?
n : (integer-in 0 15) v : any/c
procedure
(format-rgb-fg r g b v) → bytes?
r : byte? g : byte? b : byte? v : any/c
procedure
(format-rgb-bg r g b v) → bytes?
r : byte? g : byte? b : byte? v : any/c
procedure
(format-rgb-fg-bg fr fg fb br bg bb v) → bytes?
fr : byte? fg : byte? fb : byte? br : byte? bg : byte? bb : byte? v : any/c
procedure
(format-256-fg n v) → bytes?
n : (integer-in 0 255) v : any/c
procedure
(format-256-bg n v) → bytes?
n : (integer-in 0 255) v : any/c
procedure
(format-fg-base n) → bytes?
n : (integer-in 0 15)
procedure
(format-bg-base n) → bytes?
n : (integer-in 0 15)
procedure
(format-rgb-fg-base r g b) → bytes?
r : byte? g : byte? b : byte?
procedure
(format-rgb-bg-base r g b) → bytes?
r : byte? g : byte? b : byte?
procedure
(format-rgb-fg-bg-base fr fg fb br bg bb) → bytes?
fr : byte? fg : byte? fb : byte? br : byte? bg : byte? bb : byte?
procedure
(format-256-fg-base n) → bytes?
n : (integer-in 0 255)
procedure
(format-256-bg-base n) → bytes?
n : (integer-in 0 255)
procedure
(format-styled name v) → bytes?
name : symbol? v : any/c
procedure
(format-styled* name v) → bytes?
name : symbol? v : any/c
procedure
(format-styled-at row col name v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? name : symbol? v : any/c
procedure
(format-styled-at! row col name v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? name : symbol? v : any/c
procedure
(format-styled-bold v) → bytes?
v : any/c
procedure
(format-styled-bold-at row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-styled-bold-at! row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-styled-dim v) → bytes?
v : any/c
procedure
(format-styled-dim-at row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-styled-dim-at! row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-styled-italic v) → bytes?
v : any/c
procedure
(format-styled-italic-at row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-styled-italic-at! row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-styled-underline v) → bytes?
v : any/c
procedure
(format-styled-underline-at row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-styled-underline-at! row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-styled-blink v) → bytes?
v : any/c
procedure
(format-styled-blink-at row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-styled-blink-at! row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-styled-reverse v) → bytes?
v : any/c
procedure
(format-styled-reverse-at row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-styled-reverse-at! row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-content-at row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-content-at! row col v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? v : any/c
procedure
(format-fg-at row col n v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 15) v : any/c
procedure
(format-fg-at! row col n v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 15) v : any/c
procedure
(format-bg-at row col n v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 15) v : any/c
procedure
(format-bg-at! row col n v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 15) v : any/c
procedure
(format-rgb-fg-at row col r g b v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? r : byte? g : byte? b : byte? v : any/c
procedure
(format-rgb-fg-at! row col r g b v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? r : byte? g : byte? b : byte? v : any/c
procedure
(format-rgb-bg-at row col r g b v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? r : byte? g : byte? b : byte? v : any/c
procedure
(format-rgb-bg-at! row col r g b v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? r : byte? g : byte? b : byte? v : any/c
procedure
(format-256-fg-at row col n v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 255) v : any/c
procedure
(format-256-fg-at! row col n v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 255) v : any/c
procedure
(format-256-bg-at row col n v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 255) v : any/c
procedure
(format-256-bg-at! row col n v) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? n : (integer-in 0 255) v : any/c
procedure
(format-rgb-fg-bg-at row col fr fg fb br bg bb) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? fr : byte? fg : byte? fb : byte? br : byte? bg : byte? bb : byte?
procedure
(format-rgb-fg-bg-at! row col fr fg fb br bg bb) → bytes?
row : exact-nonnegative-integer? col : exact-nonnegative-integer? fr : byte? fg : byte? fb : byte? br : byte? bg : byte? bb : byte?
9 Input
9.1 Events
read-event returns a single normalized event value (event?); the event’s type is determined by its semantics, not by its byte encoding. Every event that carries modifiers uses a mods? struct: there is no positional modifier list and no polymorphic data payload.
procedure
(read-event) → event?
procedure
procedure
(event? v) → boolean?
v : any/c
struct
(struct mods (ctrl? alt? shift?) #:extra-constructor-name make-mods) ctrl? : boolean? alt? : boolean? shift? : boolean?
procedure
(mods->list m) → (list/c boolean? boolean? boolean?)
m : mods?
struct
(struct key-event (key mods) #:extra-constructor-name make-key-event) key : (or/c char? symbol?) mods : mods?
struct
(struct paste-event (bytes text) #:extra-constructor-name make-paste-event) bytes : bytes? text : string?
struct
(struct mouse-event (action button x y mods) #:extra-constructor-name make-mouse-event) action : symbol? button : (or/c symbol? #f) x : exact-nonnegative-integer? y : exact-nonnegative-integer? mods : mods?
struct
(struct resize-event (rows cols) #:extra-constructor-name make-resize-event) rows : exact-positive-integer? cols : exact-positive-integer?
struct
(struct null-event () #:extra-constructor-name make-null-event)
struct
(struct other-event (type data mods) #:extra-constructor-name make-other-event) type : symbol? data : any/c mods : mods?
procedure
(byte->key b) → (or/c char? symbol?)
b : byte?
procedure
(normalize-event type data mods) → event?
type : symbol? data : any/c mods : (or/c #f (list/c boolean? boolean? boolean?))
9.2 High-level: build-input and event loops
procedure
(build-input [ #:key on-key #:text on-text #:paste on-paste #:mouse on-mouse #:resize on-resize #:null on-null #:any on-any #:tab on-tab #:backtab on-backtab #:space on-space #:enter on-enter #:backspace on-backspace #:escape on-escape #:up on-up #:down on-down #:left on-left #:right on-right #:delete on-delete #:insert on-insert #:home on-home #:end on-end #:pageup on-pageup #:pagedown on-pagedown]) → (-> event? any) on-key : (or/c (-> (or/c char? symbol?) mods? any) #f) = #f on-text : (or/c (-> string? any) #f) = #f on-paste : (or/c (-> bytes? any) #f) = #f
on-mouse : (or/c (-> symbol? (or/c symbol? #f) exact-nonnegative-integer? exact-nonnegative-integer? mods? any) #f) = #f
on-resize : (or/c (-> exact-positive-integer? exact-positive-integer? any) #f) = #f on-null : (or/c (-> any) #f) = #f on-any : (or/c (-> event? any) #f) = #f on-tab : (or/c (-> any) #f) = #f on-backtab : (or/c (-> any) #f) = #f on-space : (or/c (-> any) #f) = #f on-enter : (or/c (-> any) #f) = #f on-backspace : (or/c (-> any) #f) = #f on-escape : (or/c (-> any) #f) = #f on-up : (or/c (-> any) #f) = #f on-down : (or/c (-> any) #f) = #f on-left : (or/c (-> any) #f) = #f on-right : (or/c (-> any) #f) = #f on-delete : (or/c (-> any) #f) = #f on-insert : (or/c (-> any) #f) = #f on-home : (or/c (-> any) #f) = #f on-end : (or/c (-> any) #f) = #f on-pageup : (or/c (-> any) #f) = #f on-pagedown : (or/c (-> any) #f) = #f
Dispatch priority (built in): null > resize > paste > mouse > key. Within a key-event, an unmodified named key first tries its shortcut callback (#:tab, #:up, ...); then a printable character (no Ctrl/Alt) goes to #:text; otherwise the key goes to #:key. A paste tries #:paste, then #:text.
Special keys are checked before the generic character handler: a single-byte special key (tab, enter, escape, backspace) is never delivered to #:text or #:key, and #\space is claimed by #:space (when set) before it can reach #:text. A modified key (for example Ctrl+Up or Ctrl+Space) never matches a shortcut and goes to #:key with its modifiers.
9.2.1 Callback arguments
All keywords are optional; an event without a matching callback falls through to #:any, or is ignored if #:any is absent.
Keyword |
| Callback |
| Argument types |
#:key |
| (key mods) |
| key — |
#:text |
| (str) |
| str — |
#:paste |
| (bytes) |
| bytes — |
#:mouse |
| (action button x y mods) |
| action — |
#:resize |
| (rows cols) |
| rows cols — |
#:null |
| () |
| No input available (noblock loop). |
#:any |
| (ev) |
| ev — |
#:tab |
| () |
| Tab, no arguments |
#:backtab |
| () |
| Shift+Tab, no arguments |
#:space |
| () |
| Space, no arguments |
#:enter |
| () |
| Enter, no arguments |
#:backspace |
| () |
| Backspace, no arguments |
#:escape |
| () |
| Escape, no arguments |
#:up |
| () |
| Arrow up, no arguments |
#:down |
| () |
| No arguments |
#:left |
| () |
| No arguments |
#:right |
| () |
| No arguments |
#:delete |
| () |
| Delete, no arguments |
#:insert |
| () |
| No arguments |
#:home |
| () |
| No arguments |
#:end |
| () |
| No arguments |
#:pageup |
| () |
| No arguments |
#:pagedown |
| () |
| No arguments |
The shortcut callbacks only fire for an unmodified named key. A modified named key (for example Ctrl+Up) always goes to #:key with its mods?.
9.2.2 Example
(define handler (build-input #:text (lambda (s) (insert-text s)) #:key (lambda (key mods) (cond [(and (char? key) (mods-ctrl? mods)) (printf "Ctrl+~a\n" key)] [else (printf "key ~a~a\n" (if (mods-alt? mods) "Alt+" "") key)])) #:up (lambda () (cursor-up 1)) #:enter (lambda () (newline)) #:mouse (lambda (action button x y mods) (printf "mouse ~a ~a (~a,~a) ctrl=~a\n" action button x y (mods-ctrl? mods))) #:resize (lambda (rows cols) (printf "~ax~a\n" rows cols)) #:any (lambda (ev) (printf "unhandled: ~a\n" ev))))
syntax
(loop-input handler ...)
syntax
(loop-input-noblock handler ...)
syntax
(loop-input/stop stop-expr handler ...)
syntax
(loop-input-noblock/stop stop-expr handler ...)
9.3 Low-level: read-event/raw
The raw byte-level parser remains available for terminal debugging or custom protocols. It returns a polymorphic triple; prefer read-event in application code.
procedure
→
symbol? (or/c bytes? list? pair?) (or/c #f (list/c boolean? boolean? boolean?))
procedure
→
symbol? (or/c bytes? list? pair?) (or/c #f (list/c boolean? boolean? boolean?))
procedure
(classify-byte b) → (or/c 'ctrl 'escape 'utf8 'key)
b : byte?
procedure
(event-null? type) → boolean?
type : symbol?
procedure
(event-key? type) → boolean?
type : symbol?
procedure
(event-utf8? type) → boolean?
type : symbol?
procedure
(event-seq? type) → boolean?
type : symbol?
procedure
(event-ctrl? type) → boolean?
type : symbol?
procedure
(event-alt? type) → boolean?
type : symbol?
procedure
(event-mod-seq? type) → boolean?
type : symbol?
procedure
(event-resize? type) → boolean?
type : symbol?
procedure
(event-up? type) → boolean?
type : symbol?
procedure
(event-down? type) → boolean?
type : symbol?
procedure
(event-left? type) → boolean?
type : symbol?
procedure
(event-right? type) → boolean?
type : symbol?
procedure
(event-del? type) → boolean?
type : symbol?
procedure
(event-insert? type) → boolean?
type : symbol?
procedure
(event-home? type) → boolean?
type : symbol?
procedure
(event-end? type) → boolean?
type : symbol?
procedure
(event-pageup? type) → boolean?
type : symbol?
procedure
(event-pagedown? type) → boolean?
type : symbol?
procedure
(event-backtab? type) → boolean?
type : symbol?
procedure
(event-mouse? type) → boolean?
type : symbol?
procedure
(event-paste? type) → boolean?
type : symbol?
procedure
(event-tab? type data) → boolean?
type : symbol? data : bytes?
procedure
(event-space? type data) → boolean?
type : symbol? data : bytes?
procedure
(event-backspace? type data) → boolean?
type : symbol? data : bytes?
procedure
(event-enter? type data) → boolean?
type : symbol? data : bytes?
procedure
(event-escape? type data) → boolean?
type : symbol? data : bytes?
procedure
(ctrl->char data) → (or/c char? #f)
data : bytes?
procedure
(alt->char data) → (or/c char? #f)
data : bytes?
procedure
(mod-seq->char data) → (or/c char? #f)
data : bytes?
procedure
(mod-seq->key data) → (or/c symbol? #f)
data : bytes?
procedure
(event->string data) → string?
data : bytes?
procedure
(event->byte data) → (or/c byte? #f)
data : bytes?
procedure
(mouse-press? detail) → boolean?
detail : list?
procedure
(mouse-release? detail) → boolean?
detail : list?
procedure
(mouse-move? detail) → boolean?
detail : list?
procedure
(mouse-scroll? detail) → boolean?
detail : list?
procedure
(mouse-left? detail) → boolean?
detail : list?
procedure
(mouse-middle? detail) → boolean?
detail : list?
procedure
(mouse-right? detail) → boolean?
detail : list?
procedure
(scroll-up? detail) → boolean?
detail : list?
procedure
(scroll-down? detail) → boolean?
detail : list?
procedure
(mouse-x detail) → exact-nonnegative-integer?
detail : list?
procedure
(mouse-y detail) → exact-nonnegative-integer?
detail : list?
procedure
(get-mouse-pos detail) →
exact-nonnegative-integer? exact-nonnegative-integer? detail : list?
procedure
(mouse-modifiers detail) → (list/c boolean? boolean? boolean?)
detail : list?
procedure
(get-resize-rows data) → exact-positive-integer?
data : (cons/c exact-positive-integer? exact-positive-integer?)
procedure
(get-resize-cols data) → exact-positive-integer?
data : (cons/c exact-positive-integer? exact-positive-integer?)
procedure
(get-resize-size data) →
exact-positive-integer? exact-positive-integer? data : (cons/c exact-positive-integer? exact-positive-integer?)
10 Terminal
procedure
(terminal?) → boolean?
procedure
(enter-raw-mode!) → void?
procedure
(exit-raw-mode!) → void?
procedure
(enter-raw-mode-keep-echo!) → void?
procedure
(call-with-terminal-reply thunk) → any
thunk : (-> any)
procedure
(make-stdin-evt) → evt?
procedure
(get-window-size [fd]) →
(or/c exact-positive-integer? #f) (or/c exact-positive-integer? #f) fd : exact-integer? = 1
procedure
(resize-monitor-start) → void?
procedure
(resize-monitor-stop) → void?
procedure
(make-resize-evt) → evt?
10.1 Platform detection
procedure
(android?) → boolean?
procedure
(termux?) → boolean?
procedure
(termux-home) → (or/c string? #f)
procedure
(termux-prefix) → (or/c string? #f)
11 Cursor state
The library tracks the cursor position in two parameters; -at functions update it.
value
current-cursor-row : exact-nonnegative-integer?
value
current-cursor-col : exact-nonnegative-integer?
procedure
(set-cursor! row col) → void?
row : exact-nonnegative-integer? col : exact-nonnegative-integer?
procedure
(get-cursor) →
exact-nonnegative-integer? exact-nonnegative-integer?
procedure
(update-cursor!) →
exact-nonnegative-integer? exact-nonnegative-integer?
12 Configuration constants
value
ESCDELAY : real?
value
CSI-MAX-BYTES : exact-positive-integer?
value
PASTE-MAX-BYTES : exact-positive-integer?
value
UTF8-READ-TIMEOUT : real?
value
PASTE-READ-TIMEOUT : real?
13 Complete example
(require tui) (define (draw-ui) (define buffer (bytes-append format-screen-clear (format-cursor-move 0 0) (format-rgb-fg 255 255 0) #"=== TUI Demo ===" format-reset (format-cursor-move 2 0) (format-rgb-fg 0 255 0) #"Press 'q' to quit" format-reset (format-cursor-move 4 0) (format-rgb-fg 255 0 0) #"Hello, TUI!" format-reset (format-cursor-move 6 0) (format-256-fg 46 "UTF-8 support: 你好世界") format-reset)) (put-bytes buffer)) (with-tui (λ () (cursor-hide) (define running? #t) (define handler (build-input #:key (lambda (key mods) (when (eqv? key #\q) (set! running? #f))))) (define (render-and-handle ev) (handler ev) (draw-ui)) (loop-input/stop (not running?) render-and-handle)))
14 Character backend (tui/char)
tui/char is a drop-in mirror of the whole API whose output goes to an in-memory character grid instead of the terminal. It renders each frame as plain text (no escape codes), which makes it practical for AI agents, tests and CI to inspect UI state without a TTY. Switch backends by the require path only; call sites stay unchanged. Unlike tui, tui/char does not load termios or FFI, so it runs on any platform or sandbox.
(require tui) (require tui/char)
(require tui/char) (parameterize ([current-screen-size (cons 6 30)]) (with-tui (lambda () (put-bytes (bytes-append format-screen-clear (format-cursor-move 1 1) (format-rgb-fg 255 255 0 "=== TUI Demo ===") (format-cursor-move 3 1) (format-256-fg 46 "count = 42"))) (displayln (char-frame)))))
prints a plain-text layout:
=== TUI Demo === |
|
count = 42 |
14.1 How it works
Every output path in base funnels through format-* (which produces ANSI bytes) and put-* (which writes them). The character backend keeps the same API but substitutes two leaves:
format-* returns an op (or op sequence) instead of ANSI bytes, and
put-* applies those ops to the character grid.
Because the semantic information is available at the formatter, there is no ANSI parsing on the main path. bytes-append is shadowed (module-locally) by an op concatenator so the documented batch idiom keeps working verbatim:
(bytes-append format-screen-clear (format-cursor-move 1 1) "hi")
An ANSI parser (make-ansi-parser) is retained only as a test oracle: the same UI rendered through ops and through ANSI bytes must agree.
14.2 Grid output, cursor and attributes
(char-frame) returns the current screen as plain text.
(screen->text g) / (screen->lines g) render a given grid; #:trim-right? defaults to #t.
(screen-ref g row col) returns a cell; cell-text, cell-fg, cell-bg and cell-attrs read it. (style->string c) renders a readable style such as "fg#1 bold".
(screen-current-style g) reports the SGR state that the next character would carry.
(screen-styled-cells g) lists all non-default cells; (screen-attr-ranges g) compresses attributes into per-row ranges.
(get-cursor) returns (row, col) 1-based (as in base); (screen-cursor g) returns 0-based.
14.3 Scripted input and event loops
Input comes from a script queue rather than the terminal. (char-input-push! spec ...) accepts an event struct, a character, a named-key symbol such as 'enter, a (key mods) pair, or a raw (type data mods) tuple.
(read-event) blocks by default (like the real terminal), so a loop does not spin when there is no input; (read-event-noblock) is the non-blocking variant. Call (char-input-close!) when the script ends; (read-event) then returns a null-event and (char-input-exhausted?) can be used as the stop condition. Both build-input and the loop-input* macros are provided with the same signatures as tui.
14.4 Frames and bounded runs
flush! triggers current-frame-hook with the current grid, but only when the rendered character grid actually changed (current-frame-dedup?, on by default), so an idle loop does not keep emitting frames. (screen-frame-log-enable! path) appends every frame as plain text to a file; (screen-frame-count) and (screen-frame-log-disable!) complete the API.
For deterministic debugging, char-run processes a finite list of events, rendering once per event and returning the sequence of frame strings (at most one frame per event plus the initial frame), so headless runs never flood output:
(char-run (list #\+ #\+ #\-) #:handle (lambda (ev) ...) #:render (lambda () ...) #:rows 24 #:cols 80)
14.5 Related entry points
(with-tui thunk) plus (parameterize ([current-screen-size (cons 24 80)]) ...) — sized session.
current-screen, (the-screen), (current-screen-size) — grid session state.
(screen-size g), (screen-cursor-cell g), (screen-style-at g row col) — queries.
The full reference lives in base-char/README.md in the source repository.
15 Implementation notes
The termios struct layout and flag constants in base/terminal/base.rkt are hardcoded (previously generated by compiling a C program). The values come from the kernel’s asm-generic/termbits.h and are identical across mainstream Linux architectures (x86, arm, aarch64, riscv, ppc, mips, sparc). TERMIOS-SIZE is fixed at 60, which is safe for both glibc and musl. The only exception is the alpha architecture, which is not supported.