ADR-0053: REPL Quake-style drop-down overlay (revive the §3F toast)
Context
Section titled “Context”The onboard Lisp REPL was a full-screen system screen. TERM did an instant screen_router_activate("repl") swap and repl/render called (render/clear), wiping the framebuffer — whatever was underneath (the bare deck or a system screen) was gone, and re-appeared only on exit by re-rendering. The byte-exact framebuffer snapshot/restore that would let the REPL drop over a live screen had been implemented for the GWP-120 “toast” and then deleted (GWP-514, when the render path moved to the Fe screen) — only the ReplToastPhase state machine + commit/persist wiring survived in repl_toast.c.
input-dispatch.md §3F (GWP-230) already specified the equake model — “toggle the REPL on the main grid, slide-down animation, pause the underlying tick while visible, restore the screen byte-exact on close” — but the build diverged to the full-screen screen. The forcing function: the REPL is a release-default, first-class utility (ADR-0002 §1), and an instant full-screen teleport is a worse interaction than the drop-down every operator already knows from equake / Yakuake / Guake / iTerm — drop a console over your work, glance, dismiss, your work is exactly where you left it.
Decision
Section titled “Decision”Realize the §3F toast as an equake-style drop-down: a partial REPL console that drops over a frozen, pixel-preserved screen. Four parts.
1. A byte-exact surface snapshot seam in the renderer (render.c/render.h).
render_snapshot()— copy the whole 1024×600 RGB565 surface into a static off-screen buffer (~1.2 MB; no malloc, trivial against the Pi Zero 2 W’s 512 MB) and mark it valid.render_restore()— lay the held snapshot back down byte-exact (a no-op without a valid snapshot, so a stray restore can’t zero a live screen). Repeatable per animation frame.render_snapshot_valid()— capture state.render_init()invalidates it (a fresh boot freezes nothing).
This revives the byte-exact preservation deleted from repl_toast.c, at the surface level rather than a text-buffer slice.
2. An overlay mode in the screen router (screen_router.c), beside the single-active full-screen swap.
screen_router_overlay_toggle(rt, "repl")— on open:render_snapshot()(freeze the base), mark the overlay active, drive the screen’soverlay-open!(arm the slide-down), pause the underlying tick. On the second TERM: driveoverlay-close!(begin the slide-up) — the router does not clear the overlay yet; the slide must play out.screen_router_overlay_render(rt)— per frame:render_restore()the frozen base, drive the overlay screen’s render (it scrims the revealed band, paints the console band at the current slide offset, drops the edge shadow); when the screen reports the slide-up finished, finalize —overlay-reset!, a final byte-exactrender_restore(), clear the overlay, resume the tick.screen_router_overlay_active/screen_router_overlay_blocks_tick— tick-pause gating for the host (the frozen base must not advance).
3. The REPL Fe screen (repl/repl.lsp) becomes the drop-down band. It owns its own slide animation on the GWP-644 idle-timer (run-with-timer, lazily armed like FIREPLACE / animlab): a fixed ~40% (30-cell / 240 px) console that slides down/up by a position interpolation (*repl-overlay-offset*, *repl-overlay-phase* ∈ opening/open/closing/closed), scrims the revealed frozen base (render/scrim), paints the console backdrop + Row-0 modeline + scrollback + prompt pinned to the band edge, and drops the edge shadow (render/shadow). The prompt cursor blinks on the same timer. The session stays warm across toggles (composer + history untouched — hide, don’t destroy).
4. Expose the depth cues + snapshot seam as System-tier FFI (sys_render.c): render/snapshot, render/restore, render/snapshot-valid?, render/scrim, render/shadow — System-tier only, like the rest of render/* (a cart context raises unbound-symbol; capability is the binding-set). scrim/shadow wrap the existing composite_scrim/composite_shadow (ADR-0036 §8.1).
Coverage + reach are settled (not re-litigated): fixed ~40% height, not operator-configurable, not full-screen. The drop-down opens over the bare deck and system screens — the surfaces where the REPL is reachable today. A running cart keeps the TERM key; nEmacs keeps TERM as its own exit. The §3C.1 TERM context-sensitivity table is unchanged — only the priority-5 binding is now the drop-down.
Options considered
Section titled “Options considered”- Approach 1 — snapshot + restore (chosen). On TERM, snapshot the surface; each frame restore it, scrim the revealed band, paint the console on top; on close, restore byte-exact and resume the tick. The cleanest realization of byte-exact preservation; the base is frozen genuinely (its tick is paused), so nothing flickers underneath.
- Approach 2 — re-render the underlying screen each frame (rejected). Saves the 1.2 MB buffer but assumes every screen is idempotently re-renderable while paused; a cart/animated frame can flicker, and a paused screen re-rendering is a contradiction. The buffer is cheap insurance.
- Approach 3 — pure-Lisp band paint with no clear (rejected). Cheapest, but no byte-exact restore — REPL bleed on close, fragile against any screen that doesn’t fully repaint its rows.
- Full-screen swap (the status quo, rejected). What we had: instant teleport, no preservation cost but a worse interaction and the deleted-preservation regression standing.
Consequences
Section titled “Consequences”- The REPL is now a glance-and-dismiss surface. Drop it over the deck/board/SYS, run an expression, dismiss — the underlying screen returns byte-exact, exactly like equake. The first-class onboard utility (ADR-0002 §1) gets a first-class interaction.
- Byte-exact preservation is back, at the surface level.
render_snapshot/render_restoreare a general seam any future overlay (a future modal, a peek) can reuse — not REPL-specific. - The slide reuses the animation substrate. No new ticking mechanism; the slide + cursor blink ride
run-with-timer(GWP-644), pumped by the host loop / the recorder’s virtual clock, lazily armed and substrate-guarded (a timer-less host snaps the console open and still works). - Small static cost. One extra 1.2 MB static RGB565 snapshot buffer beside the surface; negligible on 512 MB.
- Row 0 / Row 74 ownership intact. The console carries its own Row-0 modeline; the frozen firmware action bar shows through dimmed below. CIPHER stays OLED-exclusive — the REPL is main-grid (ADR-0015).
- One render path per surface kind. A plain active screen uses
screen_router_render; the REPL overlay usesscreen_router_overlay_render. The host gates which onscreen_router_overlay_active. - Host CMake:
sys_render.cnow referencescomposite.c; both hosts already link it (no host CMakeLists touch needed).
Action items
Section titled “Action items”-
render.c/render.h:render_snapshot/render_restore/render_snapshot_valid;render_initinvalidates. (Tests intest_render.c.) -
sys_render.c:render/snapshot/render/restore/render/snapshot-valid?/render/scrim/render/shadowbound System-tier;composite.cjoins every target that linkssys_render.c. (Tests intest_sys_render.c, incl. the cart-context capability split.) -
screen_router.c/.h: the overlay mode (overlay_toggle/overlay_render/overlay_active/overlay_blocks_tick) + theoverlay-open!/close!/closed?/reset!Lisp hook protocol. (Tests intest_screen_router.c.) -
system-image/lib/repl/repl.lsp: the drop-down band — slide state machine on the idle-timer, scrim + console + shadow, warm session. (test_repl_screen.cre-baselined to the band geometry.) -
hosts/emulator/src/main.c: TERM drives the overlay;render_framedrivesoverlay_render; the underlying tick is paused while down; text input starts on open / stops on finalize. (kn86emu --sys-screen-smokeOK.) -
kn86rec --overlayrecord mode +record-demo.sh --overlay;repl-quake.rec→repl-quake.gif. - Docs:
software/programs/repl.md§2/§3/§6 +software/runtime/input-dispatch.md§3C.1/§3F. - (Future) Lift the host overlay wiring into
libnoshwhen thenosh_host_steploop-lift lands (ADR-0040 §5.4), so the device host shares it without duplicating main.c. - (Future) Generalize the overlay seam to other drop-down surfaces if a second consumer appears (rule of three).