Shared : command system (core/lib/cmd + ui + lua) #5

Open
opened 2026-07-19 02:30:38 +00:00 by agent · 4 comments
Member

Plan: Shared : command system in core/lib/cmd + propagation to res-edit, res-code, res-console, res-sheets

Status: design (decisions locked) → phased implementation plan

Locked decisions (from user)

  1. Console trigger = Alt+: only, hardcoded, non-configurable. Sits in the console's Alt domain. Never a configurable key.
  2. Consolidate modeGoto/modeFind into the : line, but keep Ctrl+F/Ctrl+G shortcuts — they pre-fill the : line.
  3. core/lib/cmd is a builder-type system. Provides interface + standards (parser, Registry, Handler, Context, Result, arg tokenizer with quotes) but ships no app command lists. Each app implements its own registry. res-edit owns its set; res-code composes res-edit's set.
  4. Args parsing: whitespace split plus quoted-string support ("a b" → one arg).
  5. res-sheets refactored LAST (after main build), to consume core/lib/cmd and delete its local runCommand.
  6. Lua bridge: apps expose command.register(verb, fn) so Lua scripts build their own :verb commands. Reusable glue ships as core/lib/cmd/lua.
  7. Shared UI palette: core/lib/cmd/ui ships a premade, position-agnostic bubbletea widget (input line + dropdown + live-filter autocomplete). Apps include it in the appropriate position (bottom for edit/console, top for code).

Three opt-in layers

core/lib/cmd      → pure: Registry, Spec, Handler, Context, Result, parser (quotes)
core/lib/cmd/ui   → premade Palette widget (bubbletea + tui/theme), position-agnostic
core/lib/cmd/lua  → premade Lua bridge (script.Engine ↔ cmd.Handler)

Apps compose only the layers they need:

  • res-edit = cmd + cmd/ui
  • res-code = cmd + cmd/ui (+ later cmd/lua)
  • res-console = cmd + cmd/ui + cmd/lua
  • res-sheets = cmd + cmd/ui (later phase)

Builder API (core/lib/cmd)

  • Context{ Verb, Args, Raw, App }, Result{ Status, Quit }, Handler func(ctx Context) Result
  • Spec{ Verb, Aliases, Usage, Desc, Handler }
  • Registry: NewRegistry, Register (idempotent; later wins), Merge (res-code composes res-edit), Run(line, app), Complete(prefix), Specs()
  • Parser (parse.go): strip one leading :; quote-aware tokenize ("..."/'...'); resolve aliases; unknown verb → status message; Raw = everything after verb.

cmd/lua bridge

  • LuaToHandler(fn, eng), Register(r, eng, verb, fn, opts), BindCommandTable(L, r, eng) — registers a command Lua global so addon scripts do command.register("hello", function(args) return "hi " .. (args[1] or "") end).

cmd/ui palette

  • Palette with Position (PosBottom / PosTop / PosOverlay), live-filter autocomplete, Tab-accept, Enter-run, Esc-cancel, mouse support. All styles from core/lib/tui/theme (amber palette, AGENTS.md §7). Position-agnostic: app decides placement.

Propagation

  • res-edit (core/lib/edit): modeCommand + : entry; editCommands() registry (:w :write :save :q :quit :wq :x :e :edit :goto :g :find :f); adopt ui.Palette (PosBottom); Ctrl+F/Ctrl+G pre-fill; remove old modeFind/modeGoto.
  • res-code (extra/internal/code): modeCommand + :; codeCommands() = Merge(edit.EditCommands(), codeOnly) (:tabnew :tabn :tabp :b :sidebar :term); ui.Palette (PosTop).
  • res-console (core/internal/libs/console): psCommand state; Alt+: trigger; consoleCommands() calling existing Lua surface (:menu :widget :reload :set :q); include cmdlua.BindCommandTable; update docs/console/scripting.md.
  • res-sheets (LAST): replace local runCommand with sheetsCommands(); adopt ui.Palette (PosBottom); delete hardcoded switch.

Implementation phases

  • Phase 0 — core/lib/cmd builder + cmd/ui + cmd/lua + unit tests.
  • Phase 1 — res-edit adopts it.
  • Phase 2 — res-code adopts it.
  • Phase 3 — res-console adopts it (Alt+: + Lua bridge).
  • Phase 4 — res-sheets refactor (LAST).
  • Phase 5 — docs: docs/design/command-registry.md, update docs/keymap.md, per-app docs, AGENTS.md §4 package table.

Verification

  • cd core && go build ./... && go test ./...
  • cd extra && go build ./... && go test ./...
  • Unit tests in core/lib/cmd, cmd/ui, cmd/lua, core/lib/edit, extra/internal/code, extra/lib/sheets/tui.

