Integr/serial on newapi - #51
Open
Vadim1987 wants to merge 29 commits into
Open
Conversation
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]>
Carries the serial work from integr/serial-on-input77 onto the current head of the input branch as one change, leaving the debugging history behind. What comes across: - model/serial: the facade (onConnect / onDisconnect / onBytes / onLine / send / isConnected), the dispatcher that delivers to the console and program environments, the line reader, and three backends — Android over JNI, a fake for tests, a null one for the desktop. - util/jni: the LuaJIT FFI bridge to the JNI environment that SDL hands out, with the call helpers the backend uses. - The platform port is created once in prepare_env: Android gets the USB backend, everything else the null one. Both environments see it as compy.serial, held as an upvalue in get_compy_namespace like compy.input, for the same reason: the env clone is deep and a copied table would collect handlers the dispatcher never reads. - The port is pumped once per frame from love.update, and a fault it reports is printed with the environment it came from. - Outgoing bytes leave one per poll: the board's Lua REPL is an interactive terminal and drops the tail of anything written faster than about a byte every 2 ms (measured over pyserial, no Compy in the path). A frame is some 16 ms. - tests/serial, 38 specs, green here alongside the branch's own. Left behind on purpose: the robot transport and its probe (a separate line of work), and serial_probe, the console diagnostic that the terminal project's echo() replaces.
stop_project_run tears down the program's input widget and its love handlers; its serial handlers stayed. Nothing called Serial:programEnded, although the facade had it from the start. Now it is called next to clear_user_handlers, so a stopped project's onBytes / onLine no longer fire into the console, and the next run starts with an empty program environment — the same invariant the input widget keeps. The console's own handlers are untouched: they were set from the console and belong to it.
The serial-terminal demo, renamed and rewritten against the new input API: on_text_entered now takes the text flat, and after_submit clears the field while the widget stays open, so there is no re-showing between lines. The board runs the Lua REPL firmware. It ends its lines with CR — with CR CR LF after one it has echoed — and it repeats what it receives, so the terminal shows its echo followed by its answer. Whole lines are printed as they arrive; a prompt or the head of a long answer comes without a line ending, waits a fifth of a second in case the rest is still on the way, and then goes out through io.write, which leaves the line open for what follows. The typed text lands on that open line, where a terminal would put it, and sending closes it so the board's answer starts on a line of its own. Nothing checks what is typed: what is valid Lua is the board's business, and the field would only get in the way.
echo() in the console: on, and what arrives from the board is printed; echo(false), and it is not. The reading was in the micro:bit project until now, and it read badly — a board answers a character at a time, so a chunk is rarely a whole line, and printing each chunk as it lands spells the answer out letter by letter. model/serial/echo holds the two things that fixes. Whole lines go out through print, which the terminal draws at once. What is left over — a prompt, the head of a long answer — has no terminator to wait for and no sign that the stream has ended, so it goes out once the board has been quiet for a fifth of a second, through io.write, which leaves the line open for the rest of it. That wait is why this belongs here rather than in a project: the frame is where the time is, and Serial:update now takes dt and ticks the echo with it. Bytes that are not UTF-8 are dropped. A board greets a fresh connection with a few stray ones, and the terminal takes UTF-8 only — it raises on anything else, which took the whole output down. Defending io.write is the platform's to do (technical debt, registered); until then the port does not hand it anything it cannot draw. io.write and print come in through the constructor, so the specs watch what was said and what was written without a terminal anywhere near them.
The commands around the terminal, to Dani's specification.
main.lua only prints how to start; the tools are loaded into
the console with require("tools") and become commands there:
help() this list
echo(on) what the board says, in the console
send(filename) a project file to the board, as typed
exec(filename) a project file run on the board as one
chunk
echo is the console's own — it needs a frame to time the
tail of an answer by, and a project has no say in that. The
tools switch it on when they load, which is the moment the
board becomes worth listening to.
Typing to the board is the terminal project; this is the
work around it. Files are written with edit(), which the
console already has, and help() says so.
exec has to hold back the board's echo of the file while it
is in flight, but send queues bytes and returns at once —
they leave one per frame. So its handler stays silent until
the board has echoed the closing marker of the wrapper, then
hands the rest back to echo, or leaves it off if it was off
to begin with. The board echoes exactly what it is sent, so
the marker is a reliable end.
A program that shows what the board says has its own handlers, and the console's echo kept its own alongside — so every answer was printed twice, prompts included, and a line came out as "> > print(1 + 1)". The dispatcher already had suspend_env for the mirror case, where a paused program stops being read. programStarted uses it on the console side, called where the run begins; programEnded gives the hearing back. Nothing else changes: the console's handlers are left in place, they just go unread for as long as a program runs.
programStarted mutes the console for as long as a program speaks for the board, and stop_project_run was the only place that gave the hearing back. A project whose top-level code simply finishes never goes through a stop: it lands in project_open straight from the run, and the console stayed deaf until Compy was restarted. The micro:bit project is exactly that shape — main.lua prints a line and returns — so run() silenced echo for good. Two places reach project_open without stopping, and both now say so. A run that falls idle calls programIdle: the program is not stopped, so the handlers it set stay, but it is no longer the one speaking. A run that raised calls programEnded, which clears them — nothing survives the project that installed it, the same invariant the widget and the love handlers already follow there.
The console prepares serial support when it is built
(consoleController.lua), which calls love.system.getOS() to choose a
backend. The shared input fixture's mock love has no `system`, so the
tests/input specs that build a console through it errored ("attempt to
index field 'system'"), taking the suite to 841/0/16/0. Stub
love.system (desktop OS -> null backend), matching the per-spec stubs
already in editor_spec / input_editor_keys_spec.
SUITE: 841/0/16/0 -> 1222/0/0/9 (the 16 errors clear; the serial specs
now run).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AfmgSmapP9mpAYzcPcQhj2
io.write of a byte that is not valid UTF-8 took the whole console
output down: terminal_print's `for i, p in utf8.codes(res_string)`
(terminal.lua) raises `invalid UTF-8 code` on a malformed sequence,
and terminal_print is the single sink for io.write
(redirect.lua -> CanvasModel:write -> terminal:print). One stray byte
aborted the write, lost the valid tail, and propagated up so the
console stopped printing. Reported by Vadim; more reachable now that a
serial board can echo arbitrary bytes.
Defend at the boundary: utf8_clean drops invalid bytes (utf8.len gives
the first bad position) before the codes loop, so no caller can crash
the terminal. This is the policy model/serial/echo.lua already applies
to its own path (and now names as covered here); echo's as_text stays
as belt-and-suspenders and for its CR/LF normalisation.
Breaking test: tests/lib/terminal_spec.lua -- print("A\255B") must not
raise and must queue A and B.
SUITE: 1222/0/0/9 -> 1224/0/0/9 (+2, the new spec).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AfmgSmapP9mpAYzcPcQhj2
…hows Terminal scrolling off-by-one: the last printable character sent via io.write should always be visible, but numbering lines 1-16 hid "16" until a newline was sent. Cause: CanvasModel built the terminal from the full view height (cfg.view.h) while the visible console region is get_drawable_height = h - statusline - input line (util/view.lua). The terminal thus had ~2 extra rows that render under the input line; a final un-terminated line landing there was off-screen until a newline scrolled the buffer up. The intended value was present but commented out at canvasModel.lua. The terminal's own scroll (roll_up/ wrap_if_bottom) is correct -- the last line stays in the buffer; it was simply not on screen. Pre-existing platform bug, not introduced by the input API. Fix: size only the terminal to get_drawable_height; leave the program drawing canvas at full size so canvas/turtle programs are unaffected (this is why the fix is not a wholesale uncomment of that line). Kept full height in sizedebug mode. Not unit-testable headless (a rendering/occlusion fact, no LÖVE compositor in busted); a desktop/device smoke is recommended. SUITE: 1224/0/0/9 unchanged. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01AfmgSmapP9mpAYzcPcQhj2
Vadim1987
force-pushed
the
integr/serial-on-newapi
branch
from
September 11, 2026 10:00
e26d0c8 to
663eb17
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes and includes #45 (editor overhaul) and #50 (new input API) — this branch sits on top of both.
On top of them: a platform USB-serial API (src/model/serial), compy.serial in both environments, the port pumped once per frame, and two example projects — microbit (help, echo, send, exec) and terminal.
Verified on device: the tools load, a board connects, files are sent and run, the terminal types to the board and comes back