One coherent change: make the mudlib TUI library (/std/tui) work in the browser on BOTH web terminals, harden everything the work surfaced, and pin the toolchain that broke it. ## Web terminals: xterm.js + a shared telnet client * Vendor @xterm/xterm 6.0.0 + @xterm/addon-fit 0.11.0 (dist files byte-exact from the official npm tarballs, licenses included) under src/www/vendor/. xterm.js does the terminal emulation on both pages: rendering, SGR 16/256/truecolor, alternate screen, cursor state, wide characters, scrollback, mouse reporting, bracketed paste, and keyboard encoding (the whole dialect testsuite/std/tui/keys.lpc decodes, including C-_ undo). * src/www/telnet.js -- one telnet option engine shared by both pages (transport-agnostic; hooks for page-specific options): ECHO masks the password prompt, WILL/WONT SGA -- the driver's char-mode signal (set_charmode) -- automatically switches between the line-input bar and raw keystroke streaming, NAWS reports real terminal geometry from the fit addon and re-reports on resize (driving the window_size apply), TTYPE answers xterm-256color. * src/www/wasm/index.html (the wasm shell): output/input rides xterm.js; the page keeps the synchronous-bridge queueing (sends flush outside receive() -- the wasm bridge re-enters the parser otherwise), the error modal, and the jsbridge handlers. Also guards crypto.getRandomValues() against views backed by resizable ArrayBuffers (see the emsdk section below). * src/www/index.html (the websocket client for the native driver): rewritten on the same stack, replacing a parseANSI() that stripped all cursor sequences (no TUI possible) and a telnet layer whose option bytes leaked into the text stream and never answered negotiations. Passwords now mask, char mode works, GMCP/MSP kept (dead TelnetOverWebSocket/handler classes removed); ws frames arrive as arraybuffers (no Blob/FileReader path); UTF-8 and telnet sequences survive frame splits (streaming decode + stateful parser). * tools/wasm/pack-mudlib.sh and the release zip ship vendor/ and telnet.js next to index.html in both layouts. ## Driver: websocket output wedged permanently on multi-window bursts Re-arming the writeable event from inside LWS_CALLBACK_SERVER_WRITEABLE is lossy with the libevent event lib: after the user callback returns, lws core clears POLLOUT and its pollfd bookkeeping desyncs from the evlib watcher -- the request is dropped and every later lws_callback_on_writable() no-ops, freezing output on that connection for good. First bites on any burst larger than one 2048-byte window (e.g. a full-screen TUI frame; no test had ever pushed one through a ws client). The ws_telnet.cc/ws_ascii.cc handlers now drain the evbuffer in a loop gated on lws_send_pipe_choked(); a choked write is flushed by lws's own core-managed POLLOUT path, which fires the callback again. Found by the browser end-to-end run below; documented in AGENTS.md 14. ## TUI library (/std/tui): review fixes + features Fixes: wslice() dropped combining marks from every sliced render; ESC[1;mR (modified F3) misdecoded as a cursor position report; readline lost the left scroll marker when a line overflowed both viewport edges; stray mouse events cancelled incremental search; Tab on a unique already-complete match missed the trailing space; menu lines wider than the terminal wrapped and desynced the in-place repaint (width now fed by the glue and re-fed on NAWS resize); the menu overflow indicator only showed below the window; backward focus cycling from the initial state skipped the last widget; a terminal-initiated close (disconnect, tui_destroy) leaked the app clone and its widgets -- teardown now runs through a reentry-guarded app_quit() in both directions. Features (from the README's own deferred list): readline C-_ undo (per-keystroke snapshots); pterm-style type-to-filter in select/ multiselect (results index the original choices); mouse-wheel scrolling in list/table/tree/log (wheel no longer click-selects); table clicks honour the header offset; tree Left on a leaf jumps to its parent. All pinned by testsuite/single/tests/std/tui/fixes.lpc, including the app teardown cycle via a runtime-written mock terminal. ## Toolchain: pin emsdk, guard random_get() emsdk 6.0.2 defaulted GROWABLE_ARRAYBUFFERS=1, making every ALLOW_MEMORY_GROWTH build's heap a resizable ArrayBuffer (wasmMemory.toResizableBuffer()) in browsers shipping the wasm rab-integration -- and two emscripten runtime paths pass raw HEAPU8.subarray() views into Web APIs that reject resizable-backed views: random_get() -> crypto.getRandomValues() (threw at boot) and UTF8ToString() -> TextDecoder (broke jsbridge). 6.0.3 reverted the default AND fixed the string codegen (getUnsharedTextDecoderView -> getHeapViewOrCopy), but random_get() is still unguarded upstream. Reproduced and certified against real 6.0.2/6.0.3 toolchains in Chromium (--js-flags=--experimental-wasm-rab-integration): 6.0.2 unpatched throws the exact boot error, 6.0.2 + the page's getRandomValues wrapper passes, 6.0.3 passes. CI now installs a pinned emsdk-ver input (default 6.0.3) instead of "latest". ## Verification * Native Debug+ASan: GTest 312/312; full LPC suite (574 files, per-file ref-count checker) x3 across the work; TUI test dir x3 randomized. * LPC suite inside the wasm driver under node: 5274 checks, 574 files. * Browser e2e (Playwright + Chromium): wasm shell 21/21 checks (charts/SGR, char-mode auto-switch, readline editing + undo + history, select with filter, multiselect/confirm, full-screen app, dashboard live repaint + NAWS resize relayout); websocket client against the native driver 13/13 twice on one instance (plus an ascii-subprotocol multi-window burst) -- the flow that caught the lws wedge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UvX1HbmGk9zWGBsW4Rkcq |
||
|---|---|---|
| .. | ||
| w | ||
| ansi.lpc | ||
| app.lpc | ||
| canvas.lpc | ||
| keys.lpc | ||
| menu.lpc | ||
| print.lpc | ||
| readline.lpc | ||
| README.md | ||
| screen.lpc | ||
| terminal.lpc | ||
| widget.lpc | ||
/std/tui — the LPC TUI library
A terminal UI toolkit written in pure LPC, layered like readline +
ncurses + pterm: line editing / history / incremental search for the
prompt line, inline select/multiselect/confirm prompts and pretty-printers
for ordinary output, plus a screen buffer, diff renderer, and widget set for
full-screen TUIs. Lives in /std/tui/ of the testsuite mudlib and is
portable to any FluffOS mudlib.
Try it: boot the testsuite (driver etc/config.test), telnet to port
4000, and run the showcases:
| Command | Shows |
|---|---|
tuidemo |
readline prompt: editing, ↑/↓ history, Ctrl-R search, Tab completion |
tuidemo select |
inline select → multiselect → confirm prompt chain |
tuidemo print |
the pterm-style printers, in plain line mode |
tuidemo charts |
vertical bars, braille line chart, heatmap, sparkline |
tuidemo app |
minimal full-screen app (list + textfield) |
tuidemo dashboard |
animated spinner, progress bars, sparkline, live table + log |
tuidemo form |
textfield, radio group, checkbox list, buttons |
1. Driver substrate (research summary)
Everything the library needs already exists in the driver, with the quirks fixed as part of this work (see §6):
| Capability | Mechanism |
|---|---|
| Raw keystrokes | get_char(cb, flags) — one key per callback, must be re-armed from inside the callback to stay in char mode (src/comm.cc call_function_interactive reverts to line mode otherwise). Flag I_NOECHO (0x1) keeps client echo off across re-arms. |
| Echo control | I_NOECHO → driver sends telnet IAC WILL ECHO (set_localecho). |
| Terminal size | request_term_size() → client NAWS → apply window_size(int width, int height) on the interactive object; re-fires on every resize. No query efun — the library caches it. |
| Terminal type | start_request_term_type() → apply terminal_type(string term). |
| Raw ANSI output | write()/tell_object()/receive() pass ESC (0x1b) through unmolested; telnet layer only rewrites \n→CRLF (desirable) and doubles IAC (0xFF). |
| Display width | strwidth(s) (UAX#11 columns), width-aware sprintf padding. Neither skips ANSI escapes — the library provides visible_width(). |
| Timers | call_out_walltime(fp, float) for the lone-ESC disambiguation timeout. |
Constraints that shaped the design:
- One byte-ish event per callback, one pending handler per user. Escape
sequences (
ESC [ A= arrow-up) arrive as separateget_charcallbacks, so the key decoder is an explicit state machine, and a real lone ESC can only be distinguished by a short timeout (industry standard; we use a walltime call_out driven by the glue layer, keeping the decoder itself time-agnostic and unit-testable). - No prompt interference: while a
get_charis pending the driver never prints prompts, so the library owns the prompt line entirely. - Char/echo/NAWS negotiation requires a telnet-speaking transport (raw
telnet, websocket
telnetsubprotocol, or the WASM bridge — all supported; the websocketasciisubprotocol is not). - The driver must not be configured with
__RC_NO_ANSI__+__RC_STRIP_BEFORE_PROCESS_INPUT__(that rewrites inbound ESC to a space).
2. Architecture
Five modules, strictly layered; each lower layer knows nothing about the ones
above. Everything except terminal.lpc is a pure state machine — no
efuns with side effects, no interactivity — so the whole stack is exercised by
the non-interactive testsuite.
┌──────────────────────────────────────────────────────┐
apps │ /command/tuidemo.lpc + /clone/tuidemo_*.lpc demos │
├──────────────────────────────────────────────────────┤
glue │ terminal.lpc (get_char loop, NAWS/TTYPE, ESC │
│ timeout, cleanup; inherit into user objects; │
│ tui_readline / tui_select / tui_confirm / tui_open)│
├──────────────┬──────────┬────────────────────────────┤
engines │ readline.lpc │ menu.lpc │ screen.lpc widget.lpc │
│ (line editor)│ (select) │ (+ w/* widget set) app.lpc│
├──────────────┴──────────┴────────────────────────────┤
input │ keys.lpc (bytes → key events) │
├──────────────────────────────────────────────────────┤
output │ ansi.lpc (escapes, widths) print.lpc (printers) │
└──────────────────────────────────────────────────────┘
include/tui.h (key codes, states, shared macros)
2.1 ansi.lpc — escape sequences & width (stateless, inheritable)
Builders for the sequences a TUI needs: cursor addressing/motion (CUP,
CUU/CUD/CUF/CUB), erase (ED, EL), SGR color/attribute (16-color,
256-color, truecolor), alternate screen (?1049), cursor show/hide, SGR mouse
reporting on/off, bracketed-paste on/off. Plus the width toolkit the driver
lacks: visible_width(s) (display columns ignoring CSI/OSC runs),
visible_pad(s, w), strip_ansi(s), and width_slice(s, start_col, ncols)
(codepoint slice by display column, wide-char aware) used by readline's
horizontal scrolling and screen clipping.
2.2 keys.lpc — key decoder (clonable state machine)
feed(string chunk) consumes what get_char delivered and returns an array
of decoded events; flush() resolves a dangling ESC (called by the glue's
timeout); has_pending() tells the glue whether to schedule that timeout.
Event representation (no classes — portable across programs):
- string — printable text (one character; a whole paste in one event).
- int — everything else. Special keys are
0x110000 + n(above the Unicode range so they never collide with a codepoint); modifiers are bits above that:TUI_MOD_SHIFT/ALT/CTRL.Ctrl-AisTUI_MOD_CTRL | 'a',Alt-bisTUI_MOD_ALT | 'b', Ctrl-Right isTUI_MOD_CTRL | TUI_KEY_RIGHT. - array — mouse:
({ TUI_EV_MOUSE, button, x, y, pressed })from SGR (1006) reports.
Grammar handled: CSI sequences with numeric params and modifier encoding
(ESC [ 1 ; 5 C), SS3 (ESC O P F-keys/keypad), ESC [ n ~ legacy keys,
Alt-prefixed printables, bracketed paste (ESC [ 200~ … 201~ → single text
event), SGR mouse (ESC [ < b;x;y M/m), and bare control bytes (CR→ENTER,
TAB, BS/DEL→BACKSPACE, C-a…C-z). Unknown sequences are swallowed and reported
as TUI_KEY_UNKNOWN rather than leaking bytes into the app.
2.3 readline.lpc — line editor (clonable state machine)
The prompt-line centerpiece. Holds buffer, cursor (codepoint index), history,
kill buffer, search state, and a horizontal-scroll viewport; consumes key
events, accumulates ANSI output to repaint its own line only (no absolute
cursor addressing — works at any scroll position using \r, EL, CUF).
API (engine only — the glue below wires it to a real user):
void rl_set_width(int cols); // from NAWS; re-render on resize
void rl_set_prompt(string p); // may contain ANSI colour
void rl_set_history(string *lines); // oldest first; shared/persistable
void rl_set_completer(function f); // f(text, point) -> string *cands
void rl_set_masked(int on); // password mode: render '*'s
string rl_begin(string initial); // reset + initial paint
int rl_feed(mixed ev); // TUI_RL_{MORE,DONE,ABORT,EOF}
string rl_take_output(); // drain pending repaint bytes
string rl_line(); // the accepted / current line
string *rl_history();
Default keymap (emacs/readline):
| Keys | Action |
|---|---|
| printables / paste | insert at cursor |
| ←/→, C-b/C-f | move by char; C-←/→, M-b/M-f by word |
| Home/End, C-a/C-e | line start/end |
| BS / Del, C-d | delete back / forward (C-d on empty line = EOF) |
| C-w / M-d / C-k / C-u | kill word-back / word-fwd / to-end / whole line, into kill buffer |
| C-y | yank kill buffer |
| C-t | transpose chars |
| ↑/↓, C-p/C-n | history prev/next (current edit is preserved at the bottom) |
| C-r / C-s | incremental history search (reverse/forward); type to refine, repeat to step, Enter accepts, C-g restores, other keys accept + execute normally |
| Tab | completion: insert longest common prefix; on ambiguity, list candidates above the line |
| Enter | accept → TUI_RL_DONE |
| C-c | abort → TUI_RL_ABORT |
| C-l | clear screen and repaint the line |
| C-_ | undo (snapshots taken per buffer-changing key, 200 deep) |
Rendering: single logical line with horizontal scrolling when
visible_width(prompt+buffer) >= cols (busybox/readline --horizontal-scroll
style, with </> scroll markers). Wide (CJK) characters are handled via
column-aware slicing. Repaint is a full line rewrite (\r + prompt + view +
EL(0) + cursor reposition) — simple, correct, and cheap at human typing
rates. Multi-line soft-wrap is a possible v2 (see §7).
2.4 screen.lpc + widget.lpc — full-screen TUIs
screen.lpc (clonable) is the ncurses core: a virtual cell grid + damage
tracking + minimal-update renderer.
void scr_resize(int cols, int rows); // clears; call from window_size
void scr_clear(); void scr_invalidate(); // force full repaint next frame
void scr_put(int x, int y, string text, mixed attr); // attr = SGR params ("1;36") or 0
void scr_fill(int x, int y, int w, int h, string ch, mixed attr);
void scr_hline/scr_vline(...); void scr_box(x, y, w, h, style, attr);
void scr_set_cursor(int x, int y, int visible);
string scr_frame(); // ANSI delta vs. last frame
Cells store ({ grapheme, sgr_params, width }); a wide char owns its left
cell and marks the right cell as a continuation. scr_frame() diffs against
the previously-emitted grid row by row, coalescing changed spans into
CUP + SGR + text runs while tracking the current SGR state to avoid
redundant attribute churn — the standard curses algorithm, minus insert/delete
-line optimizations that don't pay at MUD line rates.
widget.lpc is the inheritable widget base (geometry, focus, draw(screen),
handle_key(ev) → handled?, the on-event callback), and app.lpc is the
application container (screen ownership, Tab focus cycling, key/mouse
routing, Ctrl-C quit). The shipped widget set (each a clonable in w/,
inspired by the pterm and blessed catalogs):
| Widget | Behavior | Events |
|---|---|---|
label |
static (multi-line) text | — |
list |
scrollable single-select list; wheel scrolls | select, change |
table |
column-aligned rows + header, scroll + selection; click/wheel aware | select, change |
tree |
collapsible tree (▸/▾), ←/→/Enter fold, ← on a leaf jumps to its parent; wheel scrolls |
select, change |
textfield |
single-line input backed by a full readline engine | submit |
checklist |
[x] multi-select, Space toggles |
change |
radiolist |
(•) single-choice group |
change |
button |
[ OK ], Enter/Space presses |
press |
progress |
label + bar + percentage | — |
spinner |
braille spinner; app drives tick() from its own call_out |
— |
log |
bottom-anchored scrollback pane (PgUp/PgDn, wheel) | — |
chart |
braille line chart with rolling history (add_point()), multi-series |
— |
2.5 print.lpc — inline printers (stateless, inheritable)
The pterm side of the library: string builders that compose with plain
write() — no raw mode, no full screen — and double as rendering helpers
for widgets. p_table(rows, opts) (boxed, width-aware, header rule),
p_tree(nodes) (├──/└──), p_bars(items) (horizontal bar chart),
p_spark(values) (▁▂▃▅▇ sparkline), p_panel(title, body),
p_bullets(items) (depth-nested), p_header(text) (centered banner),
p_progress(pct, width), p_info/p_success/p_warn/p_error(text) prefix
printers, and p_bigtext(text) (banner letters via /std/bitmap_font).
All wide-char aware; tables/trees/panels accept the TUI_BOX_* styles.
Charts, on top of canvas.lpc — a braille dot canvas (each cell is a 2×4
dot grid, U+2800 block; the blessed-contrib/drawille technique) with
c_set/c_line/c_plot in dot coordinates and per-cell colour:
p_chart(series, opts)— braille line chart, multiple coloured series, y-axis gutter and legend (series=({ ({ label, values, attr }) })).p_vbars(items, opts)— vertical bar chart with eighth-block partial tops (▁▂▃▄▅▆▇█), optional value row.p_heatmap(grid, opts)— 2D matrix as 256-colour cells with a cool→hot ramp and optional axis labels.w/chart.lpc— the same line chart as a live widget:add_series(),add_point()(rolling window sized to the widget), auto- or fixed y-range; drives the dashboard's traffic graph.
2.6 menu.lpc — inline select/multiselect (clonable state machine)
pterm's interactive prompts, MUD-style: a prompt plus a scrolling choice
list rendered in the normal output flow and repainted in place with
relative cursor movement; on completion it collapses to a one-line
? prompt: answer record. Space toggles in multi mode, Enter accepts,
Esc/Ctrl-C aborts, long lists window with height (with … indicators
on both overflow edges). Typing printable text filters the choices live
(pterm types-to-filter; case-insensitive substring, Backspace edits the
query) — results always index the ORIGINAL choices array. Rendered lines
clip to the terminal width (m_set_width, fed by the glue and re-fed on
every NAWS resize via m_redraw()), so long items can never wrap and
desync the in-place repaint. Same engine API shape as readline:
m_begin/m_feed/m_take_output/m_result (+ m_filter/m_set_width/m_redraw).
2.7 terminal.lpc — the only impure module (inheritable)
Inherit into an interactive object (user/login) to run either mode:
- Prompt mode —
tui_readline(function done_cb, mapping opts): an enhancedinput_toreplacement: raw mode on, run the readline engine, restore line mode, calldone_cb(line_or_0, state). History is kept per object (optscan seed/limit it), giving every prompt in the mudlib line editing + ↑↓ history + C-r search with one call. - Inline prompts —
tui_select(cb, prompt, choices, opts)→cb(index, item, state);tui_multiselect(...)→cb(indexes, items, state);tui_confirm(cb, prompt, default)→cb(yes, state). All run the menu engine (or a one-keystroke y/n loop) over the same char-mode plumbing as readline. - App mode —
tui_open(object app)/tui_close(): alternate screen, cursor hidden, optional mouse on; forwards decoded events to the app (on_key,on_mouse,on_resize(w,h),on_open(term),on_close) and gives it ascreenclone to draw into.
Responsibilities: get_char(..., I_NOECHO) re-arm on every keystroke inside
the callback (so echo never flickers back on), NAWS/TTYPE request + caching
(window_size/terminal_type applies, forwarded as resize events), the
lone-ESC call_out_walltime(0.05) flush, and guaranteed teardown (restore
line mode + echo + main screen + cursor + mouse off) on close, user
disconnect, or object destruction. A safety net: if the driver reverts to
line mode behind our back, the next line of input is handled gracefully.
3. File layout
testsuite/include/tui.h — key codes, states, macros (public contract)
testsuite/std/tui/README.md — this document
testsuite/std/tui/ansi.lpc — layer 0: escapes + width toolkit
testsuite/std/tui/canvas.lpc — layer 0: braille dot canvas
testsuite/std/tui/print.lpc — layer 0: pterm-style inline printers + charts
testsuite/std/tui/keys.lpc — layer 1: input decoding
testsuite/std/tui/readline.lpc — layer 2: line editor
testsuite/std/tui/menu.lpc — layer 2: inline select/multiselect
testsuite/std/tui/screen.lpc — layer 2: cell grid + diff renderer
testsuite/std/tui/widget.lpc — layer 2: widget base (inheritable)
testsuite/std/tui/w/*.lpc — the widget set (see §2.4)
testsuite/std/tui/app.lpc — layer 2: application container
testsuite/std/tui/terminal.lpc — layer 3: interactive glue
testsuite/command/tuidemo.lpc — the showcases (see top of file)
testsuite/clone/tuidemo_*.lpc — full-screen showcase apps
testsuite/single/tests/std/tui/*.lpc — testsuite coverage
docs/stdlib/tui.md — user-facing documentation
4. Testing strategy
Everything below the glue is a pure state machine, so tests are ordinary non-interactive testsuite files:
tui_ansi.lpc— sequence builders;visible_width/width_sliceagainst ANSI-laden and wide-char strings.tui_keys.lpc— feed byte streams exactly asget_charwould deliver them (split across calls, batched, interleaved) and assert the event stream: arrows, modified CSI, SS3, Alt-x, paste, mouse, lone ESC viaflush(), UTF-8 text.tui_readline.lpc— script whole editing sessions as event arrays; assert buffer, cursor, state transitions, history navigation/search results, and selected render outputs (e.g. masked mode, horizontal scroll markers).tui_screen.lpc— draw, frame, assert emitted ANSI; second frame after a small change must touch only the damaged region; wide-char overwrite edge cases.tui/widgets.lpc+tui/widgets2.lpc— the whole widget set headless: navigation, events, scrolling, toggle state, and selected render output.tui/print.lpc— exact expected strings for the printers (box drawing, width math, scaling).tui/menu.lpc— scripted select/multiselect sessions: navigation, toggling, windowing, accept/abort, the collapsed answer line.tui/charts.lpc— canvas dot/braille math (exact glyphs), line drawing,c_plotendpoints, and exactp_vbarsoutput; chart/heatmap structure checks; the chart widget's rolling window.tui/fixes.lpc— pinned regressions:wslicezero-width handling,ESC[1;mRvs CPR decoding, readline dual scroll markers / undo / isearch-vs-mouse / completion, menu clipping + filter + indicators, widget wheel support, tree parent jump, focus cycling, and app teardown on a terminal-initiated close (mock terminal drives the app_quit ↔ tui_close reentry cycle).
Clones are destructed at test end (DEBUGMALLOC check_memory() runs after
every file). The interactive glue is kept too thin to need a terminal to
verify; the demo command is the manual test.
5. Conventions
- Public API prefixes per module (
rl_,scr_,tui_) — mudlibs mix these into objects that already have dozens of functions; no generic names. - Events, not callbacks-into-the-decoder: lower layers return data; only
terminal.lpccalls outward (into the app object). - No
input_to-style string callbacks — function pointers only. - 0-based x,y screen coordinates,
(0,0)top-left, x = column. - All modules compile warning-free.
- Control characters in the edit buffer (e.g. from a bracketed paste) are kept verbatim in the data but rendered as nothing (they are zero-width); the accepted line preserves them exactly.
- The screen's rows are copy-on-write against the last emitted frame, so a frame's cost is proportional to the rows actually touched, not W×H.
6. Driver fixes bundled with this library
Five driver quirks — found by building this library and verifying it over a
live telnet connection — made a clean LPC implementation impossible; all are
fixed alongside (documented in docs/efun/interactive/get_char.md; the
UTF-8 tail logic is pinned by GTest src/tests/test_strutils.cc):
- BS/DEL erasure (
comm.cc first_cmd_in_buf): char mode zeroed0x08/0x7fbefore delivery, so the callback received""for Backspace — BS, DEL and NUL were indistinguishable. Now the literal byte is delivered. (Line-mode in-buffer BS editing is unchanged.) - UTF-8 byte-splitting (
comm.cc cmd_in_buf/first_cmd_in_buf): char mode delivered one byte per callback, so a multi-byte character arrived as 2–4 invalid one-byte strings. Char-mode extraction is now UTF-8-aware: a complete multi-byte sequence is one callback carrying one valid character. Malformed bytes fall back to byte-at-a-time delivery (no stalls). - Inbound ESC stripping (
comm.cc on_user_input): withno ansi+strip before process input(both default on), inbound ESC was rewritten to a space — arrow keys arrived as literal[A. The rewrite is an anti-ANSI-injection protection for line-mode commands; char mode now always passes ESC through (the mudlib asked for raw keystrokes). - UTF-8 split across TCP segments (
net/telnet.cc on_telnet_data+u8_incomplete_tail()inbase/internal/strutils): each received chunk wasu8_sanitized independently, so a multi-byte character straddling a segment boundary became U+FFFD before reaching the input buffer — in any input mode. An incomplete trailing sequence is now carried over (interactive_t::u8_carry) and prepended to the next chunk. - NAWS lost at logon (
net/telnet.cc+comm.cc on_user_logon): fast clients answer the initialDO NAWSwhileip->obis still the master object, so thewindow_sizeapply fired on the wrong object and the size was unrecoverable (clients only re-send NAWS on resize). The last report is now cached ininteractive_tand the apply is replayed on the user object at logon.
The decoder additionally copes with the old ""-for-Backspace behavior, so
the library degrades gracefully on older drivers for everything except
non-ASCII input and escape sequences under default configs.
7. Deliberate scope cuts
- Multi-line soft-wrapped editing (readline renders one terminal row with horizontal scroll). The engine's render layer is isolated, so a wrap renderer can replace it without touching editing logic.
- Kill ring beyond a single buffer, vi keymap. (Single-level-stack undo — C-_ — IS implemented.)
- Grapheme-cluster cursor motion — cursor moves by codepoint; combining marks may take two arrow presses. (Display width is still correct.)
- terminfo — we emit the VT100/xterm common subset that every MUD client,
xterm.js, and modern terminal understands;
terminal_typeis cached and exposed so apps can special-case, but the library does not vary output. - Scroll-region / insert-line diff optimizations in the renderer.
- From the pterm/blessed catalogs, deliberately not ported: pterm's Area and theming, blessed's FileManager, Terminal, Image/Video and absolute-positioned Layout manager — MUD-side value didn't justify the surface area.