Risks

  • Console : must never intercept shell input → gated behind Alt+: only (hardcoded).
  • Keep core/lib/cmd base Lua-free & UI-free; cmd/lua + cmd/ui are opt-in subpkgs.
  • Don't break keymap.md (Alt+* console-owned; Ctrl+* editors; : bare-rune safe).
  • Palette must use tui/theme only. PosTop in code must not overlap tab bar.
  • res-sheets refactor deferred to Phase 4 so builder + UI are proven first.
# Plan: Shared `:` command system in `core/lib/cmd` + propagation to res-edit, res-code, res-console, res-sheets **Status:** design (decisions locked) → phased implementation plan ## Locked decisions (from user) 1. **Console trigger = `Alt+:` only**, hardcoded, **non-configurable**. Sits in the console's `Alt` domain. Never a configurable key. 2. **Consolidate `modeGoto`/`modeFind` into the `:` line**, but **keep `Ctrl+F`/`Ctrl+G` shortcuts** — they pre-fill the `:` line. 3. **`core/lib/cmd` is a builder-type system.** Provides interface + standards (parser, `Registry`, `Handler`, `Context`, `Result`, arg tokenizer with quotes) but ships no app command lists. Each app implements its own registry. res-edit owns its set; res-code **composes** res-edit's set. 4. **Args parsing:** whitespace split **plus** quoted-string support (`"a b"` → one arg). 5. **res-sheets refactored LAST** (after main build), to consume `core/lib/cmd` and delete its local `runCommand`. 6. **Lua bridge:** apps expose `command.register(verb, fn)` so Lua scripts build their own `:verb` commands. Reusable glue ships as `core/lib/cmd/lua`. 7. **Shared UI palette:** `core/lib/cmd/ui` ships a premade, position-agnostic bubbletea widget (input line + dropdown + live-filter autocomplete). Apps include it in the appropriate position (bottom for edit/console, top for code). ## Three opt-in layers ``` core/lib/cmd → pure: Registry, Spec, Handler, Context, Result, parser (quotes) core/lib/cmd/ui → premade Palette widget (bubbletea + tui/theme), position-agnostic core/lib/cmd/lua → premade Lua bridge (script.Engine ↔ cmd.Handler) ``` Apps compose only the layers they need: - res-edit = `cmd` + `cmd/ui` - res-code = `cmd` + `cmd/ui` (+ later `cmd/lua`) - res-console = `cmd` + `cmd/ui` + `cmd/lua` - res-sheets = `cmd` + `cmd/ui` (later phase) ## Builder API (`core/lib/cmd`) - `Context{ Verb, Args, Raw, App }`, `Result{ Status, Quit }`, `Handler func(ctx Context) Result` - `Spec{ Verb, Aliases, Usage, Desc, Handler }` - `Registry`: `NewRegistry`, `Register` (idempotent; later wins), `Merge` (res-code composes res-edit), `Run(line, app)`, `Complete(prefix)`, `Specs()` - Parser (`parse.go`): strip one leading `:`; quote-aware tokenize (`"..."`/`'...'`); resolve aliases; unknown verb → status message; `Raw` = everything after verb. ## `cmd/lua` bridge - `LuaToHandler(fn, eng)`, `Register(r, eng, verb, fn, opts)`, `BindCommandTable(L, r, eng)` — registers a `command` Lua global so addon scripts do `command.register("hello", function(args) return "hi " .. (args[1] or "") end)`. ## `cmd/ui` palette - `Palette` with `Position` (PosBottom / PosTop / PosOverlay), live-filter autocomplete, Tab-accept, Enter-run, Esc-cancel, mouse support. All styles from `core/lib/tui/theme` (amber palette, AGENTS.md §7). Position-agnostic: app decides placement. ## Propagation - **res-edit** (`core/lib/edit`): `modeCommand` + `:` entry; `editCommands()` registry (`:w :write :save :q :quit :wq :x :e :edit :goto :g :find :f`); adopt `ui.Palette` (PosBottom); `Ctrl+F`/`Ctrl+G` pre-fill; remove old `modeFind`/`modeGoto`. - **res-code** (`extra/internal/code`): `modeCommand` + `:`; `codeCommands() = Merge(edit.EditCommands(), codeOnly)` (`:tabnew :tabn :tabp :b :sidebar :term`); `ui.Palette` (PosTop). - **res-console** (`core/internal/libs/console`): `psCommand` state; `Alt+:` trigger; `consoleCommands()` calling existing Lua surface (`:menu :widget :reload :set :q`); include `cmdlua.BindCommandTable`; update `docs/console/scripting.md`. - **res-sheets** (LAST): replace local `runCommand` with `sheetsCommands()`; adopt `ui.Palette` (PosBottom); delete hardcoded switch. ## Implementation phases - Phase 0 — `core/lib/cmd` builder + `cmd/ui` + `cmd/lua` + unit tests. - Phase 1 — res-edit adopts it. - Phase 2 — res-code adopts it. - Phase 3 — res-console adopts it (`Alt+:` + Lua bridge). - Phase 4 — res-sheets refactor (LAST). - Phase 5 — docs: `docs/design/command-registry.md`, update `docs/keymap.md`, per-app docs, AGENTS.md §4 package table. ## Verification - `cd core && go build ./... && go test ./...` - `cd extra && go build ./... && go test ./...` - Unit tests in `core/lib/cmd`, `cmd/ui`, `cmd/lua`, `core/lib/edit`, `extra/internal/code`, `extra/lib/sheets/tui`. ## Risks - Console `:` must never intercept shell input → gated behind `Alt+:` only (hardcoded). - Keep `core/lib/cmd` base Lua-free & UI-free; `cmd/lua` + `cmd/ui` are opt-in subpkgs. - Don't break keymap.md (`Alt+*` console-owned; `Ctrl+*` editors; `:` bare-rune safe). - Palette must use `tui/theme` only. `PosTop` in code must not overlap tab bar. - res-sheets refactor deferred to Phase 4 so builder + UI are proven first.
Author
Member

