Skip to content

A new input API for projects - #50

Open
hleb-rubanau wants to merge 19 commits into
aldum:devfrom
hleb-rubanau:features/new-input-api
Open

A new input API for projects #50
hleb-rubanau wants to merge 19 commits into
aldum:devfrom
hleb-rubanau:features/new-input-api

Conversation

@hleb-rubanau

Copy link
Copy Markdown
Collaborator

This branch addresses issue compy-toys#77

Stakeholders asked for a simpler and more robust input API. This delivers it:
one input widget with a precise surface and a simple wrapper over it, a lifecycle
a project configures rather than infers from the call it made, first-class
keyboard/mouse/touch shortcuts, and the removal of the legacy polling globals.
It is a large, breaking change and a 1.0.0-rc — the full, user-facing log is
CHANGELOG.md (CURRENT_SCOPE), and the project-author guide is
doc/input_api.md, which is enough on its own to review the surface.

It is submitted ahead of some remaining work — device testing, human review
and the docs/ledger finishing passes — see What's still owed
before merging.

What's delivered

Two surfaces over one widget.

  • Simplecompy.ask(prompt, …) puts a question on screen, compy.on_answer(text)
    receives it (or nil on Escape, telling answered from abandoned),
    compy.on_key(...) reports keys while it is open and its return value decides
    who consumes them, and compy.unask() withdraws it. A wrapper over the same
    widget — no second widget, no second event path.
  • Precisecompy.input.show / hide / configure / clear / set_text / set_cursor / get_text / get_cursor / is_shown, plus callbacks, hooks, shortcuts
    and fn. One show and a callback replace the old poll-a-global-every-frame
    idiom.

A lifecycle the project sets.

  • Four independent flagsclear_on_submit, hide_on_submit,
    clear_on_cancel, hide_on_cancel — each a mode that persists until unset,
    settable on show or configure. Colouring, the submit gate and the submit
    action are three optional keys (highlighter, validator, on_text_entered),
    no longer bundled into a choice of function name.
  • Defaults are least-destructive: clear_on_submit on, Escape does nothing
    until asked. show/configure split cleanly by who owns what (content is
    show's; project settings are either's), and both raise on an unknown key
    instead of dropping it.

Shortcuts, hooks and event control.

  • compy.input.shortcuts.<event>[combo] binds keyboard/mouse/touch shortcuts
    directly (including modifier classes like alt+*); compy.input.hooks[event]
    is the per-event fallback, and an existing love.* handler is seeded into it
    and keeps working. compy.input.fn.* declares repeat/propagation handling at
    the binding site. compy.before_exit lets a project restore global device
    state before its run ends.

Robustness.

  • The widget survives hide/show (settings and content persist), activation no
    longer destroys content, and a shown widget no longer blocks the project's
    own key/text handlers.
  • Fixes, several longstanding: multi-line string content now writes; non-text
    content is refused with a readable message; the error boundary no longer
    swallows crashes in love.update/pointer handlers or drops callback arguments
    on non-LuaJIT runtimes; unconsumed project input no longer accumulates in the
    hidden console.

Breaking changes (announced here, not in the version — this is pre-1.0):

  • The legacy text-input globals are removed with no shiminput_text,
    input_code, validated_input, user_input, write_to_input (and
    compy.singleclick/doubleclick). The guide's "Migration from the legacy
    globals"
    maps each to its replacement.

What it stays on

This branch is built on the platform's editor overhaul, upstream PR #45, which
it imported as its base (the editor's key routing and discard behaviour are #45's,
and the input widget seats its text field through the seam #45 owns).

Merge-order consequence, stated plainly: this branch already contains #45's
commits
. So —

The platform repository is otherwise reconciled with upstream/dev; one small
upstream edge remains and is post-release.

What's still owed

This PR ships ahead of the following, each recorded as active technical debt in
doc/development/technical_debt/ and to be closed before the release proper:

  • Device end-to-end testing (T-PR-DEVICE-EX) — the shipped surface has
    never been completely exercised on the target hardware in the form it now takes; prior
    device runs predate the surface's significant moves, so this is thorough
    validation, not a smoke. The same entry covers re-fitting the external example
    projects
    to the shipped API (they sit on an earlier version) and opening their
    own PRs. Smoke test faitfhfully done: basic editing, running projects, in-repo tixy/turtle/sapper.
  • Human review (T-PR-REVIEW) — no human code review and no cold read of the
    whole assembled change has happened yet; that is what this submission is for.
  • Corpus finishing (T-PR-CORPUS) — the final documentation compaction, a
    readability pass and a proof-read, and two ledger-hygiene sweeps. Legibility
    debt, not correctness — no behaviour depends on it.

Two capabilities are deferred deliberately, with their reasons on record: a
project cannot read/navigate the widget's input history (the limitation is
documented in the guide; the capability is backlogged), and compy.input in the
console environment
is designed but out of this release.

