Map: repl / frontend
ral/src/repl/frontend.rs declares the Frontend trait — the seam between the
session loop and the line editor. One method, read, returns
a typed Read event (Line / Edit / Interrupt / Eof); the session never
sees keybindings, escape sequences, or the buffer stack. The frontend owns
plugin sync, keybinding dispatch, continuation reads, and flushing deferred
diagnostics before returning. EditBuffer carries a re-feed buffer whose
cursor is a character offset (matching the plugin surface — see
plugins); the rustyline boundary converts to bytes.
Both backends drive the multi-line read through frontend::join_continuation,
which folds backend-supplied lines into the buffer until it parses complete
(or is abandoned by a Continuation::Discard). The loop, the newline joining,
and the abandon semantics live there once, so Ctrl-C / Ctrl-D / a read error
behave identically — abandoning the partial buffer keeps the session alive
rather than ending it (the prior per-frontend copies had drifted, letting
Ctrl-D kill the shell from the rustyline path). History saves append the
session’s own entries rather than rewriting the file, so concurrent sessions do
not clobber each other.
Three implementations live in frontend/, selected by Surface
(--surface flag or rc surface:; the capability gate forces minimal on
dumb terminals whatever was asked):
minimal.rs::MinimalFrontend— canonical-stdin fallback for dumb terminals andRAL_INTERACTIVE_MODE=minimal. No raw mode, no DECSET, no plugin features; justread_linewith a>continuation prompt.rustyline.rs::RustylineFrontend— the default editor: completion, plugin keybindings, ghost text, highlights, and rustyline history, on TTYs that support raw mode and ANSI. Its constructor wires the shell’s stdout onto rustyline’sExternalPrinter, so backgroundwatchoutput lands above the live prompt.structural.rs::StructuralFrontend— the ratatui inline-viewport projection surface (structuralfeature,--surface structural): the typed spine, worksheet, and handles matrix around the prompt, plus Tab completion (below). See repl-as-structural-surface. It drives the same in-editor plugin surface the rustyline backend does, off the shared `PluginRuntime` rather than a parallel copy: each iteration runsrun_buffer_change_hooksand reads back the fish-style ghost suggestion and highlight spans (overlaid as ratatui cells viahighlight_style::style_ratatui), accepts the ghost on right-arrow at buffer end, and resolves each keypress through a snapshot of the shared `KeyRouter` (Resolution::Defaultfalls into the surface’s own built-in key arms). A claimed binding breaks the loop to aComposed::Keybindingoutcome — no newReadvariant — tears down the viewport and raw mode, then runsdispatch_keybinding, so an_ed-tuihandler (fzf, zoxide) gets the terminal exactly as the rustyline path dispatches only after leavingreadline; an_ed-pushbuffer is popped when nothing is pending.
The structural surface and exarch TUI share their prompt editor through
the prompt-editor crate (backed by edtui 0.11): the PromptEditor facade
owns all cursor positioning, key dispatch (Emacs and Vim via edtui
EditorEventHandler), height hinting, and highlight rendering. Both
frontends show the terminal native cursor in every mode, the same shape
throughout (no painted vi modal-mode block). One implementation, not a copy each.
The structural surface handles shell-line chords before the editor:
Ctrl-U kills to line-start (readline unix-line-discard, not edtui
undo), in emacs and in vi-Insert mode. vi Normal/Visual keep the edtui keymap.
Ctrl-D on a non-empty buffer deletes the char under the cursor;
only an empty buffer reads as Eof.
Completion
The completion engine is frontend-neutral: completion.rs classifies the
token under the cursor ($-variable / command-position name / path), gathers
candidates from a Sources snapshot of the live shell (PATH commands +
builtins + handlers + bindings; cwd-anchored path entries), and ranks them.
completion::complete(line, pos, &Sources) -> (replace_from, Vec<Candidate>)
is the single entry point both surfaces call. Ranking is nucleo fuzzy
matching for every surface — path-tuned for path entries, ties broken
alphabetically — and lives in ral_core::text::rank, a generic AsRef<str>
function with no UI in it, so exarch’s pickers match the same way ral’s menus
do. A surface whose rows are not their own haystack — exarch’s /model picker,
matching an (account, model) pair by the label / model line it draws —
calls ral_core::text::rank_by, which takes the haystack as a projection out
of the item; rank is that same function under AsRef::as_ref. Ranking a row
together with its haystack is what keeps the two from drifting apart into
parallel vectors. Candidate is the engine’s own type and stays here.
complete.rs::RalHelperis the rustyline adapter: it holds theSourcessnapshot (rebuilt each prompt viarefresh), delegatesCompleter::completeto the engine, and maps eachCandidateto a rustylinePair. It also implementsHinter/Highlighter— ghost text and syntax highlights are not computed here; they come from pluginbuffer-changehooks recorded in the `PluginRuntime`, andRalHelperonly paints what the runtime last produced.highlight_style.rs::STYLESis the one data-driven table of legal highlight styles — each row gives the name, its ANSI escape (style_ansi, which_ed-highlightvalidates against), and its ratatui cell style (style_ratatui) — so the two surfaces cannot drift.structural.rsdrives the engine as a drop-down menu band: Tab completes the token under the cursor — a unique match is spliced in place, several open a bordered popup rendered over the top of the projection band, anchored under the token. Tab/↓ and ⇧Tab/↑ cycle the selection, Enter accepts, Esc (or any editing key) dismisses. The lower band reserves room for the menu so a fresh session still has space to drop it down. The popup itself isprompt_editor::completion::Menu, shared with exarch and painted in the host’s palette; it takes its ownCandidate(display,detail,replacement), which the engine’s candidates convert into at the one call toMenu::open— always withdetail: None, since a filename or a binding name is its own whole story and carries no gloss. exarch’s slash commands are the one caller that fillsdetail, from the registry’s ownhelpline; see frontend.