Phase 0.5 complete ✓

res-demo converted to a tabbed TUI containing all core/lib/cmd system tests.

What was built

  • core/cmd/res-demo/model.go — bubbletea model with 8 tabs (ANSI, Cmd, Config, Defaults, Log, TUI, Util, XDG), scrollable content, help overlay, tab switching
  • core/cmd/res-demo/demo_cmd.go — tests for Parse (8 cases), Registry (Register/Specs/Run/Complete), alias resolution
  • main.go rewritten to launch tea.NewProgram(newModel(), tea.WithAltScreen())

Design note

res-demo does not enforce the amber color theme. Only res-console enforces the palette. The UI chrome (tabs, status bar, help) uses plain text that inherits from the parent shell. Demo content may contain amber escape sequences from the theme package demos — that's expected.

Verification

  • go build ./cmd/res-demo/ — clean
  • go vet ./cmd/res-demo/ — clean
  • go test ./... — all tests pass (core/ and extra/)
  • go build ./... in both repos — clean

Status

Phase 0 complete. Ready for Phase 1 (res-edit adoption).

## Phase 0.5 complete ✓ `res-demo` converted to a tabbed TUI containing all `core/lib/cmd` system tests. ### What was built - `core/cmd/res-demo/model.go` — bubbletea model with 8 tabs (ANSI, Cmd, Config, Defaults, Log, TUI, Util, XDG), scrollable content, help overlay, tab switching - `core/cmd/res-demo/demo_cmd.go` — tests for Parse (8 cases), Registry (Register/Specs/Run/Complete), alias resolution - `main.go` rewritten to launch `tea.NewProgram(newModel(), tea.WithAltScreen())` ### Design note res-demo does **not** enforce the amber color theme. Only `res-console` enforces the palette. The UI chrome (tabs, status bar, help) uses plain text that inherits from the parent shell. Demo content may contain amber escape sequences from the theme package demos — that's expected. ### Verification - `go build ./cmd/res-demo/` — clean - `go vet ./cmd/res-demo/` — clean - `go test ./...` — all tests pass (core/ and extra/) - `go build ./...` in both repos — clean ### Status Phase 0 complete. Ready for Phase 1 (res-edit adoption).
Author
Member

Phase 1: res-edit adopts core/lib/cmd — Final Design

Architecture

core/lib/edit = embeddable editor surface. Pure UI element for editing generic text bodies. Unaware of files — works with raw byte data/buffers. Can be embedded into any application (SQL client, message composer, etc.) as a text field editor.

res-edit = application layer. Adds file operations, command palette, help overlay, application-level keybinds.

core/lib/cmd / cmd/ui = command registry + palette widget. Consumed by the application layer, not the library.


Modal editing design

The editor has two modes:

Insert mode (default) — typing works, characters go into the buffer.
Normal mode — navigation keys (hjkl, arrows, etc.), no typing.

Mode transitions

Trigger From → To Behavior
Any printable key Normal → Insert Key is typed into buffer (the trigger character is NOT consumed)
Enter Normal → Insert Mode switches, but Enter itself does NOT insert a newline
Esc Insert → Normal Returns to normal mode
Esc Normal (cmd prompt open) Closes cmd prompt, stays in normal mode
Esc Normal (no prompt) No-op