dsent and others added 19 commits September 4, 2026 10:29
The Keys tables in README.md and doc/EDITOR.md were stale and mutually
inconsistent. Correct them to the behavior verified against the shipped
editor build 1b86c90: toggle edit/run = Ctrl+T (was F8/F9), stop project
= Ctrl+S (was Ctrl+Shift+S), quit project = Ctrl+Q (EDITOR had
Ctrl+Shift+Q), leave editor = Shift+Esc / Ctrl+Shift+S, drop the stale
editor Esc/Ctrl+S/Ctrl+Y rows, and add the input clipboard/selection
keys.

These tables document commit 1b86c90. The editor-stage1 (2eed100) keymap
update follows as a separate change.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0177PwS4Xws9YFAGqJMADY5c
…work spec

The editor's implicit `edit` flag becomes two first-class
modes: `nav`, where the arrows walk the file, and `edit`,
where an open block sits in the input strip. Transitions
are declared in one table in set_mode: nav reaches edit,
reorder and search; the special modes and edit only return
to nav. A freshly opened buffer always starts in nav.

Keys, per spec 2.2/2.3:
- Enter on an empty input opens the selected block; Enter
  while editing accepts it
- bare Esc does nothing (it also defuses the device's
  RMB->Esc binding silently replacing typed input)
- Shift+Esc discards the edit; on an empty input it leaves
  the editor. Ctrl+W no longer leaves a block
- Ctrl+S is reserved for the checkpoint: saving is
  automatic on accept, so bare Ctrl+S does nothing
- Ctrl+Delete drops a block in navigation only; while
  editing it stays the widget's delete-next-word
- typing straight into a fresh file inserts a new block
  and never replaces the first one (replace needs a block
  deliberately opened)

Files open at the first block, the view following the
selection; the cursor position is remembered per file for
the session and restored on reopen.

The statusline receives the real mode. A mode tag was
tried and dropped: the mode is evident from the input
strip itself, and the tag collided with the left label.

Co-Authored-By: Claude <[email protected]>
The buffer model gains an active line under the block
selection: an absolute source line, always inside the
selected block, clamped on every selection mutation, with
move_line stepping across block boundaries. The view
draws it and follows it on scroll.

Keys, per spec 2.2/2.7:
- bare Up/Down move the active line; PageUp/PageDown
  move it by a viewport page; Home/End go to the file's
  first and last line; opening a block lands the cursor
  on the active line, not on line 1
- Ctrl+Up/Down jump block-wise with the paragraph
  convention: Down to the next block's first line, Up
  first to the current block's first line
- Alt is the peek: Alt+Up/Down scroll a line, Alt+
  PageUp/PageDown a page (Alt+Left/Right too — PgUp/PgDn
  is a four-key chord on the device keyboard), Alt+Home/
  End to the file's edges, the selection staying put in
  both modes; typing after a peek returns the view.
  Ctrl+Alt keeps working as a synonym. Alt does not move
  blocks: that is the reorder mode's job (Ctrl+M)
- inside the input widget bare Home/End are line-scoped
  and Ctrl+Home/End reach the whole block (dsent)
- Ctrl+J follows the require under the selection; Ctrl+O
  is left free for a conventional "open file"
- returning from a followed require restores the view to
  the stored position, as opening a file does

The textinput guard widens to any Ctrl/Alt chord: device
chords leak glyphs, only Shift composes.

Co-Authored-By: Claude <[email protected]>
Co-Authored-By: dsent <[email protected]>
…ith a knock

