Skip to content

ADR-0053: REPL Quake-style drop-down overlay (revive the §3F toast)

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.

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’s overlay-open! (arm the slide-down), pause the underlying tick. On the second TERM: drive overlay-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-exact render_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.

  • 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.
  • 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_restore are 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 uses screen_router_overlay_render. The host gates which on screen_router_overlay_active.
  • Host CMake: sys_render.c now references composite.c; both hosts already link it (no host CMakeLists touch needed).
  • render.c/render.h: render_snapshot / render_restore / render_snapshot_valid; render_init invalidates. (Tests in test_render.c.)
  • sys_render.c: render/snapshot / render/restore / render/snapshot-valid? / render/scrim / render/shadow bound System-tier; composite.c joins every target that links sys_render.c. (Tests in test_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) + the overlay-open!/close!/closed?/reset! Lisp hook protocol. (Tests in test_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.c re-baselined to the band geometry.)
  • hosts/emulator/src/main.c: TERM drives the overlay; render_frame drives overlay_render; the underlying tick is paused while down; text input starts on open / stops on finalize. (kn86emu --sys-screen-smoke OK.)
  • kn86rec --overlay record mode + record-demo.sh --overlay; repl-quake.recrepl-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 libnosh when the nosh_host_step loop-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).