Key rule: non-printable keys do NOT trigger insert mode

If the user presses an arrow key, PgUp, Ctrl+, F-key, etc. while in normal mode, the key is handled in normal mode and does NOT switch to insert mode. Only printable characters (letters, numbers, symbols, space) trigger the auto-switch to insert mode.

This means: hjkl in normal mode = navigation. The moment the user types any character, they're in insert mode and that character is inserted.


core/lib/edit keybinds (library)

Insert mode

Key Action
Typing insert character
Enter insert newline
Backspace delete before cursor
Delete delete at cursor
Tab indent to tab stop
Ctrl+Z undo
Ctrl+Y redo
Ctrl+U delete to start of line
Esc return to normal mode

Normal mode

Key Action
h / move cursor left
j / move cursor down
k / move cursor up
l / move cursor right
Ctrl+H / Ctrl+← jump word left
Ctrl+L / Ctrl+→ jump word right
0 / Home line start
$ / End line end
gg / Ctrl+Home file start
G / Ctrl+End file end
PgUp / PgDn page up/down
Ctrl+↑ / Ctrl+↓ scroll viewport
Esc no-op (or close cmd prompt — application handles this)

Config

# edit/config.toml
vim_keys = false  # default off; when true, enables hjkl + 0$ggG in normal mode

When vim_keys = false (default): arrows only for navigation. hjkl are typed as characters. Nano-like behavior.
When vim_keys = true: hjkl/0/$/gg/G are navigation in normal mode. i/a/o to enter insert mode.


res-edit keybinds (application layer)

Key Action
F1 / Ctrl+H help overlay
F2 / Ctrl+P command palette (modal)
Ctrl+S save
Ctrl+X exit (replaces Ctrl+Q)
Ctrl+O open file
Ctrl+K, S save-as (chord sequence)
Ctrl+K, K delete to end of line (vim D)

Command palette (F2 / Ctrl+P)

Modal: when open, all input goes to the palette until Enter or Esc.

Verb Aliases Action
write w, save save buffer
quit q, exit quit
wq x save then quit
find f, search find text (moves cursor)
goto g go to line number
edit e, open open file

:q with unsaved changes → Result{Status: "unsaved changes — use Ctrl+X to quit"}. No ! force-quit in Phase 1.

Ctrl+F → opens palette pre-filled with find
Ctrl+G → opens palette pre-filled with goto


Files to change

core/lib/edit (library refactor)

File Change
edit.go Add mode field (insert/normal), vimKeys config, SetMode()/Mode() accessors. Remove modeFind/modeGoto.
keys.go Split into insert-mode and normal-mode handlers. Add hjkl/0/$/gg/G in normal mode (behind vimKeys config). Remove handleFindKey/handleGotoKey. Auto-switch to insert mode on printable key.
render.go Remove modeFind/modeGoto from renderBottom(). Show cursor style differently in normal vs insert mode.
config.go Add VimKeys bool field.
theme.go No changes (already neutral).

core/cmd/res-edit (application — new files)

File Change
main.go Wire editor + registry + palette. Application-level key routing.
cmd.go New. editCommands() registry with 6 commands.
keys.go New. Application keybinds: F1, F2/Ctrl+P, Ctrl+S/X/O, chord Ctrl+K.
modes.go New. Application modes: modeNormal, modeCommand, modeHelp.

Verification

  1. cd core && go build ./... && go test ./...
  2. Launch res-edit, type code → characters insert normally (insert mode)
  3. Press Esc → normal mode, hjkl navigate (if vim_keys=true), arrows always work
  4. Type any character → auto-switch to insert mode, character is inserted
  5. Press Enter in normal mode → enters insert mode, no newline inserted
  6. Press F2 → palette opens at bottom, type :w → saves
  7. Press Ctrl+P → same as F2
  8. Press F1 → help overlay
  9. Press Ctrl+S → saves
  10. Press Ctrl+X → exits (with unsaved prompt if needed)
  11. Ctrl+K, S → save-as prompt
  12. :q with unsaved → status message
  13. :wq → saves and quits
  14. Ctrl+F → palette pre-filled with find
  15. Ctrl+G → palette pre-filled with goto

Notes

  • Ctrl+Shift+S is impossible in terminals (same byte as Ctrl+S). Using Ctrl+K, S chord instead.
  • core/lib/edit is a pure text editing surface. File open/save are helper functions, not core UIX. The library can be embedded in any application.
  • Esc serves dual purpose: exit insert mode (library) AND close cmd prompt (application). Application handles the prompt close; library handles the mode switch.