One path out of an open block, shared by Enter, the arrow
edge crossing, Ctrl+Up/Down from editing and the mouse:
dirty-check, validate, auto-format, re-chunk, size check,
write. Acceptance in place keeps the block (2.4.4); the
arrow transition opens the neighbor — downward on its
first line, upward on its last (2.4.1). An untouched
block leaves freely and writes nothing; an invalid one
refuses and stays.

- opening a Lua block runs it through the pretty-printer
  (9.4): a sloppy block is reshaped in the input, dirty
  from birth, while the file stays untouched until
  acceptance
- the block size limit is the input strip's height (14
  lines); the refusal names how many lines to remove and
  the way out, per 1.4
- an eval refusal moves the cursor to the first error's
  line and shows the parser's message until the first
  fixing keystroke (2.5); the refusing keypress is blocked
  from the widget so it does not clear its own message
- every refusal knocks (2.4.3): a rejected block, a jump
  or page move at the file's edge, a click away from an
  invalid block, Ctrl+J with nothing to follow — one
  neutral sound, since walking off the end of a file is
  not a mistake
- typing in navigation opens the block at the active line
  with a blank line to type into (2.1); on a blank line it
  composes a new block. Ctrl+Enter opens a fresh block
  below (Ctrl+Shift+Enter above) and accepts while editing
- the acceptance pipeline lives in controller methods
  (accept_block and friends), reachable from any input,
  and reports its verdict so a refusal blocks the widget
- writes are durable: the accept path fsyncs the file, and
  a failed write refuses with a message instead of reading
  as accepted (dsent, after real data loss on the device's
  exfat SD card)
- typing right after deleting the last block anchors the
  input properly (dsent)

Co-Authored-By: Claude <[email protected]>
Co-Authored-By: dsent <[email protected]>
…) in the REPL

Ctrl+K copies the file to {name}.~save; Ctrl+Shift+K copies
it back and reloads the open buffer. Both raise a dialog
when there is something to lose — an existing checkpoint,
a file about to be overwritten — showing the timestamps of
the checkpoint and the file (FS.getInfo). While editing,
Ctrl+K accepts the open block first, so the checkpoint
reflects the screen; a refused block aborts it.

The REPL gains revert(name) (default main.lua): restore
without a prompt, false when no checkpoint exists.

Co-Authored-By: Claude <[email protected]>
…double click opens

BufferView:line_at(y) maps a pixel row through the scroll
offset and the wrap to a source line; BufferModel:
block_at_line finds its block. In navigation a click
selects the block and line; a double click opens it, the
mouse counterpart of Enter. While editing, a click inside
the open block places the input cursor on that line, and
a click outside goes through the acceptance gate exactly
as an arrow transition does: an untouched block leaves,
a changed one is accepted and written, an invalid one
refuses and keeps the editor.

Buffer-area clicks route to the editor through the
console; input-strip clicks keep going to the widget.

Co-Authored-By: Claude <[email protected]>
Text level, inside an open block: EditHistory keeps a
32-step ring of {text, cursor} snapshots taken before each
mutation. Consecutive same-kind edits at the expected
cursor coalesce and a typed whitespace starts a new step,
so Ctrl+Z removes the word just typed — no wall-clock
timers, deterministic and testable. The history is born
with the block (set_text resets it) and never leaks across
blocks or into the file.

Block level, in navigation: a 32-step ring on the buffer
records every file write as a trimmed diff — the common
prefix and suffix are cut, leaving exactly the affected
line range. One recording wrapper (record_write) serves
every write site: acceptance, insertion, deletion, the
reorder move. Applying a step is a splice plus the usual
re-chunk and save; selection restores to the step's
remembered side. The history lives on the buffer, so it
survives a follow-require round trip and dies with the
file; a checkpoint restore rebuilds the buffer, which is
the boundary. A new write kills the redo tail.

Keys: Ctrl+Z / Ctrl+Y in both modes; bare Delete drops a
block in navigation now that it is undoable (2.7), without
touching the clipboard — cutting is Ctrl+X alone, since on
the device every clipboard write pops the share overlay.

Dialogs are repeat-proof by construction, as agreed after
the device pass: the confirming key differs from the
invoking one. Discard (Shift+Esc on a changed block),
checkpoint overwrite and restore all confirm on Enter or
Space and cancel on anything else — the invoking chord and
Esc included — so a held key oscillates ask/cancel and
never fires. One executor runs the confirmed action; the
handlers only raise. A parseable draft discarded with
confirmation leaves a recoverable pair in the block
history: one Ctrl+Z puts it in the file, another takes it
out. A plain error message closes on Enter, Esc or
Shift+Esc without re-submitting, matching the REPL.

