Repository navigation
Closed
Labels
ffiForeign-function interface boundaries and interopForeign-function interface boundaries and interoprefactorInternal restructuring without an intended feature changeInternal restructuring without an intended feature changespikeTime-boxed investigation needed before implementationTime-boxed investigation needed before implementation
Description
Context
The TUI editor owns terminal-capability and raw-input FFI in define_ffi.mjs.
Current boundary:
src/glendix/define_ffi.mjsis_tty,exit_process,terminal_size(fromprocess.stdout.columns/rows)set_terminal_raw_mode,terminal_mode_error_messagepoll_key_raw+ stdin lifecycle and key decoding (ensureStdin,onStdinData,parseKeyBuf)
src/glendix/define.gleam- externals
is_tty,exit_process,terminal_size,poll_key_raw,set_terminal_raw_mode,terminal_mode_error_message
- externals
Proposed package/API (spike)
- Evaluate a terminal package (e.g.
term_size) to replace theterminal_sizeFFI. - Assess whether any package can safely provide raw-mode toggling and key polling; if not, keep raw terminal input/lifecycle as intentionally retained custom FFI.
- Goal is to minimize custom terminal FFI to only what has no reliable ecosystem equivalent, and clearly document the residue.
Compatibility and public API impact
- Preserve terminal size fallback (80x24 when columns/rows are unavailable).
- Preserve raw-mode error handling as a
Resultwith a message, including the "stdin does not support raw mode" case. - Preserve key decoding semantics (arrow/nav keys, Enter, Backspace, Ctrl+C, Tab, UTF-8 multibyte) and one-shot poll-with-timeout behavior.
Non-goals
- Rewriting the TUI event loop or key model.
- Adopting a package that cannot preserve non-blocking one-shot key polling.
Acceptance criteria
- Spike records which terminal functions have viable ecosystem replacements and which must remain custom, with rationale.
-
terminal_size(and any other replaceable function) delegates to the chosen package; retained raw input/lifecycle FFI is documented as intentional. - Size fallback, raw-mode error path, and key decoding behavior are preserved.
- Tests/contract checks cover the retained and replaced boundaries.
- FFI contract checks report no stale external declarations or exports.
-
glendix -> mendrawretains its currently declared dependency source form.
Verification
- Local implementation loop:
./scripts/verify.sh inner glendix - If public signatures change:
./scripts/verify.sh shared glendix - Before a release or family-wide compatibility claim:
./scripts/verify.sh final
Metadata
Metadata
Assignees
Labels
ffiForeign-function interface boundaries and interopForeign-function interface boundaries and interoprefactorInternal restructuring without an intended feature changeInternal restructuring without an intended feature changespikeTime-boxed investigation needed before implementationTime-boxed investigation needed before implementation
Activity
GG-O-BP commentedon Sep 7, 2026
Analysis of #18 — terminal FFI reduction spike
I worked through the current boundary and enumerated the behaviors/edge cases the issue implies, then implemented a candidate on a dedicated branch/worktree.
Current boundary
The definition TUI owned six terminal externals in
define.gleam, all backed by a singlesrc/glendix/define_ffi.mjs:is_tty,exit_process,terminal_size,set_terminal_raw_mode,terminal_mode_error_message, andpoll_key_raw(plus the stdin lifecycle andparseKeyBufkey decoding).Enumerated behaviors / edge cases to preserve
#(columns, rows)tuple ordering; fallback to 80x24 when a dimension is unavailable — including the non-positive/0case that the oldcolumns || 80guard swallowed.Result; the"stdin does not support raw mode"message whensetRawModeis missing; resumes stdin on enable; converts thrown exceptions into a descriptive error (no emptycatch).KeyNone.[0, ""]; queue buffering when input arrives before a waiter.@externalkeeps a matching export (no stale declarations/exports);glendix -> mendrawsource form unchanged.Ecosystem evaluation (spike outcome)
terminal_size→ replaced by theterm_sizepackage (columns()/rows()returningResult), keeping the 80x24 fallback.etch's terminal module only emits ANSI strings and cannot query size.is_tty,exit_process,set_terminal_raw_mode,terminal_mode_error_message,poll_key_raw→ retained as intentional custom FFI (runtime capability/lifecycle and non-blocking one-shot raw input have no reliable ecosystem equivalent). Adopting a package that breaks non-blocking one-shot polling is an explicit non-goal, so the key model/event loop are untouched.Candidate implemented
glendix/define/terminal_control(+terminal_control_ffi.mjs); removeddefine_ffi.mjs.size()delegates toterm_size; retained externals documented as intentional; addedterminal-ffi-spike.mdrecording the evaluation.Verification (candidate branch)
gleam format --check,gleam check,gleam build --warnings-as-errors,gleam docs build,gleam test(91 passed), FFI-contract check, style check, anddependency-mode check-commit glendixall pass.Candidate branch:
refactor/term-size-terminal-ffi-issue-18.refactor(define): minimize custom terminal FFI
GG-O-BP commentedon Sep 7, 2026
Resolved via #33 (merged to
main)Final implementation notes (superseding earlier details in this thread):
term_size.get(), converting the package's#(rows, columns)result into the TUI's#(columns, rows)contract, with the80x24fallback preserved (including independent fallback for non-positivedimensions).
plinth/node/process.exitdependency; the custom
exit_processFFI was removed.glendix/internal/define/terminal_control(not a public API), replacingsrc/glendix/define_ffi.mjs.failure messages ("stdin does not support raw mode"), stdin lifecycle,
non-blocking one-shot key polling with timeout, and UTF-8 aware key decoding.
Verification:
innerandsharedtiers passed, and the mandatory childfinaltier passed end to end via the pre-push hook (family gates, allstandalone consumers, widget
.mpkbuild, and both Lustre and Mendix browsersmoke checks). Glendix test suite green (136 passed on the merged base, which
includes the async-init tests already on
main). Theglendix -> mendrawdependency source form is unchanged.