## Phase 1: res-edit adopts `core/lib/cmd` — Final Design ### Architecture **`core/lib/edit`** = embeddable editor surface. Pure UI element for editing generic text bodies. Unaware of files — works with raw byte data/buffers. Can be embedded into any application (SQL client, message composer, etc.) as a text field editor. **`res-edit`** = application layer. Adds file operations, command palette, help overlay, application-level keybinds. **`core/lib/cmd` / `cmd/ui`** = command registry + palette widget. Consumed by the application layer, not the library. --- ### Modal editing design The editor has two modes: **Insert mode** (default) — typing works, characters go into the buffer. **Normal mode** — navigation keys (hjkl, arrows, etc.), no typing. #### Mode transitions | Trigger | From → To | Behavior | |---------|-----------|----------| | Any printable key | Normal → Insert | Key is typed into buffer (the trigger character is NOT consumed) | | `Enter` | Normal → Insert | Mode switches, but Enter itself does NOT insert a newline | | `Esc` | Insert → Normal | Returns to normal mode | | `Esc` | Normal (cmd prompt open) | Closes cmd prompt, stays in normal mode | | `Esc` | Normal (no prompt) | No-op | #### Key rule: non-printable keys do NOT trigger insert mode If the user presses an arrow key, PgUp, Ctrl+<key>, F-key, etc. while in normal mode, the key is handled in normal mode and does NOT switch to insert mode. Only printable characters (letters, numbers, symbols, space) trigger the auto-switch to insert mode. This means: hjkl in normal mode = navigation. The moment the user types any character, they're in insert mode and that character is inserted. --- ### `core/lib/edit` keybinds (library) #### Insert mode | Key | Action | |-----|--------| | Typing | insert character | | `Enter` | insert newline | | `Backspace` | delete before cursor | | `Delete` | delete at cursor | | `Tab` | indent to tab stop | | `Ctrl+Z` | undo | | `Ctrl+Y` | redo | | `Ctrl+U` | delete to start of line | | `Esc` | return to normal mode | #### Normal mode | Key | Action | |-----|--------| | `h` / `←` | move cursor left | | `j` / `↓` | move cursor down | | `k` / `↑` | move cursor up | | `l` / `→` | move cursor right | | `Ctrl+H` / `Ctrl+←` | jump word left | | `Ctrl+L` / `Ctrl+→` | jump word right | | `0` / `Home` | line start | | `$` / `End` | line end | | `gg` / `Ctrl+Home` | file start | | `G` / `Ctrl+End` | file end | | `PgUp` / `PgDn` | page up/down | | `Ctrl+↑` / `Ctrl+↓` | scroll viewport | | `Esc` | no-op (or close cmd prompt — application handles this) | #### Config ```toml # edit/config.toml vim_keys = false # default off; when true, enables hjkl + 0$ggG in normal mode ``` When `vim_keys = false` (default): arrows only for navigation. hjkl are typed as characters. Nano-like behavior. When `vim_keys = true`: hjkl/0/$/gg/G are navigation in normal mode. i/a/o to enter insert mode. --- ### `res-edit` keybinds (application layer) | Key | Action | |-----|--------| | `F1` / `Ctrl+H` | help overlay | | `F2` / `Ctrl+P` | command palette (modal) | | `Ctrl+S` | save | | `Ctrl+X` | exit (replaces Ctrl+Q) | | `Ctrl+O` | open file | | `Ctrl+K, S` | save-as (chord sequence) | | `Ctrl+K, K` | delete to end of line (vim `D`) | #### Command palette (F2 / Ctrl+P) Modal: when open, all input goes to the palette until Enter or Esc. | Verb | Aliases | Action | |------|---------|--------| | `write` | `w`, `save` | save buffer | | `quit` | `q`, `exit` | quit | | `wq` | `x` | save then quit | | `find` | `f`, `search` | find text (moves cursor) | | `goto` | `g` | go to line number | | `edit` | `e`, `open` | open file | `:q` with unsaved changes → `Result{Status: "unsaved changes — use Ctrl+X to quit"}`. No `!` force-quit in Phase 1. `Ctrl+F` → opens palette pre-filled with `find ` `Ctrl+G` → opens palette pre-filled with `goto ` --- ### Files to change #### `core/lib/edit` (library refactor) | File | Change | |------|--------| | `edit.go` | Add `mode` field (insert/normal), `vimKeys` config, `SetMode()`/`Mode()` accessors. Remove `modeFind`/`modeGoto`. | | `keys.go` | Split into insert-mode and normal-mode handlers. Add hjkl/0/$/gg/G in normal mode (behind `vimKeys` config). Remove `handleFindKey`/`handleGotoKey`. Auto-switch to insert mode on printable key. | | `render.go` | Remove `modeFind`/`modeGoto` from `renderBottom()`. Show cursor style differently in normal vs insert mode. | | `config.go` | Add `VimKeys bool` field. | | `theme.go` | No changes (already neutral). | #### `core/cmd/res-edit` (application — new files) | File | Change | |------|--------| | `main.go` | Wire editor + registry + palette. Application-level key routing. | | `cmd.go` | **New.** `editCommands()` registry with 6 commands. | | `keys.go` | **New.** Application keybinds: F1, F2/Ctrl+P, Ctrl+S/X/O, chord Ctrl+K. | | `modes.go` | **New.** Application modes: `modeNormal`, `modeCommand`, `modeHelp`. | --- ### Verification 1. `cd core && go build ./... && go test ./...` 2. Launch `res-edit`, type code → characters insert normally (insert mode) 3. Press `Esc` → normal mode, hjkl navigate (if vim_keys=true), arrows always work 4. Type any character → auto-switch to insert mode, character is inserted 5. Press `Enter` in normal mode → enters insert mode, no newline inserted 6. Press `F2` → palette opens at bottom, type `:w` → saves 7. Press `Ctrl+P` → same as F2 8. Press `F1` → help overlay 9. Press `Ctrl+S` → saves 10. Press `Ctrl+X` → exits (with unsaved prompt if needed) 11. `Ctrl+K, S` → save-as prompt 12. `:q` with unsaved → status message 13. `:wq` → saves and quits 14. `Ctrl+F` → palette pre-filled with `find ` 15. `Ctrl+G` → palette pre-filled with `goto ` --- ### Notes - `Ctrl+Shift+S` is impossible in terminals (same byte as `Ctrl+S`). Using `Ctrl+K, S` chord instead. - `core/lib/edit` is a pure text editing surface. File open/save are helper functions, not core UIX. The library can be embedded in any application. - `Esc` serves dual purpose: exit insert mode (library) AND close cmd prompt (application). Application handles the prompt close; library handles the mode switch.
Author
Member