Co-Authored-By: Claude <[email protected]>
…editing flag

UserInputModel:backspace_word eats the word before the
cursor together with the whitespace in front of it, as
readline does, on Ctrl+Backspace and Ctrl+W (spec 2.7).

UserInputModel is the whole platform's input — the
console, the project inputs the games read, and search
all build on it — so the editor's extras must not leak.
An explicit `editing` flag on the model (off by default;
only the editor model opts in) scopes them: word deletion
and the refusal frame gate on it, the edit history
records nothing without it, and the widget's Ctrl+Y
delete-line stays for the console (in the editor the
controller takes Ctrl+Y for redo first). Regression specs
pin the contract from the console's side: a flagless
model records nothing, Ctrl+Backspace deletes one
character, Ctrl+W does nothing, Ctrl+Y deletes the line.

Co-Authored-By: Claude <[email protected]>
README.md and doc/EDITOR.md describe the final layout:
line-wise navigation with Ctrl-arrow block jumps, Home/End
to the file's edges, the Alt peek, Ctrl+Enter blocks,
undo/redo, bare Delete, word deletion, the dialog keys,
checkpoints and Ctrl+J. The input widget's Home/End rows
follow the line-scoped fix.

Co-Authored-By: Claude <[email protected]>
Co-Authored-By: dsent <[email protected]>
…gnore rules

Update the top-level README's editor walkthrough to the key shortcuts after the
editor overhaul this branch builds on: Enter opens a block for editing and Enter
sends it back, Shift+Esc leaves the editor, and the deprecated Ctrl+Shift+S is
called out as leaving without the acceptance step. Also marks the doc/mermaid
diagram sources as historical design sketches and updates .gitignore. Docs only;
no behaviour change.
The project-lifecycle tests need to stub the filesystem methods invoked while a
project run tears down; this exposes them as seams so the suite can drive exit
without touching the real filesystem. Isolated to src/util/filesystem.lua.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds doc/input_api.md -- the project-facing guide to compy.input (the precise
surface), the simple compy.ask/on_answer/on_key surface, the four lifecycle
flags, shortcuts and hooks, and a migration table from the removed legacy
globals -- and the CHANGELOG.md CURRENT_SCOPE at user-facing altitude, including
the two breaking changes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The developer documentation corpus under doc/development: the internals notes,
the conventions, the decisions ledger (decisions/input.md), the technical-debt
register (technical_debt/*), and the overview, drawing-system, smoke-checklist
and tests docs. Reference material for maintainers; no shipped code.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The busted specs for the widget surface and its callbacks/lifecycle, the input
routing chain, global shortcuts and clicks, the simple surface, console
fall-through, editor keys, cursor/text handling, combo serialisation, config-key
agreement and the memory NFR, plus shared helpers and fixtures and updates to the
editor, history, model, view and mock specs. Green suite: 1174 / 0 / 0 / 9.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The routing spine: controller.lua dispatch, consoleController, the new
projectInputController, and userInputController. Events walk a chain of
responsibility -- shortcuts, then hooks, then the shown widget -- with the
project run owning its route and the console taking only what nothing consumed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The input widget's model and view: userInputModel (content, cursor, history and
the submit/cancel lifecycle behind the four flags), userInputView rendering, and
the consoleModel touchpoints. Activation no longer destroys content; get_text
reads it back; clearing and hiding are flag-driven at each verb.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Editor key handling sited at route level (editorController), and the editorModel
and searchModel touchpoints, reconciled to the imported editor rework. NOTE: part
of this diff is the editor-rework baseline rather than new work -- see the PR
slices manifest's base-drift caveat.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…mony

main.lua wiring (the widget built at the run seam, key-repeat, before_exit),
src/util/key.lua (the combo and modifier vocabulary), types.lua, the evaluator's
move to line callbacks and validators, and the harmony screenshot-test init.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The in-tree example projects move off the removed polling globals onto
compy.input and compy.ask: turtle, tixy, sapper, guess, repl, valid, clock,
paint and pong, with a held-key fix in pong and README updates. The nested
example repositories keep their own PRs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants