Skip to content

[ffi] Evaluate terminal packages and minimize raw terminal FFI #18

Closed

Description

@GG-O-BP

Context

The TUI editor owns terminal-capability and raw-input FFI in define_ffi.mjs.

Current boundary:

  • src/glendix/define_ffi.mjs
    • is_tty, exit_process, terminal_size (from process.stdout.columns/rows)
    • set_terminal_raw_mode, terminal_mode_error_message
    • poll_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

Proposed package/API (spike)

  • Evaluate a terminal package (e.g. term_size) to replace the terminal_size FFI.
  • 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 Result with 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 -> mendraw retains 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

Activity

  1. added
    ffiForeign-function interface boundaries and interop
    refactorInternal restructuring without an intended feature change
    spikeTime-boxed investigation needed before implementation
    on Sep 7, 2026
  2. GG-O-BP commented on Sep 7, 2026

    @GG-O-BP
    CollaboratorAuthor

    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 single src/glendix/define_ffi.mjs: is_tty, exit_process, terminal_size, set_terminal_raw_mode, terminal_mode_error_message, and poll_key_raw (plus the stdin lifecycle and parseKeyBuf key decoding).

    Enumerated behaviors / edge cases to preserve

    • Size: #(columns, rows) tuple ordering; fallback to 80x24 when a dimension is unavailable — including the non-positive/0 case that the old columns || 80 guard swallowed.
    • Raw mode: returns a Result; the "stdin does not support raw mode" message when setRawMode is missing; resumes stdin on enable; converts thrown exceptions into a descriptive error (no empty catch).
    • Key decoding: arrows, Home/End, PageUp/PageDown, Escape, Enter, Backspace, Ctrl+C, Tab, and UTF‑8 multibyte (2/3/4‑byte) sequences; empty buffer → KeyNone.
    • Polling: non-blocking one-shot with timeout → [0, ""]; queue buffering when input arrives before a waiter.
    • Contract/policy: every @external keeps a matching export (no stale declarations/exports); glendix -> mendraw source form unchanged.

    Ecosystem evaluation (spike outcome)

    • terminal_size → replaced by the term_size package (columns()/rows() returning Result), 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

    • Extracted a dedicated boundary module glendix/define/terminal_control (+ terminal_control_ffi.mjs); removed define_ffi.mjs.
    • size() delegates to term_size; retained externals documented as intentional; added terminal-ffi-spike.md recording the evaluation.
    • Added boundary tests (size fallback incl. zero/negative/error, size positivity, TTY probe).

    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, and dependency-mode check-commit glendix all pass.

    Candidate branch: refactor/term-size-terminal-ffi-issue-18.

  3. GG-O-BP commented on Sep 7, 2026

    @GG-O-BP
    CollaboratorAuthor

    Resolved via #33 (merged to main)

    Final implementation notes (superseding earlier details in this thread):

    • Terminal size delegates to term_size.get(), converting the package's
      #(rows, columns) result into the TUI's #(columns, rows) contract, with the
      80x24 fallback preserved (including independent fallback for non-positive
      dimensions).
    • Process exit delegates to the existing plinth/node/process.exit
      dependency; the custom exit_process FFI was removed.
    • The boundary moved into the internal module
      glendix/internal/define/terminal_control (not a public API), replacing
      src/glendix/define_ffi.mjs.
    • Intentionally retained custom FFI: TTY probe, raw-mode toggling with exact
      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: inner and shared tiers passed, and the mandatory child
    final tier passed end to end via the pre-push hook (family gates, all
    standalone consumers, widget .mpk build, and both Lustre and Mendix browser
    smoke checks). Glendix test suite green (136 passed on the merged base, which
    includes the async-init tests already on main). The glendix -> mendraw
    dependency source form is unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ffiForeign-function interface boundaries and interoprefactorInternal restructuring without an intended feature changespikeTime-boxed investigation needed before implementation

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions