In-World Fennel Interpreter Dialog: Implementation Plan
Objective
- Add an interactive Fennel interpreter widget/dialog that runs code inside the app process (same runtime context as remote control).
- Open this dialog from
normal-statewhen the backquote key is pressed. - Reuse existing UI/layout/state patterns (
DefaultDialog,Scene.add-panel-child,Input,ListView, focus/input routing).
Current Architecture Findings
assets/lua/normal-state.fnlowns non-text-mode key handling and is the right place to add a new global shortcut in normal mode.assets/lua/scene.fnladd-panel-child/remove-panel-childalready manage dialog lifecycle and cleanup viaon-close.- Dialog composition should follow
DefaultDialog+ content widget builder (seeassets/lua/launchables-helpers.fnlterminal dialog). - Input routing uses
assets/lua/input-state-router.fnlwith a single active input and state switching (normal/text/insert). - Existing Fennel eval path is in
assets/lua/remote-control.fnlusing:pcall fennel.eval source {:env _G}fennel.viewformatting for non-string values.
Recommended Design
1) New reusable evaluator utility
- Add
assets/lua/fennel-evaluator.fnlto centralize:eval-source(source)->{ :ok bool :result any }format-result(value)(nil/string/other viafennel.view)format-error(value)(stringify safe)
- Refactor
assets/lua/remote-control.fnlto consume this module instead of local duplicated helpers. - Benefit: identical semantics between remote-control and in-world interpreter, lower drift risk.
2) New interpreter view widget
- Add
assets/lua/fennel-interpreter-view.fnl. - Compose with existing widgets:
- Top: multiline
Inputfor source entry (e.g. 6-10 lines visible). - Middle/Bottom: output/history area via
ListView(orTextbuffer if list complexity is unnecessary). - Action row:
Run,Clear Output, maybeClear Input.
- Top: multiline
- Behavior:
- On
Run, evaluate current input with evaluator module. - Append transcript entries (
> code,< resultor! error) to output list. - Keep history bounded (configurable max entries, e.g. 200) to avoid unbounded memory growth.
- Keep focus on input after run for iterative use.
- On
- Runtime context:
- Execute with
{:env _G}to match remote-control behavior and support direct app inspection/manipulation.
- Execute with
3) New dialog builder helper
- Extend
assets/lua/launchables-helpers.fnlwithmake-fennel-interpreter-dialog. - Pattern should mirror
make-terminal-dialog:DefaultDialogtitle:"Fennel Interpreter".- Wrap child in
PaddingandSizedfor stable in-world dimensions. - Provide stable
:name/:focus-namefor focus and tests.
4) Scene-level singleton management
- Add a small helper in
normal-stateto open/toggle a single instance:- If already open, close/remove it.
- If closed, create via
scene:add-panel-child.
- Store pointer in app state, e.g.
app.fennel-interpreter-dialog. - Ensure pointer is nulled on dialog close (through
builder-options.on-close) and when removed externally. - This avoids stacking multiple interpreter dialogs from repeated key presses.
5) Keybinding in normal-state
- In
assets/lua/normal-state.fnl, add backquote key branch before fallback tobase-on-key-down. - Handle only in
normal-stateas requested. - Guardrails:
- If an active input is currently attached, do not force-open (avoid surprising interruption of text editing).
- Return
trueonly when shortcut is handled.
6) Optional launcher/control-panel integration (follow-up)
- Optional: add launchable entry
assets/lua/launchables/fennel-interpreter.fnl. - Optional: add HUD control-panel button/icon.
- Not required for this request (shortcut-triggered dialog), but low-cost discoverability improvement.
Test Plan
Unit tests
- Update
assets/lua/tests/test-states.fnl:- New test: backquote in normal-state opens interpreter dialog.
- New test: second backquote closes existing interpreter dialog (if toggled behavior chosen).
- Ensure key is swallowed when handled (does not reach first-person controls).
- Add
assets/lua/tests/test-fennel-interpreter-view.fnl:- Evaluates expression and appends formatted result.
- Errors are captured and shown in output.
- Output clear action works.
- History cap enforcement works.
- Update
assets/lua/tests/test-remote-control.fnlonly if evaluator refactor changes interface expectations. - Register new test module in
assets/lua/tests/fast.fnl.
Manual verification
- Run:
SKIP_KEYRING_TESTS=1 XDG_DATA_HOME=/tmp/space/tests/xdg-data SPACE_DISABLE_AUDIO=1 SPACE_ASSETS_PATH=$(pwd)/assets make test
- In app:
- Press backquote in normal-state -> dialog appears in scene.
- Execute sample code:
(+ 1 2 3)app.scene(error "boom")
- Confirm results/errors render, focus remains usable, and close action fully cleans up.
Implementation Sequence
- Add
fennel-evaluator.fnl. - Refactor
remote-control.fnlto use evaluator. - Implement
fennel-interpreter-view.fnl. - Add dialog helper in
launchables-helpers.fnl. - Wire backquote handling + dialog lifecycle in
normal-state.fnl. - Add/adjust tests (
test-fennel-interpreter-view.fnl,test-states.fnl,fast.fnlregistration). - Run test suite and fix regressions.
Risks and Mitigations
- Risk: keycode mismatch for backquote across SDL/event payload.
- Mitigation: confirm actual key value in this codebase before final binding.
- Risk: eval side effects destabilize app state.
- Mitigation: document that interpreter executes in
_Gintentionally; keep explicit transcript and error reporting.
- Mitigation: document that interpreter executes in
- Risk: focus/input-state interactions become sticky after close.
- Mitigation: ensure
dropdisconnects input/focus listeners andon-closeclears app pointer.
- Mitigation: ensure
Open Questions (need confirmation before implementation)
- Backquote behavior: should it toggle open/close, or always open and keep existing instance if already open?
- Execution trigger: is
Runbutton sufficient initially, or do you want keyboard execution in MVP (for example Ctrl+Enter)? - Scope safety: should evaluator run with full
_G(same as remote-control), or with a restricted environment for user-facing safety?