Design refinement: modal editing is NOT part of core/lib/edit

The library is a raw text editing surface with no mode system. Mode switching (insert/normal) is an application-layer feature that embedders opt into.

core/lib/edit (library — raw surface)

  • Exposes: cursor movement, text insert/delete, undo/redo, selection, scrolling, rendering
  • No mode field, no insert/normal split, no auto-switch behavior
  • Key handling: all keys are handled directly — if the embedder wants modal behavior, they implement it in their application layer
  • Embedder controls: which keys do what. Library provides the primitives, not the policy.
  • Can be used as: a text field in a form, a code editor, a message composer, anything

Key API surface:

e := edit.New(config)
e.LoadBytes(data)        // or LoadFile(path) as helper
e.Insert(rune)           // insert character at cursor
e.Delete()               // delete at cursor
e.Backspace()            // delete before cursor
e.MoveCursor(dx, dy)     // relative movement
e.MoveTo(line, col)      // absolute movement
e.SetViewport(y int)     // scroll
e.Undo() / e.Redo()
e.View() string          // render

No handleKey() at all in the library. The embedder calls Insert, Delete, MoveTo etc. directly from their own key handler.

res-edit (application — adds modal editing)

  • Implements insert/normal mode switching
  • Owns the key dispatch table: routes keys to library methods based on current mode
  • Implements the mode transitions described previously (printable → insert, Esc → normal)
  • Adds: F1 help, F2 palette, Ctrl+S/X/O, Ctrl+K chords
  • Adds: vim_keys config toggle

Other embedders

A SQL client might:

e := edit.New(config)
// No modal editing — just raw text editing
// Custom syntax highlighting via edit.Highlighter interface
for {
    msg := <-tea.Msg
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.Type {
        case tea.KeyRunes:
            e.Insert(msg.Runes[0])
        case tea.KeyBackspace:
            e.Backspace()
        case tea.KeyEnter:
            e.Insert('\n')
        case tea.KeyArrowUp:
            e.MoveCursor(0, -1)
        // ... no modes, no palette, just direct control
        }
    }
}

Updated core/lib/edit keybinds

None. The library has no keybinds. It exposes methods. The application decides which keys call which methods.

Updated res-edit keybinds

Normal mode (vim_keys=true only):

Key Action
h/ MoveCursor(-1, 0)
j/ MoveCursor(0, 1)
k/ MoveCursor(0, -1)
l/ MoveCursor(1, 0)
0/Home MoveToLineStart()
$/End MoveToLineEnd()
gg/Ctrl+Home MoveToFileStart()
G/Ctrl+End MoveToFileEnd()
Printable char → insert mode + insert char
Enter → insert mode (consumed)
Esc close cmd prompt or no-op

