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
Rework of the mudlib JSON library using the driver's buffer features:
- json_decode() scans UTF-8 bytes with a single-pass, position-based
parser: strings without escapes and numbers decode via one buffer
range slice instead of byte-by-byte copies (LPC string indexing is
codepoint-based and O(i) per access, which made the old parser O(n^2)
on long inputs). Escaped strings copy plain runs in slices and route
all appends through one json_append(buffer ref, int ref, mixed)
helper built on to_buffer() promotion and range assignment.
- json_decode() also accepts a buffer directly, skipping the string
conversion round-trip.
- \uXXXX escapes decode via sprintf("%c") -> raw UTF-8 bytes, including
UTF-16 surrogate pairs (with validation of lone/misordered
surrogates); astral-plane characters encode as \uXXXX\uXXXX pairs
(they used to produce a corrupt 5-digit escape).
- json_encode_string() emits escape sequences as string literals
through the same append helper instead of hand-poked hex bytes.
The test suite grows from 44 to 599 lines / 178 checks: numbers,
strings, escapes, Unicode/surrogates, booleans/null, arrays, objects,
nesting, encoding for every type, non-string keys, circular reference
detection, round-trips, real-world JSON files, the buffer-input path,
buffer-growth regressions, and print-only performance benchmarks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Char-mode input: deliver real keystrokes (BS/DEL, whole UTF-8, raw ESC); fix NAWS lost at logon
Five input-path fixes that make raw-keystroke (get_char) applications
viable, found by building the LPC TUI library on top of them:
- comm.cc: char mode delivered "" for Backspace/Delete (the byte was
zeroed before delivery, making BS/DEL/NUL indistinguishable). The
literal byte is now delivered; line-mode in-buffer editing unchanged.
- comm.cc: char mode delivered one *byte* per callback, splitting a
multi-byte UTF-8 character into 2-4 invalid one-byte strings.
Extraction is now UTF-8 aware: a complete sequence arrives as one
callback carrying one valid character; malformed bytes still go
byte-at-a-time (no stalls).
- comm.cc: the "no ansi" + "strip before process input" ESC->space
rewrite (both default on) also applied to char mode, so 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.
- net/telnet.cc: each received chunk was u8_sanitize()d independently,
so a UTF-8 character split across TCP segments became U+FFFD in any
input mode. An incomplete trailing sequence (new u8_incomplete_tail(),
GTest-covered) is now carried over in interactive_t and prepended to
the next chunk.
- net/telnet.cc + comm.cc: fast clients answer the initial DO NAWS
while ip->ob is still the master object, so the window_size apply
fired on the wrong object and the size was lost until the next
resize. The last report is cached and replayed on the user object at
logon.
docs/efun/interactive/get_char.md documents the delivery contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpXv4yGCWpkzbkifyZy9EE
* Add /std/tui: an LPC TUI library (readline + ncurses for the prompt line and full-screen apps)
A terminal-UI toolkit in pure LPC, strictly layered (design + rationale
in testsuite/std/tui/DESIGN.md; user docs in docs/concepts/general/tui.md):
- ansi.lpc: escape builders plus the width toolkit the driver lacks
(visible_width/wslice/wpad are ANSI-blind and wide-char aware).
- keys.lpc: keystroke decoder state machine: the get_char byte stream ->
key events (CSI/SS3 with xterm modifiers, Alt prefixes, bracketed
paste as one event, SGR mouse, UTF-8, lone-ESC via caller-driven
flush()).
- readline.lpc: the line editor: emacs keymap (motion/kill/yank/
transpose), Up/Down history, C-r/C-s incremental search, Tab
completion, masked mode, horizontal scrolling with wide-char aware
viewport; repaints only its own line so it works at any scroll
position.
- screen.lpc: virtual cell grid + minimal-diff frame renderer with
copy-on-write rows (frame cost tracks touched rows, not W x H);
wide chars own two cells, boxes/fills/attrs as SGR param strings.
- widget.lpc + w/ (label, list, textfield) + app.lpc: widget protocol,
focus cycling, event routing; textfield embeds a readline engine.
- terminal.lpc: the one impure module, inherited by the user object:
get_char re-arm loop (I_NOECHO across re-arms), NAWS/TTYPE caching
via the window_size/terminal_type applies, lone-ESC walltime timeout,
guaranteed teardown. tui_readline() is a drop-in input_to()
replacement; tui_open() runs full-screen apps on the alt screen.
Everything below terminal.lpc is a pure state machine: 194 checks in
single/tests/std/tui/ cover the decoder, editor sessions, history
search, screen diffing and widgets headlessly. `tuidemo` /
`tuidemo app` (command/tuidemo.lpc) demo both modes over any telnet
client; the whole stack was verified end-to-end over a live connection
including split-packet UTF-8, modifier keys, paste, resize and
alternate-screen teardown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpXv4yGCWpkzbkifyZy9EE
* /std/tui v2: pterm/blessed-inspired widgets, inline printers and prompts, showcases
Reviewed the pterm (Go) and blessed widget catalogs and ported what earns
its keep in a MUD; DESIGN.md is renamed to README.md and documents the
full set (including what was deliberately not ported).
New layers:
- print.lpc — pterm-style printers that compose with plain write():
p_table (boxed, width-aware, header rule), p_tree, p_bars, p_spark,
p_panel, p_bullets, p_header, p_progress, p_info/success/warn/error,
and p_bigtext via the existing /std/bitmap_font.
- menu.lpc + terminal glue — inline interactive prompts in the normal
output flow: tui_select(), tui_multiselect() (Space toggles, list
windows by height, collapses to a "? prompt: answer" line) and
tui_confirm() (y/n with a default).
New widgets (/std/tui/w/): table (columns + header + selection), tree
(collapsible, arrows fold), checklist, radiolist, button, progress,
spinner (app-driven tick()), and log (bottom-anchored scrollback pane).
Showcases (command/tuidemo.lpc): `tuidemo select` (prompt chain),
`tuidemo print` (all printers), `tuidemo dashboard` (animated spinner,
progress bars, sparkline, live table and log on a call_out tick), and
`tuidemo form` (textfield, radio group, checkboxes, buttons).
Coverage: three new test files (print exact-output, menu sessions,
widgets2) bring /std/tui to 8 files in the suite; the live-connection
e2e run now drives all six showcases end-to-end (52 checks), including
the select->multiselect->confirm chain, unattended dashboard animation,
and full form entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpXv4yGCWpkzbkifyZy9EE
* /std/tui: charts — braille canvas, line charts, vertical bars, heatmap
Fills the chart gap left by v2 (only the horizontal p_bars existed):
- canvas.lpc: a braille dot canvas (each cell is a 2x4 dot grid from the
U+2800 block — the blessed-contrib/drawille technique), giving sub-cell
resolution: c_set/c_unset/c_get, Bresenham c_line, and c_plot (scale a
value series across the canvas), with per-cell colour. Renders as a
string, per-row strings, or per-cell ({ ch, attr }) pairs for blitting
into a screen.
- print.lpc: p_chart (multi-series braille line chart with y-axis gutter
and coloured legend), p_vbars (vertical bar chart with eighth-block
partial tops and optional value row), p_heatmap (2D matrix as
256-colour cells, cool-to-hot ramp, optional axis labels).
- w/chart.lpc: the live line-chart widget — add_series()/add_point()
rolling history sized to the widget, auto or fixed y-range; replaces
the dashboard's sparkline label with a real animating graph.
- `tuidemo charts` showcase; chart docs in README.md and the docs page
(heatmap removed from the not-ported list).
Tests: single/tests/std/tui/charts.lpc pins the braille bit math with
exact glyphs, line drawing, plot endpoints, exact p_vbars output,
chart/heatmap structure, and the widget's rolling window. The live e2e
run verifies `tuidemo charts` output and braille frames streaming from
the dashboard chart.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpXv4yGCWpkzbkifyZy9EE
* docs: move the TUI page from Concepts to STDLIB
/std/tui is a mudlib library like base64/break_string/json, so its page
belongs under docs/stdlib/ with the rest of the /std modules, not under
Concepts. Registered in stdlib/index.md; README file-layout pointer
updated. (Concepts' sidebar is autogenerated, so the old entry disappears
with the file.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpXv4yGCWpkzbkifyZy9EE
---------
Co-authored-by: Claude <noreply@anthropic.com>
A pure-LPC decimal library (integer math, no driver/VM change and no
external dependency). A decimal is a 2-element array ({ mant, scale })
with value mant / 10^scale, so arithmetic is exact -- 0.1 + 0.2 == 0.3 --
which binary floats can't do. Backed by 64-bit LPC ints (~18 significant
digits); mantissa overflow is detected and raised, not silently wrapped.
API (simul-efuns via inherit "std/decimal"): to_decimal(string|int|
float|decimal), decimal_add/sub/mul/div/mod/neg, decimal_cmp/eq/lt/gt,
decimal_to_string/to_int/to_float, decimalp. Decimals are immutable
(every op returns a fresh array). An array rather than a `class` on
purpose: a simul_efun class would leak into every object's global
classes()/num_classes().
Pinned by /single/tests/std/decimal.lpc (33 checks: construction,
exact add/sub, scale-aligned compare, mul/div with trailing-zero trim,
repeating-division truncation, mod, negation, conversions, float
construction, overflow + bad-input errors).
Verified: testsuite x3 (ASan Debug) + ctest 297, RelWithDebInfo 298.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Source resolution is now explicit-extension-exact: load_object("/foo.c")
probes only foo.c, never foo.lpc (and vice versa); extension-less names
prefer .lpc and fall back to .c. Object identity stays extension-blind:
object names carry no extension, any spelling finds a loaded object, and
the registry is consulted before the filesystem. The caller's raw
spelling now flows through find_object()/inherit/master/simul_efun loads
instead of being pre-stripped away.
- filename_to_obname and otable basename() strip .lpc too (children())
- save_object() strips either source extension before appending .o
- replace_program()/function_exists() handle both suffixes
- testsuite: all LPC sources renamed to .lpc; runner globs, master
get_include_path cases, and program-name assertions updated;
README.md rewritten with the extension rules and suite conventions
- new single/tests/efuns/dual_extension.lpc pins exact-pick, fallback,
no-crossover, identity, and registry-before-filesystem with .c/.lpc
fixture pairs in /clone
Verified: ctest 297/297 and driver-autotest x3 (ASan Debug) plus
RelWithDebInfo ctest + autotest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>