Shared : command system (core/lib/cmd + ui + lua) #5
Labels
No labels
harness
proj-core-console
proj-core-demo
proj-core-edit
proj-core-init
proj-core-lib
proj-core-login
proj-core-res
proj-core-sh
proj-core-theme
proj-docs
proj-extra-calc
proj-extra-code
proj-extra-playbook
proj-extra-sheets
proj-harness
proj-os
residual
tier-0-trivial
tier-1-easy
tier-2-medium
tier-3-hard
tier-4-major
tier-5-epic
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
residual/.agent#5
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Plan: Shared
:command system incore/lib/cmd+ propagation to res-edit, res-code, res-console, res-sheetsStatus: design (decisions locked) → phased implementation plan
Locked decisions (from user)
Alt+:only, hardcoded, non-configurable. Sits in the console'sAltdomain. Never a configurable key.modeGoto/modeFindinto the:line, but keepCtrl+F/Ctrl+Gshortcuts — they pre-fill the:line.core/lib/cmdis 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."a b"→ one arg).core/lib/cmdand delete its localrunCommand.command.register(verb, fn)so Lua scripts build their own:verbcommands. Reusable glue ships ascore/lib/cmd/lua.core/lib/cmd/uiships 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
Apps compose only the layers they need:
cmd+cmd/uicmd+cmd/ui(+ latercmd/lua)cmd+cmd/ui+cmd/luacmd+cmd/ui(later phase)Builder API (
core/lib/cmd)Context{ Verb, Args, Raw, App },Result{ Status, Quit },Handler func(ctx Context) ResultSpec{ Verb, Aliases, Usage, Desc, Handler }Registry:NewRegistry,Register(idempotent; later wins),Merge(res-code composes res-edit),Run(line, app),Complete(prefix),Specs()parse.go): strip one leading:; quote-aware tokenize ("..."/'...'); resolve aliases; unknown verb → status message;Raw= everything after verb.cmd/luabridgeLuaToHandler(fn, eng),Register(r, eng, verb, fn, opts),BindCommandTable(L, r, eng)— registers acommandLua global so addon scripts docommand.register("hello", function(args) return "hi " .. (args[1] or "") end).cmd/uipalettePalettewithPosition(PosBottom / PosTop / PosOverlay), live-filter autocomplete, Tab-accept, Enter-run, Esc-cancel, mouse support. All styles fromcore/lib/tui/theme(amber palette, AGENTS.md §7). Position-agnostic: app decides placement.Propagation
core/lib/edit):modeCommand+:entry;editCommands()registry (:w :write :save :q :quit :wq :x :e :edit :goto :g :find :f); adoptui.Palette(PosBottom);Ctrl+F/Ctrl+Gpre-fill; remove oldmodeFind/modeGoto.extra/internal/code):modeCommand+:;codeCommands() = Merge(edit.EditCommands(), codeOnly)(:tabnew :tabn :tabp :b :sidebar :term);ui.Palette(PosTop).core/internal/libs/console):psCommandstate;Alt+:trigger;consoleCommands()calling existing Lua surface (:menu :widget :reload :set :q); includecmdlua.BindCommandTable; updatedocs/console/scripting.md.runCommandwithsheetsCommands(); adoptui.Palette(PosBottom); delete hardcoded switch.Implementation phases
core/lib/cmdbuilder +cmd/ui+cmd/lua+ unit tests.Alt+:+ Lua bridge).docs/design/command-registry.md, updatedocs/keymap.md, per-app docs, AGENTS.md §4 package table.Verification
cd core && go build ./... && go test ./...cd extra && go build ./... && go test ./...core/lib/cmd,cmd/ui,cmd/lua,core/lib/edit,extra/internal/code,extra/lib/sheets/tui.Risks
:must never intercept shell input → gated behindAlt+:only (hardcoded).core/lib/cmdbase Lua-free & UI-free;cmd/lua+cmd/uiare opt-in subpkgs.Alt+*console-owned;Ctrl+*editors;:bare-rune safe).tui/themeonly.PosTopin code must not overlap tab bar.Phase 0.5 complete ✓
res-democonverted to a tabbed TUI containing allcore/lib/cmdsystem 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 switchingcore/cmd/res-demo/demo_cmd.go— tests for Parse (8 cases), Registry (Register/Specs/Run/Complete), alias resolutionmain.gorewritten to launchtea.NewProgram(newModel(), tea.WithAltScreen())Design note
res-demo does not enforce the amber color theme. Only
res-consoleenforces 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/— cleango vet ./cmd/res-demo/— cleango test ./...— all tests pass (core/ and extra/)go build ./...in both repos — cleanStatus
Phase 0 complete. Ready for Phase 1 (res-edit adoption).
Phase 1: res-edit adopts
core/lib/cmd— Final DesignArchitecture
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
EnterEscEscEscKey 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/editkeybinds (library)Insert mode
EnterBackspaceDeleteTabCtrl+ZCtrl+YCtrl+UEscNormal mode
h/←j/↓k/↑l/→Ctrl+H/Ctrl+←Ctrl+L/Ctrl+→0/Home$/Endgg/Ctrl+HomeG/Ctrl+EndPgUp/PgDnCtrl+↑/Ctrl+↓EscConfig
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-editkeybinds (application layer)F1/Ctrl+HF2/Ctrl+PCtrl+SCtrl+XCtrl+OCtrl+K, SCtrl+K, KD)Command palette (F2 / Ctrl+P)
Modal: when open, all input goes to the palette until Enter or Esc.
writew,savequitq,exitwqxfindf,searchgotogedite,open:qwith unsaved changes →Result{Status: "unsaved changes — use Ctrl+X to quit"}. No!force-quit in Phase 1.Ctrl+F→ opens palette pre-filled withfindCtrl+G→ opens palette pre-filled withgotoFiles to change
core/lib/edit(library refactor)edit.gomodefield (insert/normal),vimKeysconfig,SetMode()/Mode()accessors. RemovemodeFind/modeGoto.keys.govimKeysconfig). RemovehandleFindKey/handleGotoKey. Auto-switch to insert mode on printable key.render.gomodeFind/modeGotofromrenderBottom(). Show cursor style differently in normal vs insert mode.config.goVimKeys boolfield.theme.gocore/cmd/res-edit(application — new files)main.gocmd.goeditCommands()registry with 6 commands.keys.gomodes.gomodeNormal,modeCommand,modeHelp.Verification
cd core && go build ./... && go test ./...res-edit, type code → characters insert normally (insert mode)Esc→ normal mode, hjkl navigate (if vim_keys=true), arrows always workEnterin normal mode → enters insert mode, no newline insertedF2→ palette opens at bottom, type:w→ savesCtrl+P→ same as F2F1→ help overlayCtrl+S→ savesCtrl+X→ exits (with unsaved prompt if needed)Ctrl+K, S→ save-as prompt:qwith unsaved → status message:wq→ saves and quitsCtrl+F→ palette pre-filled withfindCtrl+G→ palette pre-filled withgotoNotes
Ctrl+Shift+Sis impossible in terminals (same byte asCtrl+S). UsingCtrl+K, Schord instead.core/lib/editis a pure text editing surface. File open/save are helper functions, not core UIX. The library can be embedded in any application.Escserves dual purpose: exit insert mode (library) AND close cmd prompt (application). Application handles the prompt close; library handles the mode switch.Design refinement: modal editing is NOT part of
core/lib/editThe 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)modefield, no insert/normal split, no auto-switch behaviorKey API surface:
No
handleKey()at all in the library. The embedder callsInsert,Delete,MoveToetc. directly from their own key handler.res-edit(application — adds modal editing)Other embedders
A SQL client might:
Updated
core/lib/editkeybindsNone. The library has no keybinds. It exposes methods. The application decides which keys call which methods.
Updated
res-editkeybindsNormal mode (vim_keys=true only):
h/←MoveCursor(-1, 0)j/↓MoveCursor(0, 1)k/↑MoveCursor(0, -1)l/→MoveCursor(1, 0)0/HomeMoveToLineStart()$/EndMoveToLineEnd()gg/Ctrl+HomeMoveToFileStart()G/Ctrl+EndMoveToFileEnd()EnterEscInsert mode (always active, even with vim_keys=false):
Insert(rune)EnterInsert('\n')BackspaceBackspace()DeleteDelete()TabCtrl+ZUndo()Ctrl+YRedo()Ctrl+UEscApplication layer (always active):
F1/Ctrl+HF2/Ctrl+PCtrl+SCtrl+XCtrl+OCtrl+K, SCtrl+K, KCtrl+FfindCtrl+GgotoUpdated files
core/lib/edit(library)edit.goInsert,Delete,Backspace,MoveCursor,MoveTo,SetViewport,Undo,Redoas public methods.keys.gorender.gomodeFind/modeGotofromrenderBottom().config.goVimKeysas advisory hint for embedders.core/cmd/res-edit(application)main.gokeys.gocmd.gomodes.goPhase 1 complete ✅
What changed
core/lib/edit— now a pure text editing surface:keys.goentirely — no key handling in libraryrender.goInsertRune,InsertNewline,Backspace,Delete,InsertTab,DeleteToSOL,DeleteToEOL,MoveUp/Down/Left/Right,MoveWordLeft/Right,MoveLineStart/End,MoveFileStart/End,GotoLine,Undo,Redo,ScrollUp/Down,FindModified(),Width(),Height(),TabSize(),VimKeys(),SetStatusLeft()accessorsUpdate()now handles onlyWindowSizeMsgandMouseMsg— key handling is the embedder's responsibilityVimKeys boolconfig 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 chordcmd.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 overlaydocs/editor/edit.md— updated for new architectureBuild verification
go build ./...— cleango test ./...— all passgo vet ./...— cleanextra/builds against updated library — cleanCommit
43eda57onfeature/core-cmdbranch