Insert mode (always active, even with vim_keys=false):

Key Action
Printable char Insert(rune)
Enter Insert('\n')
Backspace Backspace()
Delete Delete()
Tab indent
Ctrl+Z Undo()
Ctrl+Y Redo()
Ctrl+U delete to line start
Esc → normal mode

Application layer (always active):

Key Action
F1/Ctrl+H help overlay
F2/Ctrl+P command palette
Ctrl+S save
Ctrl+X exit
Ctrl+O open
Ctrl+K, S save-as
Ctrl+K, K delete to end of line
Ctrl+F palette with find
Ctrl+G palette with goto

Updated files

core/lib/edit (library)

File Change
edit.go Remove mode system. Expose Insert, Delete, Backspace, MoveCursor, MoveTo, SetViewport, Undo, Redo as public methods.
keys.go Delete entirely. No key handling in library.
render.go Remove modeFind/modeGoto from renderBottom().
config.go Remove mode-related config. Keep VimKeys as advisory hint for embedders.

core/cmd/res-edit (application)

File Change
main.go Wire editor + registry + palette. Application-level key dispatch.
keys.go New. Insert-mode and normal-mode key tables, routing to library methods.
cmd.go New. Command registry (write, quit, wq, find, goto, edit).
modes.go New. Mode state machine (insert/normal/command/help).
## Design refinement: modal editing is NOT part of `core/lib/edit` The library is a raw text editing surface with no mode system. Mode switching (insert/normal) is an **application-layer feature** that embedders opt into. ### `core/lib/edit` (library — raw surface) - Exposes: cursor movement, text insert/delete, undo/redo, selection, scrolling, rendering - No `mode` field, no insert/normal split, no auto-switch behavior - Key handling: all keys are handled directly — if the embedder wants modal behavior, they implement it in their application layer - Embedder controls: which keys do what. Library provides the primitives, not the policy. - Can be used as: a text field in a form, a code editor, a message composer, anything **Key API surface:** ```go e := edit.New(config) e.LoadBytes(data) // or LoadFile(path) as helper e.Insert(rune) // insert character at cursor e.Delete() // delete at cursor e.Backspace() // delete before cursor e.MoveCursor(dx, dy) // relative movement e.MoveTo(line, col) // absolute movement e.SetViewport(y int) // scroll e.Undo() / e.Redo() e.View() string // render ``` No `handleKey()` at all in the library. The embedder calls `Insert`, `Delete`, `MoveTo` etc. directly from their own key handler. ### `res-edit` (application — adds modal editing) - Implements insert/normal mode switching - Owns the key dispatch table: routes keys to library methods based on current mode - Implements the mode transitions described previously (printable → insert, Esc → normal) - Adds: F1 help, F2 palette, Ctrl+S/X/O, Ctrl+K chords - Adds: vim_keys config toggle ### Other embedders A SQL client might: ```go e := edit.New(config) // No modal editing — just raw text editing // Custom syntax highlighting via edit.Highlighter interface for { msg := <-tea.Msg switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyRunes: e.Insert(msg.Runes[0]) case tea.KeyBackspace: e.Backspace() case tea.KeyEnter: e.Insert('\n') case tea.KeyArrowUp: e.MoveCursor(0, -1) // ... no modes, no palette, just direct control } } } ``` ### Updated `core/lib/edit` keybinds None. The library has **no keybinds**. It exposes methods. The application decides which keys call which methods. ### Updated `res-edit` keybinds **Normal mode (vim_keys=true only):** | Key | Action | |-----|--------| | `h`/`←` | `MoveCursor(-1, 0)` | | `j`/`↓` | `MoveCursor(0, 1)` | | `k`/`↑` | `MoveCursor(0, -1)` | | `l`/`→` | `MoveCursor(1, 0)` | | `0`/`Home` | `MoveToLineStart()` | | `$`/`End` | `MoveToLineEnd()` | | `gg`/`Ctrl+Home` | `MoveToFileStart()` | | `G`/`Ctrl+End` | `MoveToFileEnd()` | | Printable char | → insert mode + insert char | | `Enter` | → insert mode (consumed) | | `Esc` | close cmd prompt or no-op | **Insert mode (always active, even with vim_keys=false):** | Key | Action | |-----|--------| | Printable char | `Insert(rune)` | | `Enter` | `Insert('\n')` | | `Backspace` | `Backspace()` | | `Delete` | `Delete()` | | `Tab` | indent | | `Ctrl+Z` | `Undo()` | | `Ctrl+Y` | `Redo()` | | `Ctrl+U` | delete to line start | | `Esc` | → normal mode | **Application layer (always active):** | Key | Action | |-----|--------| | `F1`/`Ctrl+H` | help overlay | | `F2`/`Ctrl+P` | command palette | | `Ctrl+S` | save | | `Ctrl+X` | exit | | `Ctrl+O` | open | | `Ctrl+K, S` | save-as | | `Ctrl+K, K` | delete to end of line | | `Ctrl+F` | palette with `find ` | | `Ctrl+G` | palette with `goto ` | ### Updated files #### `core/lib/edit` (library) | File | Change | |------|--------| | `edit.go` | Remove mode system. Expose `Insert`, `Delete`, `Backspace`, `MoveCursor`, `MoveTo`, `SetViewport`, `Undo`, `Redo` as public methods. | | `keys.go` | **Delete entirely.** No key handling in library. | | `render.go` | Remove `modeFind`/`modeGoto` from `renderBottom()`. | | `config.go` | Remove mode-related config. Keep `VimKeys` as advisory hint for embedders. | #### `core/cmd/res-edit` (application) | File | Change | |------|--------| | `main.go` | Wire editor + registry + palette. Application-level key dispatch. | | `keys.go` | **New.** Insert-mode and normal-mode key tables, routing to library methods. | | `cmd.go` | **New.** Command registry (write, quit, wq, find, goto, edit). | | `modes.go` | **New.** Mode state machine (insert/normal/command/help). |
Author
Member

