A self-contained terminal abstraction for a TUI rendering target.
This namespace OWNS the normalized key-event model that downstream TUI code consumes, a pure
(JLine-free) key decoder, a Terminal protocol, a real JLine-backed implementation, and an
atom-backed fake (string-terminal) used for tests.
This is JVM/babashka only (plain .clj).
A self-contained terminal abstraction for a TUI rendering target. This namespace OWNS the normalized key-event model that downstream TUI code consumes, a pure (JLine-free) key decoder, a `Terminal` protocol, a real JLine-backed implementation, and an atom-backed fake (`string-terminal`) used for tests. This is JVM/babashka only (plain `.clj`).
(csi-event rest-ints)Returns [event remaining] for a CSI (ESC [) sequence, given rest-ints (the ints AFTER the
leading 27 91). Returns nil if the sequence is not recognized so the caller can fall back.
Returns `[event remaining]` for a CSI (ESC `[`) sequence, given `rest-ints` (the ints AFTER the leading `27 91`). Returns nil if the sequence is not recognized so the caller can fall back.
(csi-u-event rest-ints)Returns [event remaining] for a Kitty/fixterms CSI-u key sequence, given rest-ints (the ints
AFTER the leading 27 91). The form is <codepoint> [; <modifiers>] u (final byte u = 117). The
modifier field is 1 + bitmask where bit 0 = shift, bit 1 = alt, bit 2 = ctrl. Returns nil when the
sequence is not a well-formed CSI-u sequence, so the caller can fall back to csi-event.
:char is populated only for unmodified or shift-only keys (actual text); ctrl/alt combos carry a
nil :char, matching the legacy control-code decoding.
Returns `[event remaining]` for a Kitty/fixterms CSI-u key sequence, given `rest-ints` (the ints AFTER the leading `27 91`). The form is `<codepoint> [; <modifiers>] u` (final byte `u` = 117). The modifier field is `1 + bitmask` where bit 0 = shift, bit 1 = alt, bit 2 = ctrl. Returns nil when the sequence is not a well-formed CSI-u sequence, so the caller can fall back to `csi-event`. `:char` is populated only for unmodified or shift-only keys (actual text); ctrl/alt combos carry a nil `:char`, matching the legacy control-code decoding.
(ctrl-letter n)Returns the lowercase letter string for a control code n in 1..26 (1 -> "a" .. 26 -> "z").
Returns the lowercase letter string for a control code `n` in 1..26 (1 -> "a" .. 26 -> "z").
(cursor t)Returns the last recorded cursor map {:x :y :visible?} for the fake terminal t, or nil.
Returns the last recorded cursor map `{:x :y :visible?}` for the fake terminal `t`, or nil.
(cursor-position-string x y)Returns the ANSI escape sequence that moves the cursor to 0-based (x,y). ANSI is 1-based, so
both are incremented.
Returns the ANSI escape sequence that moves the cursor to 0-based (`x`,`y`). ANSI is 1-based, so both are incremented.
(decode-key ints)Decodes the next key from a sequence of input code points ints.
Returns [event remaining-ints], consuming exactly the code points for one key, or nil when
ints is empty. The decoder is pure and contains no JLine/IO dependency. Handles:
:tab; 10 or 13 -> :enter; 8 or 127 -> :backspace:escape27 91 <cp> [; <mods>] u) -> modified keys with :ctrl?/:alt?/:shift?
(Kitty/fixterms enhanced keyboard protocol)27 91 ...) -> arrows, home/end, delete, page-up/down{:ctrl? true :key "a".."z"} (tab/enter handled first)Decodes the next key from a sequence of input code points `ints`.
Returns `[event remaining-ints]`, consuming exactly the code points for one key, or `nil` when
`ints` is empty. The decoder is pure and contains no JLine/IO dependency. Handles:
* printable ASCII / multi-byte unicode code points -> printable event
* 9 -> `:tab`; 10 or 13 -> `:enter`; 8 or 127 -> `:backspace`
* 27 alone (nothing following) -> `:escape`
* CSI-u sequences (`27 91 <cp> [; <mods>] u`) -> modified keys with `:ctrl?/:alt?/:shift?`
(Kitty/fixterms enhanced keyboard protocol)
* CSI cursor/edit sequences (`27 91 ...`) -> arrows, home/end, delete, page-up/down
* control combos 1..26 -> `{:ctrl? true :key "a".."z"}` (tab/enter handled first)(feed! t & events)Enqueues additional scripted key events onto the fake terminal t's read queue.
Enqueues additional scripted key `events` onto the fake terminal `t`'s read queue.
(jline-terminal)Returns a Terminal backed by a system JLine terminal (TerminalBuilder).
Returns a `Terminal` backed by a system JLine terminal (`TerminalBuilder`).
(key-event key)(key-event key opts)Returns a normalized key event map. key is a 1-char string (printable) or a keyword from
special-keys. The remaining values default to a non-modified, non-char event; pass opts
(a map of any of :char :ctrl? :alt? :shift? :raw) to override.
Returns a normalized key event map. `key` is a 1-char string (printable) or a keyword from `special-keys`. The remaining values default to a non-modified, non-char event; pass `opts` (a map of any of `:char :ctrl? :alt? :shift? :raw`) to override.
(output t)Returns the accumulated string written to the fake terminal t.
Returns the accumulated string written to the fake terminal `t`.
(printable-event cp)Returns a printable ::key-event for the unicode code point cp (the :key and :char are the
1-char string for cp).
Returns a printable `::key-event` for the unicode code point `cp` (the `:key` and `:char` are the 1-char string for `cp`).
(probe-enhanced-keys!)Diagnostic: builds a system JLine terminal, sends the Kitty/CSI-u progressive-enhancement query,
reads whatever the terminal replies (no alt-screen, no shortcut handling), restores the terminal,
and PRINTS the raw reply bytes and the OK/not-OK verdict to stdout. Run this directly in the
terminal you want to test (e.g. clojure -e "((requiring-resolve 'com.fulcrologic.fulcro.tui.terminal/probe-enhanced-keys!))").
A conforming terminal replies ESC [ ? <flags> u (bytes start 27 91 63, end 117).
Diagnostic: builds a system JLine terminal, sends the Kitty/CSI-u progressive-enhancement query, reads whatever the terminal replies (no alt-screen, no shortcut handling), restores the terminal, and PRINTS the raw reply bytes and the OK/not-OK verdict to stdout. Run this directly in the terminal you want to test (e.g. `clojure -e "((requiring-resolve 'com.fulcrologic.fulcro.tui.terminal/probe-enhanced-keys!))"`). A conforming terminal replies `ESC [ ? <flags> u` (bytes start `27 91 63`, end `117`).
(probe-key!)(probe-key! n)Diagnostic: builds a system JLine terminal, ENABLES the enhanced keyboard protocol (same push the
driver does), then reads n (default 8) keypresses and prints, for each, the raw code points and
the decode-key result. Use this to see what a terminal actually sends for a chord like Alt-s
AFTER the protocol is enabled — a Kitty-conforming terminal sends ESC [ <cp> ; <mods> u; iTerm
with Left-Option set to compose sends a single composed code point and NO :alt?.
Run directly in the target terminal, press the keys to test, then press q:
clojure -e "((requiring-resolve 'com.fulcrologic.fulcro.tui.terminal/probe-key!))"
Diagnostic: builds a system JLine terminal, ENABLES the enhanced keyboard protocol (same push the driver does), then reads `n` (default 8) keypresses and prints, for each, the raw code points and the `decode-key` result. Use this to see what a terminal actually sends for a chord like Alt-s AFTER the protocol is enabled — a Kitty-conforming terminal sends `ESC [ <cp> ; <mods> u`; iTerm with Left-Option set to compose sends a single composed code point and NO `:alt?`. Run directly in the target terminal, press the keys to test, then press `q`: `clojure -e "((requiring-resolve 'com.fulcrologic.fulcro.tui.terminal/probe-key!))"`
(read-decimal v)Returns [n remaining] for the leading run of ASCII decimal digits in v (a vector of ints), or
nil when v does not start with a digit.
Returns `[n remaining]` for the leading run of ASCII decimal digits in `v` (a vector of ints), or nil when `v` does not start with a digit.
(resize! t rows cols)Sets the fake terminal t dimensions to rows x cols, then invokes its registered resize handler
(see t-on-resize!), if any — simulating a real terminal's SIGWINCH delivery.
Sets the fake terminal `t` dimensions to `rows` x `cols`, then invokes its registered resize handler (see `t-on-resize!`), if any — simulating a real terminal's SIGWINCH delivery.
(single-codepoint-string? s)Returns true if s is a string consisting of exactly one unicode code point. Note that an
astral/supplementary code point occupies two UTF-16 chars yet is still a single code point, so
this counts code points rather than count (which counts UTF-16 code units).
Returns true if `s` is a string consisting of exactly one unicode code point. Note that an astral/supplementary code point occupies two UTF-16 chars yet is still a single code point, so this counts code points rather than `count` (which counts UTF-16 code units).
The set of keyword values allowed as a special-key :key.
The set of keyword values allowed as a special-key `:key`.
(string-terminal {:keys [rows cols keys sync? enhanced-keys?]
:or {rows 24 cols 80 keys []}})Returns an atom-backed fake Terminal for testing. opts:
:rows - terminal height (default 24):cols - terminal width (default 80):keys - a seq of scripted ::key-events that t-read-key will dequeue, in order:sync? - the boolean reported by t-sync-supported? (default false):enhanced-keys? - the boolean reported by t-enhanced-keys? (default false)Use the accessors output, cursor, feed!, and resize! to drive/inspect it.
Returns an atom-backed fake `Terminal` for testing. `opts`: * `:rows` - terminal height (default 24) * `:cols` - terminal width (default 80) * `:keys` - a seq of scripted `::key-event`s that `t-read-key` will dequeue, in order * `:sync?` - the boolean reported by `t-sync-supported?` (default false) * `:enhanced-keys?` - the boolean reported by `t-enhanced-keys?` (default false) Use the accessors `output`, `cursor`, `feed!`, and `resize!` to drive/inspect it.
Abstraction over a terminal device. Coordinates are 0-based (x column, y row).
Abstraction over a terminal device. Coordinates are 0-based (`x` column, `y` row).
(t-enhanced-keys? t)Returns true if the enhanced (Kitty/fixterms CSI-u) keyboard protocol was detected and enabled for this terminal (so modified keys arrive reliably as CSI-u sequences). False otherwise; the keyboard-shortcut layer is gated on this.
Returns true if the enhanced (Kitty/fixterms CSI-u) keyboard protocol was detected and enabled for this terminal (so modified keys arrive reliably as CSI-u sequences). False otherwise; the keyboard-shortcut layer is gated on this.
(t-flush! t)Flushes buffered output.
Flushes buffered output.
(t-sync-supported? t)Returns true if synchronized output (DEC 2026 / terminfo Sync) is available.
Returns true if synchronized output (DEC 2026 / terminfo Sync) is available.
(t-leave! t)Restores: shows cursor, leaves alt screen, exits raw mode, closes.
Restores: shows cursor, leaves alt screen, exits raw mode, closes.
(t-size t)Returns {:rows R :cols C}.
Returns `{:rows R :cols C}`.
(t-enter! t)Enters raw mode + alternate screen + hides the cursor.
Enters raw mode + alternate screen + hides the cursor.
(t-write! t s)Writes string s to the terminal (no flush).
Writes string `s` to the terminal (no flush).
(t-set-cursor! t x y visible?)Positions the hardware cursor at 0-based (x,y) and shows/hides it.
Positions the hardware cursor at 0-based (`x`,`y`) and shows/hides it.
(t-read-key t)Returns the next normalized key event (blocking for real terminals), or nil on EOF/empty.
Returns the next normalized key event (blocking for real terminals), or nil on EOF/empty.
(t-on-resize! t handler)Registers zero-arg handler to be invoked when the terminal's size changes (e.g. SIGWINCH on a
real terminal). At most one handler is kept; registering again replaces it. nil clears it.
Registers zero-arg `handler` to be invoked when the terminal's size changes (e.g. SIGWINCH on a real terminal). At most one handler is kept; registering again replaces it. `nil` clears it.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |