feat(core): let a plugin type into a program it started - #1117
Conversation
`command("program", { text = …, keys = "…" })` sends bytes to the pane's
stdin as if they had been typed at it. The third thing a plugin can do to
its own pane, after starting and closing it, and the one that lets a
long-lived program be *told* something rather than replaced.
Restarting an editor to open a second file is the case that asked for it.
Starting is idempotent, so asking again with different `args` does
nothing — the pane is already there — and the only way to change what a
program was showing was to close it and pay for the process again.
Sent *with* a program it means "type at it, or start it if it is not
running", and the coordinator picks: whether the pane is alive is not
something a plugin can read. The keys are not sent to a program it just
started, because a process that has not begun reading would lose them.
On its own it starts nothing and says so when the pane holds nothing
live, rather than silently dropping the bytes.
Greptile SummaryThis revision fixes the previously reported byte-loss issue by preserving Lua strings as raw bytes throughout the program-input path.
Confidence Score: 5/5The PR appears safe to merge; the previously reported non-UTF-8 byte-loss defect is fixed and no new actionable issue was found. The Lua boundary now reads
|
| Filename | Overview |
|---|---|
| src/kernel/host/api.rs | Reads the keys Lua string directly as bytes, preserving non-UTF-8 input. |
| src/kernel/command/mod.rs | Carries program input as Vec<u8> and retains validation for empty, closing, and fallback commands. |
| src/coordinator/commands.rs | Delivers byte slices unchanged to an existing plugin-owned program pane. |
| tests/plugin_programs.rs | Adds regression coverage showing that a Lua string containing 0xff survives command construction. |
| docs/PLUGINS.md | Documents that keys preserves arbitrary Lua-string bytes. |
| ui/lib/thurbox.d.lua | Extends the Lua program-command declaration with the keys field and fallback semantics. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Plugin Lua byte string] --> B[LuaString]
B --> C[Command::Program keys: Vec<u8>]
C --> D[Coordinator]
D -->|pane running| E[Program stdin]
D -->|pane absent and repo supplied| F[Start program without sending keys]
D -->|pane absent and no repo| G[Report error]
Reviews (2): Last reviewed commit: "fix(core): carry a program's keys as byt..." | Re-trigger Greptile
`keys` is documented as bytes reaching a program's stdin as if typed, but
it was read off the Lua table through a Rust `String` with the conversion
error swallowed: `t.get::<Option<String>>("keys").ok().flatten()`. A Lua
string is free to hold a sequence that is not UTF-8 — an escape for a
program that speaks its own encoding — and such a field vanished whole.
With `repo` also given, the command then read as "start it" instead of
"type at it", and nothing reported anything.
Read it as an `mlua::LuaString` and carry `Vec<u8>` through `Args`,
`Command::Program` and `apply_program`. The bottom of the chain already
wanted bytes: `send_to_program` takes `Vec<u8>`.
The new test drives the real Lua boundary — a plugin writing
`keys = "\27\255q\r"` — and fails without the fix.
LeTuR
left a comment
There was a problem hiding this comment.
Reviewed at df39565a. This is the keys half of the split #1107's review asked for. Nothing here blocks landing it: the four inline comments are all nit:.
The contract question. It is settled for both sides it named. With keys alone and a missing or exited pane, the failure is reported. With keys and a program, the program starts from args, the keys are not sent, and start_program replaces an exited slot, so the keys never land in a pane the plugin did not expect. One narrow path is still silent, raised inline: a live pane whose send fails.
CI on df39565a. 15 passed, 0 failed, 0 cancelled, 0 held. 7 were skipped by path filters: the two install-script jobs, winget, website lint, ShellCheck, SonarQube and PR Title Checker. Nextest and Windows tests and clippy ran. No job sends keys through a real tmux or psmux pane.
Security. No security findings. The owner is stamped from the running plugin, never read from the options table, and trust is checked again when the command is applied. The bytes reach the program's pty through send-keys, and only its output returns, through thurbox's vt100 parser, so nothing reaches the host terminal. Program panes are local only, so SSH and WSL are never reached. The only bound is tmux's 512-byte chunking, but a plugin granted program can already run anything, so typing adds no authority.
The nits are yours to take or leave; merging is the maintainer's call.
— LeTuR's agent
|
Three things break in the editor-tab call from this PR on real nvim 0.12.5 (plain
Per-case output
— LeTuR's agent |
The second of the two APIs from #1107's
4a2cacb, rebased onto currentmain. The restored files are byte-identical to what was taken out; everything else comes frommain.What it is
command("program", { text = …, keys = "…" })sends bytes to the pane's stdin as if they had been typed at it. The third thing a plugin can do to its own pane, after starting and closing it, and the one that lets a long-lived program be told something rather than replaced.Restarting an editor to open a second file is the case that asked for it. Starting is idempotent — asking again with different
argsdoes nothing, the pane is already there — so without this the only way to change what a program was showing was to close it and pay for the process again.The contract, and the silent failure you named
You wrote: "'Send keys, or start the program if the pane is not there' is a contract with a silent failure mode on both sides — the keys are dropped when the pane turns out to be missing and the fallback start puts the program somewhere the keys would not have, and a plugin cannot tell which happened."
Here is where that landed:
keysalone, pane holds nothing live → it starts nothing and says so. Not a silent drop: the plugin gets the failure back.keyswith a program → the coordinator picks, because whether the pane is alive is not something a plugin can read. If it had to start the program, the keys are not sent: a process that has not begun reading would lose them, and a lost keystroke is worse than one never sent.The consumer you asked to see
It exists and it is in use daily, but it is not in
ui/plugins/— it is in my own interface directory, and it cannot be bundled as it stands. I will say why below. This is the whole call site:What it shows about the API, which is what you wanted to judge:
keyscannot help with that and does not pretend to — which is also why I would not hold this API up as an editor-opening feature.:confirmrather thane— the failure that matters is not the API's, it is a refusal the interface would then contradict.program.exited(merged in fix(core): announce a program pane's exit and give each window the remain-on-exit it needs #1107), because a pane that ends must put the tab back; the two features are each other's other half.start_programhad been replacing an exited slot all along, and what the close bought was a fresh nvim per file, which was the visible lag.Why it cannot be bundled as it stands
The path comes from a file tree: the tree emits
user.openfile, the centre pane catches it and calls the code above. There is no tree inui/plugins/— mine is 1447 lines, plus 326 for the right-button menu, on top of a centre pane 576 lines past the bundled one. A bundled editor tab would be an editor with nothing to open, because nothing in the shipped interface supplies a path.And the key string is nvim's, not an editor's:
:confirm eis vim syntax, so "the bundled editor tab" would either hardcode nvim or need editor detection — a design argument that would drown this review.So: the honest unit for the bundle is a tree plus an editor tab together, which is a feature proposal, not an API review. Happy to open that conversation once this lands, or sooner if you would rather see them as one thing — but I did not want to make that decision inside a PR about a command field.
Checks
a_program_command_can_type_into_a_pane_it_already_startedcovers the shape: typing names a pane that is already running, so it asks for no program, and it is not a way to close one.cargo nextest run --all: 2642 tests, failures only the four that reproduce onmainin this environment.fmtandclippy --all-targetsclean.on_contextis #1116, independent of this one.