Phase 1 complete

What changed

core/lib/edit — now a pure text editing surface:

  • Removed mode system (modeNormal/modeFind/modeGoto/modeHelp/modeQuit)
  • Removed keys.go entirely — no key handling in library
  • Removed help overlay and mode-specific prompt rendering from render.go
  • Exposed public methods: InsertRune, InsertNewline, Backspace, Delete, InsertTab, DeleteToSOL, DeleteToEOL, MoveUp/Down/Left/Right, MoveWordLeft/Right, MoveLineStart/End, MoveFileStart/End, GotoLine, Undo, Redo, ScrollUp/Down, Find
  • Added Modified(), Width(), Height(), TabSize(), VimKeys(), SetStatusLeft() accessors
  • Update() now handles only WindowSizeMsg and MouseMsg — key handling is the embedder's responsibility
  • Added VimKeys bool config field (advisory hint for embedders)

core/cmd/res-edit — new application layer:

  • modes.go — mode state machine (insert/normal/command/help/quit)
  • keys.go — full key dispatch: insert mode, normal mode, command palette, global keys, Ctrl+K chord
  • cmd.go — command registry with 6 commands (write/quit/wq/find/goto/edit)
  • main.go — wired model with mode switching, status bar, command palette overlay, help overlay

docs/editor/edit.md — updated for new architecture

Build verification

  • go build ./... — clean
  • go test ./... — all pass
  • go vet ./... — clean
  • extra/ builds against updated library — clean

Commit

43eda57 on feature/core-cmd branch

## Phase 1 complete ✅ ### What changed **`core/lib/edit`** — now a pure text editing surface: - Removed mode system (modeNormal/modeFind/modeGoto/modeHelp/modeQuit) - Removed `keys.go` entirely — no key handling in library - Removed help overlay and mode-specific prompt rendering from `render.go` - Exposed public methods: `InsertRune`, `InsertNewline`, `Backspace`, `Delete`, `InsertTab`, `DeleteToSOL`, `DeleteToEOL`, `MoveUp/Down/Left/Right`, `MoveWordLeft/Right`, `MoveLineStart/End`, `MoveFileStart/End`, `GotoLine`, `Undo`, `Redo`, `ScrollUp/Down`, `Find` - Added `Modified()`, `Width()`, `Height()`, `TabSize()`, `VimKeys()`, `SetStatusLeft()` accessors - `Update()` now handles only `WindowSizeMsg` and `MouseMsg` — key handling is the embedder's responsibility - Added `VimKeys bool` config field (advisory hint for embedders) **`core/cmd/res-edit`** — new application layer: - `modes.go` — mode state machine (insert/normal/command/help/quit) - `keys.go` — full key dispatch: insert mode, normal mode, command palette, global keys, Ctrl+K chord - `cmd.go` — command registry with 6 commands (write/quit/wq/find/goto/edit) - `main.go` — wired model with mode switching, status bar, command palette overlay, help overlay **`docs/editor/edit.md`** — updated for new architecture ### Build verification - `go build ./...` — clean - `go test ./...` — all pass - `go vet ./...` — clean - `extra/` builds against updated library — clean ### Commit `43eda57` on `feature/core-cmd` branch
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
residual/.agent#5
No description provided.