`tty7 doctor` is described as checking "socket, dialect, config,
versions, agent hooks, links, context". With a `config.json` that does
not parse it printed every row green: server ok, dialect ok, status ok.
Meanwhile every setting in that file was being ignored, a copy had been
kept beside it, and saving was suppressed.
The rows that mention config report `TTY7_CONFIG_DIR` — where the file
should be, which is a different question from whether it is being read.
There is now a row for the answer, from the same `LoadOutcome` the
loader already returns: ok, none yet, not valid JSON, unreadable.
It also exits 1 for the two broken states, for the reason written above
the server check: doctor is the verb people run when something is not
working, and `tty7 doctor || alert` has to fire. A config nothing reads
is that, as much as an unreachable server is.
All four states run against a live server: ok, none yet, NOT VALID JSON
(rc 1), and back to ok after repairing the file with no restart.
Swept every snake_case name cited in a comment against the names that
actually appear in code, after nearly committing an invented test name
last commit. Twenty-four came back; twenty-two were fine and only look
wrong from inside this repo — serde's `deny_unknown_fields`, gpui's
`set_maximized`, nu-path's `configurable_dir_path`, git's own
`wt_status_print_state`, zsh's `share_history`, Zed's `paint_layer`.
`render_idle` is real too: it names the `render_idle_gpui_tests` modules,
which a word-boundary search does not match.
The two that were rot:
`read_machine` is `load_machine`, and the sentence around it describes
that function exactly — quarantines the file, hands back an empty tree.
`pane_attachable` was deleted by 412bfcfc, the same commit that wrote
the comment naming it. It meant "the aliveness check we are not doing
here", so it now says that instead of naming a function nobody can look
up.
Both were only findable by comparing comments against code, which is
worth knowing: rustdoc checks `[`links`]` and says nothing about plain
backticks, and these had survived every gate in the repo.
One line in config.json can go wrong two ways, and they were reported in
two shapes:
keybinding for unknown action "NewTabb" ignored
ignoring keybinding for 'NewTab': invalid keystroke 'cmd-shft-t'
Same news about the same setting, quoted differently and built
differently. They now share a shape, so a reader scanning a log does not
have to notice that they belong together.
The third message in this file is a different audience and now says so.
It fires only when the default list offers an action `make_binding`
cannot build — `set_binding` has already turned away anything a config
misspelled — which is a disagreement between two lists in this file, not
a mistake in anyone's config. `every_dispatchable_action_has_a_slot_to_bind_it_in`
exists to keep that from happening; if it ever reaches a user, the log
should not blame their config for tty7's inconsistency.
Both guards verified against a running GUI before and after: a bad
action name and a bad keystroke each named, the good binding beside
them silent.
Nothing else needed here. Unknown actions and invalid keystrokes were
already caught — the same treatment the last two commits gave config
values and unknown keys, arrived at independently in this file first.
A `config.json` holding `font_siz` behaves exactly like one holding a
setting that does not work: the value is dropped and nothing is said.
Confirmed before changing anything — a config with `font_siz`,
`totally_made_up` and an out-of-range `ui_font_size` produced one
warning, about the range.
Unknown keys are ignored on purpose and that stays: a retired key must
not make a whole config unreadable, which
`a_config_still_carrying_the_retired_theme_key_loads` pins. Ignoring a
key and never mentioning it are different things, though, and only the
second one leaves someone staring at a setting that looks right.
The known set is the serialised default rather than a hand-written
list, so it cannot drift from the struct. That trick only holds while
`Config` skips no field on the way out, so the test asks each field
individually whether it would be called a typo — a skipped one would be
reported to its owner as a misspelling, and this fails the day that
becomes possible.
Behind `log_enabled!`, because it parses the file a second time and this
path reloads per pane spawn. Verified: `font_siz` and `totally_made_up`
each named, beside the clamp warning from the last commit.
A hand-edited `config.json` asking for `ui_font_size: 8`, `font_size:
999` and `scrollback_limit: 5` runs at 12, 256 and 100. Both halves of
that are right — clamping beats refusing the whole file over one silly
number, and the file is deliberately not rewritten, the same restraint
the loader already shows toward a config it cannot read. What was
missing is that nothing said so. The file keeps claiming 8 and the app
keeps running at 12, and the two never meet.
Each correction now names the field, what was asked for and what is in
force. The `FontFeatures` parser in this same file has always logged a
tag it could not use; a value quietly replaced is no easier to work out
than a tag quietly dropped.
Logging is off unless `TTY7_LOG` asks for it, so this is silent for
everyone who is not looking — which is what makes it safe to put on a
path that reloads per pane spawn.
Verified against a running server: three warnings, one per clamped
field, each with both numbers, and `config.json` untouched afterwards.
Auditing my own earlier documentation rather than the code, on the
grounds that three defects have already come out of it. This one held
up, and checking it turned up something adjacent.
The restored `CAPABILITY_ENV` comment claims a user's `env` map must not
override `TERM` or `COLORTERM`. Verified by running rather than reading
this time: a config with `TERM=hacked-term`, `COLORTERM=hacked-color`
and `RALPH_OK=yes` produced a pane with `TERM=xterm-256color`,
`COLORTERM=truecolor` and `RALPH_OK=yes`. The filter is exact, and the
rest of the map is applied as written.
Right, and undocumented anywhere the person writing that config would
look: `env` has no entry in the settings docs, and the field had no
comment. Someone who sets `TERM` there gets no warning and no effect.
The reason it is refused belongs next to the field, so it is written
down where the schema is read.
Also checked, and correct: the `ws attach` note from 32f893b describes
`host/server.rs`, and that is the only handler of `WorkspaceAttach` —
the local daemon serves the control dialect through it too, so the note
applies to the local case it was written for.
In 335d982 I gave `tty7 wait %3 --until free` as the form to use for a
plain command. `--changed` is what makes that safe after a `send`, and I
left it out.
The states are levels, not edges. A pane that has not yet started what
you just sent it is still `free`, so a wait that gets there first is
answered by the state the pane was already in — the flag's own comment
says exactly this, and it is why `--changed` exists. The example now
sends and waits the way a delegation loop actually runs.
Being straight about the evidence: I did not reproduce the race. Sending
a six-second command and waiting without `--changed` blocked the full
six seconds, because the shell had gone busy before the second process
started. The window is small and the failure is intermittent, which is
the worst kind to leave in an example — not a reason to call it fine.
`--changed` itself is correct, and now checked: `--until free` on an
idle pane returns at once, while `send` then `--until free --changed`
holds for the five seconds the command takes and the output is there
afterwards.
`stop` is described as "Stop the server; sessions end". `restart` was
"Stop, then start" — the same destruction, undisclosed, on the verb
people reach for casually when something seems stuck.
Measured rather than assumed: two panes before, zero after, workspaces
still there. So the layout survives, because the tree is on disk, and
what those shells were running does not.
The long form also separates this from the thing it will be confused
with. The server can hand panes to a new build of itself with their pids
and ptys kept — that is a real wire request (`ClientMsg::Handoff`, into
`hand_over` and `handoff::take_over`), and it is how an update keeps
sessions. `restart` is not that path, and someone who has read about one
should not assume the other.
`tty7 wait %N` on an idle shell blocks for as long as you let it. The
default states are `waiting`, `done` and `exit`; a plain shell with no
agent is `no-agent` and reaches none of them, so the wait has nothing to
wake for. Verified with a bounded run: still blocked at twelve seconds
against a freshly created pane.
Blocking is the contract — the verb is `wait` — so what was missing is
that choosing the wrong state is a hang rather than an error, and that
`--timeout` has no default to fall back on. Both are now said where
they are read: the trap in the command's help, the absence of a default
on the flag itself.
This is the same shape as the stdin note in the last commit. An agent
orchestrating panes has two ways to stop forever with no diagnostic, and
in both the software is doing exactly what it was asked to.
`tty7 run -- /no/such/binary` answered "no such shell on this machine".
The check in front of a spawn serves both the configured shell and a
command handed to `run`, and its three sentences all said "shell" —
so someone who typed `tty7 run -- ./build.sh` was told tty7 had
misunderstood what they asked for. They say "program" now, which is
true of both callers; the CLI already prefixes "spawning `…`" with the
context.
The other half is worse and is documented rather than changed:
`echo hi | tty7 run -- cat` never returns. The command reads the pane's
terminal, which nothing is typing into, so it waits for input that
cannot arrive — and there is no error, because an idle terminal is not
a failure. Output streams back; input does not go the other way.
Measured, after a first attempt sat for ten minutes: a bounded re-run
with `head -1` was still running at ten seconds with the pipe unread.
For a CLI whose stated audience is agents, `cmd | tty7 run -- …` is a
natural thing to try and an unbounded wait is the worst way to answer
it. Forwarding stdin would be a new capability; saying so is not, and
`sh -c 'cat < input.txt'` gets the input there today.
A pane running an editor or a pager answers `capture` with that
program's screen, and the scrollback behind it is not in the reply.
Measured: with a pane on the alternate screen the capture holds the
alt-screen marker and not the main-screen one; after leaving it, the
main scrollback is back and the alt content is gone, with only the
echoed command left behind — which was typed on the main screen and
belongs there.
That is right, and it is the sort of right that reads as a bug from the
outside. An agent that captures a pane mid-`less` gets a screenful and
no history, and the obvious conclusion — output was lost — is wrong. So
the help says which of the two you are holding, and that an
empty-looking capture is usually a TUI in front of the scrollback.
Also noted there: long output is trimmed from the oldest end, so a
capture after a big build gives the end of it. `seq 1 200000` through a
pane comes back as the last ~70 KB, ending at the prompt.
Checked while here, and correct: `send` carries an 8000-character
payload with nothing truncated.
`lines.last_mut().unwrap()` appears six times while laying out the
inline editor, the furthest of them seventy lines from the thing that
makes it safe: `lines` is seeded with one row and rows are only ever
pushed. A reader meeting the unwrap first has to go looking, and a
reader adding a `pop` or a `drain` has nothing to warn them.
Found by sweeping the tree for panicking arithmetic and unwraps rather
than by anything failing. Everything else that sweep turned up is
already guarded, and the guards are worth recording as checked:
- `heal_active` in tree_sync returns early on an empty tab list before
`tabs.len() - 1`, matching the `ActiveTabChanged` contract that a
workspace with no tabs simply has no active tab.
- The scrollback ring's `bytes[bytes.len() - RING_CAP..]` sits behind
`if bytes.len() >= RING_CAP`, and its neighbours use saturating adds.
- The tab-title eliders bound every head against `cells.len()` before
subtracting, and the binary search's `mid - 1` cannot underflow
because `mid` is at least 1 whenever `lo < hi`.
Checked against the code, not pasted:
- `LayoutResync` is the server saying a mirror is unrepairable, and the
half worth having is the ordering: it arrives *instead of* the deltas
the queue was still holding, not ahead of them. Confirmed in
`spawn_layout_forwarder`, which drains the channel before announcing —
those deltas are older than the gap and already inside the tree the
client is about to pull, so replaying them would walk it backwards.
- `deadline` is per-method because the spread is real, and a timeout
**never drops the connection**: the request fails, a `CANCEL` goes
out, every other in-flight request is untouched. The timeout arm still
does exactly that.
- The `kind` bytes overlap between directions on purpose — HELLO and
HELLO_OK are both 60, REQUEST and RESPONSE both 61, and so on — which
reads as a copy-paste error until someone says it is deliberate. 64-69
are still free, as the note claims.
`handoff.rs` prompted this: it reads far better than its neighbours, and
the reason is that it postdates the comment strip. The gap is historical,
not a matter of standards — restoring is bringing older files up to what
the newer ones already do.
I left the date off in b77da67 and gave a reason: that turning epoch
seconds into a civil date would mean hand-rolling calendar arithmetic,
and getting it subtly wrong was worse than making the reader take the
day from the file's mtime.
That was wrong. `core::crash` has had `civil_from_days` all along —
Hinnant's algorithm, with a test covering the epoch, a leap day and the
day after it — and `crash.log` has been writing `YYYY-MM-DD HH:MM:SS
UTC` the whole time. So the two logs in one config directory disagreed
about how to stamp a line, and tty7.log was the one that could not say
which day it meant.
Now `2026-08-16 00:27:21.565Z`, from the same helper, which is also what
the test checks it against: the two files must not disagree about the
date.
The old test asserted "the first two characters parse as an hour under
24". With the date in front that is true of `2026-…` too, so it would
have passed whatever this produced. It now reads the fields.
`quarantine_path` ends in
candidates.find(|c| !c.exists()).unwrap_or(base)
which reads as a defensive branch for a case that cannot happen. It is
the wrap. Eight is a cap on how much of a repeatedly-corrupted file's
history a config directory may accumulate, and once every name is taken
the oldest copy is the one overwritten.
The part worth writing down is what that leaves behind: nothing rotates
the numbered names, so from the cap onward `.corrupt.1` through
`.corrupt.7` hold the second through eighth corruptions forever and only
the base name moves. Read as a timeline — which is how someone
recovering a tree would read it — that is the opposite of what it looks
like.
Pinned rather than asserted: nine corruptions, eight files left, the
ninth landing on the base, and the base holding the ninth's contents.
Found by sweeping numeric constants for ones whose *value* is
load-bearing. Most of the 367 undocumented ones are self-evident (font
size bounds, test timeouts); this was the one where the number decides
what a user can still recover.
`!cfg!(target_os = "linux")` decides whether printable keys go through
the IME. Bare, it reads like an arbitrary platform carve-out. It is a
recorded trade-off: gpui's IBus integration does not reliably commit
plain ASCII back through `replace_text_in_range`, so routing printable
keys to the IME on Linux swallows ordinary letters — the key never
reaches the terminal, while Enter, Tab and the arrows still work because
they bypass the IME as non-printable keys. CJK composition is given up
on Linux to keep typing working at all.
That is the worst kind of line to leave unexplained. It looks like an
inconsistency worth tidying, the tidy is one character, and the result
is a Linux build where nothing types. I cannot build Linux here, so the
comment is the only thing standing in front of it.
Also the two symlink-cycle tests, which is the pair worth having
together: neither walk detects a cycle, and they end for different
reasons. The tree search has a whole-walk directory budget and keeps
what it found; a copy stops at `MAX_DEPTH` and reports an error,
because 64 levels of a silently copied loop is not a copy anyone asked
for. Only the search one had a comment to restore — the copy test
postdates the strip and never had one, so its half is written from what
the code does now, checked at `copy_tree`'s depth guard.
if cfg!(target_os = "linux") { window.zoom_window() }
else { window.titlebar_double_click() }
reads like a stylistic split and is a bug fix. gpui implements
`titlebar_double_click` on macOS only — everywhere else the trait method
is an empty default, so on Linux this row swallowed the double-click and
nothing zoomed. `zoom_window` is the maximise toggle there, and what
gpui-component's own `TitleBar` calls for the same reason. Windows needs
neither: the row maps to HTCAPTION and the OS has already acted.
Nothing in the code said any of that, so the obvious tidy — collapse the
branch to the one gpui documents — silently breaks Linux again. The
branch is byte-for-byte what it was when that was written.
Also `WindowMoveArm`, whose point is *where* the flag lives rather than
what it holds, and `last_focused`, which is why switching tabs returns
to the pane you left instead of the first one.
Checked `last_focused`'s reference to `remember_active_pane` before
restoring it, on the assumption it had rotted — it has not, the function
is still there and still called from two places, and `focus_target`
still falls back to `first_leaf()` exactly as described. My first grep
said otherwise because I had truncated it.
Measured function length across the tree while here: the only
production function over 400 lines is `Tty7App::render` at 578, which is
a gpui element tree — long, linear, and not something to split while
its behaviour cannot be verified visually.
`pane ls` reports PANE/WS/TAB/CWD/LIVE and `pane ls --all` reports
PANE/WS/OWNER/CWD/LIVE. The swap is right — a pane no workspace holds is
in no tab, so a TAB column would be empty for exactly the panes `--all`
exists to show — but nothing said so, and it is the sort of difference a
reader assumes they misread.
It also costs something specific: the @ numbers. A "no tab @7" error
sends the reader to `tty7 pane ls`, and reaching for `--all` there, as
one does when a thing seems to be missing, takes the column away.
Verified while here, and all correct: `--key C-c` interrupts a running
command (the `sleep` is gone and `procs` shows the shell back in the
foreground); `send` delivers quotes, backslashes, `$`, backticks, braces
and pipes verbatim; `-q` silences all twelve listing and creating verbs,
still prints errors, still exits 1, and wins over `--json` on success.
Same shape as the working directory, and the same surprise:
RALPH_MARKER=hello tty7 run -- sh -c 'echo [$RALPH_MARKER]'
[]
The pane is the server's child, so it gets the server's environment.
`FOO=bar tty7 run -- …` is a universal idiom and it silently does
nothing here, with no `--env` to reach for instead.
Documented beside the `--cwd` note, with the workaround that does work
— set the variable inside the command, where the shell running it can
see it. Adding `--env` would be a new flag, not a fix.
Checked while here, and correct: a pane does get a usable environment
(PATH, HOME) from the server; `$TTY7_PANE`, `$TTY7_WS` and
`$TTY7_CONFIG_DIR` are all set in a `run` pane as the top-level help
promises; and the payoff holds — `tty7 procs` with no address, run
inside a pane, resolves `$TTY7_PANE` and reports that pane's own
processes.
It does not run where you typed it. The pane is the server's child, so
it starts in the server's working directory — whatever that process was
launched from. Measured, not assumed: `tty7 run -- /bin/pwd` from /usr
and again from /tmp both answered with the directory the server had been
started in.
The help's own example is `tty7 run -- cargo test`, and a server the app
started is not sitting in your project. So the example as written builds
somewhere the reader did not choose. It fails loudly for a build — no
Cargo.toml — but not for anything that would happily run in the wrong
tree.
Documenting rather than changing it. Defaulting a local run to the
caller's directory is the behaviour most callers expect and would make
the example true, but it is a different result for every existing
invocation, and `-m` routes to a machine where the local path means
nothing. That is a decision to take deliberately, not a side effect of a
docs pass.
Checked while here, and correct: `split` inherits the split pane's cwd,
and a pane's reported cwd follows the shell through `cd`.
Twelve `tab new` at once against one workspace, then check that every
tab landed exactly once and that the tree's pane count still matches the
registry's.
Both invariants this leans on are invisible to a single-threaded test.
The store takes `notify_order` before its state lock and holds it across
delivery, so subscribers see mutations in the order they happened; and
each `tab new` spawns its pane before the tree is asked to hold it, so a
refusal in between leaves a pane running that nothing references. That
second one is quiet — it shows up only as these two counts disagreeing,
which is what `pane close --orphans` exists to mop up.
Verified by hand first: twelve racing creates, and a mixed race of
splits, tab creates, renames and closes, both left the counts equal with
no orphans; the same held with a GUI mirroring the changes live, whose
log recorded no resync or divergence. This is that check, kept.
Also measured while here, and sound, so it is not re-run: 180 pane
create/close cycles move the daemon from 10 fds and 5 threads to 13 and
7, and then stay there across two further rounds — one-time overhead,
not a leak.
Read as oracles first, on the theory that a comment stating a rule is a
test of whether the code still follows it. All four still do:
- `completion_generation` is the guard against a background generator
landing its results in a session the user has since closed or
replaced. Bumped on open and on close, and checked before the result
is used — `self.completion_generation != generation` is still there.
- `link_modifier_down` exists because mouse events can lag or omit the
modifier while a mouse-tracking TUI is foreground. Both hover sites
still read `mods.secondary() || v.link_modifier_down()`, so neither
depends on the move event's snapshot alone.
- `scroll_frac` is the sub-line part of the scroll position that makes
trackpad scrolling continuous, and is still reset at every path that
jumps the view.
- `scroll_debt` accumulates sub-line wheel deltas instead of rounding
each one away, which is what stops slow scrolling from never moving:
`total - lines` is still how it is spent.
`scroll_debt` was also the target of a doc link from `zoom_debt`
("kept apart from [`scroll_debt`]") pointing at an undocumented field,
so the pair reads as a pair again.
All four re-verified against the code rather than pasted back:
- `notify_order` is a `Mutex<()>` guarding nothing a reader can see, and
it is the reason subscribers apply history in the order it happened:
the state lock orders the mutations, but deltas go out after it is
released, so without this one writer's deltas can overtake another's
and leave every mirror on the losing state with no error to prompt a
re-pull. Taken at line 1067, before the state lock at 1071, and still
held when `notify_all` runs at 1093 — exactly what it claims.
- `MachineStore::open` is infallible on purpose: a machine whose tree
file is missing or unreadable still has to serve panes and files, and
an unparseable one is set aside as `machine.json.corrupt` first.
- `set_liveness_probe` exists because a seed used to enter the registry
`live: true` unconditionally, and a pane that died between its spawn
and its adopting operation had its death dropped — `note_pane_facts`
still returns early for a pane the tree does not hold, so nothing
would ever have flipped the record back.
- `ActiveTabChanged` is emitted for implicit changes too, so a mirroring
client need not re-implement the heal rule. Confirmed at all five
emission sites, and the one case it deliberately omits already has a
test: "losing the last tab needs no ActiveTabChanged".
Also swept for the class found last commit — a reply whose effect dies
with the connection — and found none left: the CLI speaks no watch verb,
and the server's teardown clears watches, releases workspaces and
unregisters the GUI on every exit path, EOF and protocol errors included.
`tty7 ws attach <ws>` answers `{"attached": "<id>", "took_over_from":
null}`, and `tty7 ws ls` shows the workspace unattached a moment later.
Both are correct: an attachment belongs to the connection that made it,
and this command's connection ends with the command.
A caller has no way to tell that from the reply. An agent reading
`attached` reasonably believes it now holds the workspace, and the next
`ws ls` says otherwise with nothing to explain the difference.
So say it. The claim does not outlive the process, that is not a
failure, and what does last is the displacement: the previous holder has
been told, and a dedicated one has been hung up. `took_over_from` is the
part of the reply worth acting on.
Behaviour unchanged — this is the same reasoning the `Attachment` type
already carries ("an attachment belongs to a live connection, and one
read back at boot would name a holder that no longer exists"), said
where the person running the command can see it.
Also checked while auditing, and sound: `tty7 <PATH>` already stats the
directory and refuses a file (`resolve_gui_path`), `ws stop` says "(not
implemented yet)" in its own help rather than only when run, and repeat
`ws detach` is idempotent on purpose for wire-compatibility reasons the
handler documents.
Auditing my own earlier fix, the way the last commit's lesson says to.
`run --cwd` and `new <path>` were fixed together and `tab new --cwd` was
missed, so it went on starting the tab's shell in whatever directory the
CLI happened to be run from and answering with a pane id:
tty7 tab new <ws> --cwd /nonexistent-xyz
%2
with `pane ls` then showing %2 rooted in the CLI's own directory.
The check is one function now, and the three verbs call it, which is
what should have happened when there were two of them. It refuses before
anything is spawned, and only judges paths on this machine — a routed
`--cwd` belongs to the far side's filesystem.
Pulling it into one place dropped the path from the sentence for a
moment ("--cwd: no such directory", naming the flag but not which of the
reader's paths was refused). The flag is optional in the message and the
path never is, since `new <path>` has no flag to name.
The sweep added last commit knew one prefix. `throwaway_dir` hands out
four — zsh's ZDOTDIR, and a scratch directory each for bash, nushell and
WSL — so bash's had gone on piling up untouched the whole time zsh's
were being cleared. There were 607 of them here against zsh's 3,850,
from the same killed daemons.
The prefixes now live in one list that the sweep reads, and the three
call sites take their prefix from it instead of spelling a literal. A
shell added to `setup_*` is swept the moment it takes its directory the
same way, and a prefix defined without being added to the list fails a
test rather than quietly leaking.
Verified by planting a directory for each of the four prefixes under a
pid that is not running, starting a daemon, and finding all four gone.
The living-owner guard still holds.
Every zsh pane gets a throwaway ZDOTDIR under the temp directory, four
redirector files in it, and the pane's own teardown removes it — stop a
server cleanly and it leaves none. A server that is killed never runs
that teardown, and nothing ever goes back to look: this machine had
3,850 of them, from months of crashes and `kill -9`s.
Same shape as the socket a killed daemon used to leave behind, and the
same answer: a later startup is the only thing in a position to notice,
so it sweeps beside the endpoint cleanup.
Timid on purpose. A directory goes only when the name is exactly ours,
the pid in it parses, and that pid is not a live process — so a running
daemon's directories are never touched, and a pid since reused by
something else just waits for another day. Verified after the sweep:
3,850 down to 14, and every one of those 14 belongs to a live process,
the installed tty7.app's daemon among them.
`process_alive` was wrong about that, which the test caught. `kill(pid,
0) == 0` is only half the answer: `EPERM` means the process exists and
belongs to someone else, and reading that as dead would have had the
sweep delete a live owner's directory. Both callers wanted the other
reading — one decides whether to clean up after a daemon, the other
whether to delete its files.
The two tests that bind an endpoint now hold a lock while they do.
`set_config_dir` is first-wins, so every test in the process shares one
socket path, and the one added here made the pair flaky together while
each passed alone.
Log lines read `23:00:24.563` on a machine whose clock said `07:00:24`.
The stamp is seconds since the epoch folded into a day, so it is UTC,
and nothing marked it — the line looked eight hours stale rather than
eight hours offset, which is the wrong conclusion to reach while reading
a log to work out when something happened.
One `Z`. UTC is the right choice for a daemon that outlives sessions and
can be read from anywhere; being unlabelled was the defect.
Still no date, and now the comment says why: turning epoch seconds into
a civil date is calendar arithmetic this crate would have to hand-roll,
and getting that subtly wrong is worse than a reader taking the day from
the file, whose path `server logs` prints directly above the lines.
Verified against a running server: the line now reads `23:02:15.701Z`
with `date -u` at `23:02:17`.
Nothing else came out of this sweep, which is worth recording: the
remaining `(s)` spellings are all either log lines or the *seconds* unit
in `Keepalive interval (s)` — a blind pass over them would have been
wrong. The daemon also survives malformed wire input intact (empty,
garbage, a 4 GB length header, unknown kinds, a 100 KB burst, and 200
junk connections) and leaks no threads doing it.
Three lines still wrote `pane(s)`: the partial-close report, the
hang-up failure, and the orphan note. All three are reachable with a
count of one, and all three are what a reader meets when something has
already gone wrong — a poor moment to look unfinished.
`pane(s)` is the form a codebase uses when it has not decided, and this
one has: the GUI counts through `t_plural` with one/other branches, and
`pane close` narrates a batch only when there is a batch. These were
what was left.
I said in the doctor fix that "1 panes" was the last of these. It was
not — I had swept the success paths and the format strings that spell
the word out, and these spell it `pane(s)`, so the search missed them.
Checked against a live server: "closed 1 pane; 1 could not be closed"
for one, "closed 0 panes" and "2 could not be closed" for the rest.
Following the shape found in the split fix to the other two verbs that
have it. Every verb adding a pane spawns it first and files it second,
because the seed carries the daemon's own pane id — there is no other
order. A refusal in that gap ended the command with a shell running that
no tree referenced: absent from `tty7 ls`, present only in `pane ls
--all`, and collectable only with `pane close --orphans`.
`tab new` and `new <path>` both had it. Confirmed rather than assumed:
the mock answers `TabCreate` with a reply `tab new` cannot read, and the
command left `spawned=1, killed=[]`.
One guard for all three sites now, since the ordering is forced and so
the window is permanent. `new <path>` also takes back the workspace it
made on the way in: it holds nothing, and leaving it adds a row to
`tty7 ls` the caller never asked for.
The mock grows a way to fail a chosen control call. Both cleanup paths
are reachable only after something has already been created, so there
was no way to test them from outside.
Verified against a live server that the ordinary paths are unchanged:
`new`, `tab new` and `split` leave the tree and the registry agreeing at
three panes, and `pane close --orphans` finds nothing.
`tty7 split` spawns the shell and only then asks the tree to hold it, so
any refusal in between leaves a pane running that nothing references —
`pane ls --all` shows it, the tree does not, and `pane close --orphans`
is the only way to be rid of it. Two attempts at `--ratio nan` left two.
NaN is the case that gets there, because it cannot be serialised onto
the control connection at all. The link drops mid-request, so the
daemon's own "a split ratio must be a finite number" never comes back:
the caller sees "control connection lost (request 2)" instead, which
says nothing about the ratio, and the pane is already running.
Refuse a non-finite ratio before the spawn, in the daemon's own terms.
Its clamp to a usable range is deliberate and stays its business — 0,
1 and 5.0 still land on the clamp, as intended; this only refuses what
the daemon would refuse anyway.
Then take the pane back down if the split fails for any other reason
too. Nobody asked for a pane that no tree holds, and leaving it for the
user to find with `--orphans` is not a refusal, it is a mess.
Verified against a live server: two refused splits now leave the pane
count where it was, and a valid `--ratio 0.3` split still works.
Swept the rest of the value-taking flags while here, all sound: `-m` on
an unknown machine, `--key` on an unknown key (which lists the real
ones), `--timeout 0` (exits 124, the conventional code) and a negative
timeout are each refused with the right message.
tty7 run --cwd /nonexistent -- /bin/pwd
/Users/thomas/repo/wt/ralph-wc
Exit 0, no warning, and the command ran wherever the CLI happened to be
started from. `tty7 run --cwd ~/porj -- make` builds the wrong tree and
reports success — and the caller most likely to typo a path is a script,
which has nothing to notice it by.
The daemon falls back to a directory that does resolve when the one it
is handed does not. That is right for a cwd inherited from a pane's OSC
7, which is only as good as the shell that reported it, and wrong for
one typed on the command line: an explicit `--cwd` is an instruction,
and one that cannot be carried out has to stop the run rather than be
quietly replaced. Same for `tty7 new <path>`, which rooted the workspace
in the CLI's directory instead and handed back an id as if it had not.
Both refuse before anything is spawned or created — a refusal after the
spawn leaves an orphan pane, and after `WorkspaceCreate` an empty
workspace to clean up. "No such directory" and "not a directory" are
told apart, because they read differently to whoever typed the path.
Only judged for this machine. A `-m` path belongs to the far side's
filesystem, which this process cannot stat — the reason `agent_hooks_state`
already gives for refusing to answer config questions when routed. Asking
the backend, so the two stay consistent.
The mock now defaults to *not* being this machine. It models a Windows
box (`C:\proj`), so leaving it on made two unrelated spawn-ordering
tests judge Windows paths against the host running the suite. A test
about local paths turns it on and uses paths that exist.
On a config dir it cannot write, `tty7 new` answers:
tty7: Permission denied (os error 13)
and that is the whole of it. The refusal is right and the exit code is
right — nothing is silently lost — but the sentence names neither what
tty7 was doing nor which file it could not write, on a machine where the
config dir is exactly what the reader has to go and fix. Every other
error this CLI prints says what failed and what to try.
The `io::Error` came off `OpenOptions::open` and travelled the wire
unchanged. Name it where the meaning is, keeping the kind so callers
that match on it still can:
tty7: could not write the machine tree at /…/machine.json:
Permission denied (os error 13)
The test skips itself under uid 0, where the mode bits it relies on do
not apply and it would fail for a reason it is not about.
Durability checked while here, and sound, so it is not re-tested: three
workspaces and a rename survive SIGTERM, and survive SIGKILL sent
immediately after the rename — the store persists on the mutation, not
at shutdown.
Six `tty7 server start` at once reported six different pids, five of
them gone by the time they were printed, and every one of them claimed
`"started": true`.
The singleton admits one server and the losers exit immediately — but
they all spawned a child first, and they all then see `running()` go
true, because the winner is up. Each reported the child it had spawned.
`pid` is the field a script keeps in order to watch or stop the server,
so five of six callers were handed a dead one.
Report the pid that is actually serving, read from the pidfile, and say
`started: false` when this call was not the one that started it — the
same answer `start` already gives when a server was up before it ran.
Readable by then: the daemon writes the pidfile after `bind`, and
`running()` needs a request answered, which is later still.
The ordinary path is untouched: a start that wins reports its own child,
because that is the serving pid.
Verified by racing six starts against an isolated config dir — all six
now report the one live pid, exactly one says it started it — and by a
single start, which reports the pid `ps` shows for that config dir.
Also checked while looking for this, and sound: a truncated, garbage,
empty or wrong-schema machine.json is quarantined (`.corrupt`, then
`.corrupt.N`) and the server starts clean; a killed server's pane
children do not leak, since closing the pty master hangs them up.
`kill -9` the server and it never comes back:
tty7-server did not open its endpoints within 10s —
it had already exited with an error
with the real reason only visible by running it by hand:
bind /var/…/T/tty7-<hash>.sock failed: Address already in use
A unix endpoint is a socket *file*. Killed uncleanly, the daemon leaves
it on disk, and `bind` on a path that exists fails instead of replacing
it. The user is stuck until they delete a file out of a temp directory
nothing points them at — after SIGKILL, an OOM kill, or a lost machine.
The startup path already had exactly the right code — probe the
endpoint, refuse if something answers, unlink it if nothing does — and
skipped it whenever the recorded daemon was gone. The comment gives the
reasoning: a dead recorded daemon cannot own a live endpoint, so there
is no point paying the refusal delay, and the bind below "overwrites the
file". That last part holds for a Windows daemon.port, which is a
recorded number; it does not hold for a socket file. So the shortcut
skipped the unlink precisely when the daemon had died the way that
leaves one behind.
Keep the shortcut for the probe, which is what costs, and unlink
whichever way the probe went.
Verified end to end on an isolated config dir: capture the socket path,
kill -9, confirm the file survives, and `server start` now recovers with
no manual cleanup. A live server is still refused and keeps serving —
the singleton seat turns the second one away before it reaches this
code, and only one server is left running.
The test pins the platform fact the fix rests on rather than the fix
itself: bind fails on a leftover socket path, and succeeds once it is
removed.
Found by running every `tty7 …` command the help and README recommend.
`tab ls @7` answered "no workspace named '@7'". `@7` is a well-formed
tab address typed into a *tab* command, and its own siblings `tab close
@7` and `tab rename @7` both answer "no tab @7" — only `ls` takes a
workspace in that slot, so the sigil looked like the problem instead of
the slot. `parse_workspace` accepts any word at all, unlike `parse_pane`
and `parse_tab` which each validate their own sigil, so nothing noticed.
Name it for what it is, `%` included. The hint is only reachable after
the lookup has already failed, so a workspace someone really did name
`@7` is found first and never sees it.
Then, checking my own new message did not recommend a dead end — the
mistake this same sweep caught last time — it did: `tty7 ls` renders
WORKSPACE/NAME/TABS/PANES/ATTACHED and has no @ column at all, so it
cannot answer "which workspace holds @7". The pre-existing "no tab @7 —
`tty7 ls` shows the @ numbers" was wrong the same way, and has been
since before this branch.
`tty7 pane ls` is the listing that actually carries panes, their tabs'
@ numbers and the workspace holding them, so all three now point there.
Verified against a live server: every message, and the command each one
recommends.
Also checked and found sound, so it is not re-swept: --json is a single
valid object for all ten listing verbs, errors are empty stdout plus a
message on stderr with rc=1, and a mistyped subcommand is named as one
rather than offered to the GUI (deliberate, and tested).
`tty7 wait`'s headline example is
tty7 wait %3 && tty7 capture %3 --plain
and `exit` is one of the three states `--until` waits for by default. Run
it that way and the pane is gone by the time capture runs: wait reports
`exit` and succeeds, `&&` proceeds, and capture answers that nothing is
running. An agent following the documented pattern gets no output for a
command that produced some.
The example is right for an agent — `waiting` and `done` leave the pane
alive. It is wrong for a plain command, which finishes by the shell
exiting. Spell both out in `wait`'s long help, and say plainly that
`exit` is the end state with nothing left to read.
Verified against a live server: `wait --until free` blocks for the
command (5s for a 4s sleep), and the capture after it finds the marker.
Also: a server running one pane had `doctor` say "1 panes" on its first
line. It is the first thing a new user runs. Every other count in the
tree is already plural-aware — the GUI routes these through `t_plural`
with "one"/"other" branches, and the CLI's other count (`closed N panes`)
handles the single case separately — so this was the last one.
Found by running the CLI rather than reading it. `tty7 run --keep` files
its pane into a workspace and the tree keeps that leaf after the command
exits — `tab ls --json` goes on reporting `panes: [31]`. But every
`%PANE` verb answered:
no pane %31 on this machine — `tty7 pane ls --all` lists them
which is wrong twice. The pane *is* on this machine. And `pane ls --all`
is documented as "every pane the server runs", so the command it sends
you to is the one guaranteed never to show this pane — the user follows
the advice, sees nothing, and has no next move.
`or_no_such_pane` consulted only the running registry, which cannot tell
"never existed" from "not running". It now asks the tree once it already
knows the pane is not running, and answers accordingly:
pane %31 is not running — ivory-heron still holds it, so there is
nothing to read or type into. `tty7 tab ls 1790a260` shows it;
opening the workspace in the GUI revives it.
The tree is fetched only on a path that has already failed, so no
ordinary call pays for it, and an unreadable tree falls back to the
plain sentence. A genuinely absent pane is unchanged.
Verified against a live daemon on an isolated config dir: both messages,
both exit 1, and the recommended `tab ls` really does list the pane —
having just fixed one dead-end recommendation, I checked the new one.
Third pass over #268, filtered to the claims that bind two places
together ("the only place that…", "keep in sync with…"). Those are the
ones 640 commits can silently break, unlike a comment that only
describes the line under it. 36 such blocks are still missing; these
five are checked and hold:
- `PaneRoute::for_workspace` is infallible on purpose — its callers (a
close, a restore probe) have nowhere to put an error, and `Unroutable`
is the safe answer where the local daemon would be the wrong one.
- `SshManager::probes` caches a `None` as firmly as a hit, so a host
with nothing to inject is not re-probed per tab.
- `identifier_could_match` is prefix-matching before the `;` and exact
after it, which is what keeps OSC `77;` from matching `777`.
- The `/proc/<pid>/cwd` reading is rejected unless it still stats as a
directory: a deleted cwd reads back as `<path> (deleted)`, and that is
broadcast now, so it would aim every new tab and split at it.
- `GRID_PAD_X/Y` are the single source of truth for the surface inset
that grid-aligned overlays offset by.
The `run` half of install's probe-cost invariant also still holds — it
derives usability from `stat` mode bits and never spends a probe on a
hit — so it needed no change.
`GRID_PAD` is restored with its claim corrected: the comment asked the
reader to keep `render`'s `.px()/.py()` in sync by hand, but render has
since been changed to apply the constants themselves. Telling anyone to
hand-sync that now would be inviting the drift it warns about.
Second pass over what #268 stripped, this time reading each comment as
a claim to check rather than prose to paste back. All eight hold, so
this is documentation only — but they were worth checking, and two of
them are the kind that gets "simplified" by someone who cannot see why:
- `OutputGate::queued` is `AtomicI64` because add runs per PTY read at
~100k/s and sub per socket write, so neither may take a lock; signed
so a decrement racing a reset drifts negative instead of underflowing.
- `wait_below_high_water` returns before touching the mutex when the
backlog is under the mark, which is the common path.
- `MAX_RING_SEGMENTS` bounds a leak: drag-resize cuts a segment per
column, none of them filling `RING_CAP`, so attach would degrade
linearly over a pane's life. Past the cap the two oldest merge.
- `CAPABILITY_ENV` keeps `TERM`/`COLORTERM` out of reach of the user's
env map — a fact about what the far end decodes, not a preference.
- `ForegroundProbes::agent` and `::cwd` distinguish "no reading" from
"nothing there"; the caller applies the outer Option, never flattens
it, so a backend with no process-table view cannot wipe an agent
identified by sentinel events.
- The spawn path avoids appending argv to `new_default_prog()`, which
portable-pty panics on by design.
Also fix an unresolved `[`ForwardEntry`]` link I added in 61002ec. It
only fails under `--document-private-items`; plain `cargo doc --no-deps`
never resolves private-item links, so it is the wrong gate for this
tree and I have moved to the stricter one.
#268 ("strip every comment from the Rust sources") removed 38,164
comment lines. Much has been rewritten since, but 7,587 blocks still sit
in front of code that is unchanged and now undocumented. Last commit I
started implementing a pair of deliberate no-op stubs before `git log -L`
turned up the stripped comment saying they were deliberate — which is
the failure mode this class of loss produces.
Seven restored here, chosen for documenting why something must not
change rather than what it does:
- `LinkShutdown` — "not optional politeness; without it a client cannot
be closed". A reader parked in a blocking read is not woken by any
flag, so closing has to act on the fd, and no std trait spans the
transports. Nothing else says this.
- `peek_frame_kind` / `is_error_kind` — why Attach is classified before
the payload is paid for.
- `FontFeatures` — a frozen config key deliberately replicating
gpui's type so tty7-core parses without linking gpui.
- `desired_tabs` / `every_leaf_is_native_ssh` — held vs
permanently-invisible tabs, and why conflating them either deletes a
daemon tab mid-revival or freezes a window's ordering forever.
- `control_for` / `TreeLink` / `classify_tree_link` — local and remote
links unified, and unserved as a fact about the peer rather than a
transient down.
Restored against the current code, not verbatim: three claims had gone
stale and are corrected. `Duplex` has since adopted `LinkShutdown`
(`Halves` carries one) rather than needing to; `peek_frame_kind` has two
callers now, not one; and `gpui_font_features` moved out of `ui::app`.
src/terminal/remote.rs opened with a blanket `#![allow(dead_code)]` —
every other suppression in the tree is per-item or cfg_attr'd. It was
hiding nine unused associated functions, and four of them are the sort
worth catching: `spawn`, `attach`, `list_panes` and `kill_pane`, the
local-only wrappers left behind when panes became route-addressed. Each
hardcodes `PaneRoute::Local`, so reaching for one instead of its `_on`
sibling silently talks to the local daemon whatever machine owns the
pane — the exact mistake the route argument exists to prevent. Better
they not be there to reach for.
The rest: `on_workspace_forwards`, a swallowing copy of what
`ForwardRoute::forwards` now returns as Option, and the four known-host
and loopback-forward wrappers no UI ever called.
Also restore, on the daemon side, the rationale that #268 stripped from
the two loopback stubs. Without it they read as an unfinished feature —
they are not, and the comment saying so is what stops the next reader
(this one included) from "finishing" them.
Windows CI is the check that cannot run here: the two platform-gated
regions in this file are self-contained, so nothing ungated is left
stranded, but only a Windows build proves no new dead_code warning.
A workspace's forwards are released by exactly one thing: the
TeardownForwards request the GUI sends as the workspace closes. Nothing
daemon-side reaps them, unlike a pane's, which DaemonPane::drop always
takes down. So the request failing is the case that leaves ports bound
for the rest of the daemon's life.
It was also the case nobody could see. ForwardRoute::teardown answered
Vec::new() both when the route could not be addressed and when the
request went unanswered, and the caller's only diagnostic fired on
`!left.is_empty()` — so the one path that partly worked warned, and all
three that did nothing at all stayed silent.
Hand back Option, as `forwards` already does one method up, for the
reason its doc comment already gives: an empty list is a far side with
nothing left, and a failed request says nothing about what it still
holds. Warn when no pane is left to address the workspace through, and
when the teardown is not delivered.
This makes the leak observable; it does not close it. A GUI that is
killed rather than closed still never sends the request.
`run --keep` resolves `--ws` against the machine tree before it spawns
anything, and takes `$TTY7_WS` on trust — it only parses the id. Filing
the pane happens after the spawn, so a shell whose workspace has since
been removed, or one opened against another machine, started the pane and
then failed to file it. The pane kept running with nothing holding it.
Measured against a live daemon: with a `$TTY7_WS` this machine does not
have, `run --keep` exited 1 with a clear message and left a `sleep`
behind, `pane ls --all` reporting `"orphans":1`. With `--ws` spelled out,
the same bad id spawned nothing.
Recoverable — `pane ls --all` finds it and `pane close` ends it, both
documented — but nobody asked for the pane, and `$TTY7_WS` is the
*documented default* for `--keep`, so this is the ordinary path rather
than an exotic one.
Resolving the inherited id is now the same check the explicit one gets,
and only when `--keep` needs it: a plain `run` uses the workspace as an
ownership stamp, where a stale id costs nothing and the round trip to
find out would.
The spawn still precedes the filing — `TabCreate` names the pane the
daemon assigned, so it has to, and
`run_keep_spawns_first_then_files_the_pane_into_the_workspace` pins that.
What moved is only where the workspace is checked.
`serve_sigterm` blocks SIGTERM on the daemon's main thread before any
other starts, so every thread inherits the block and only its `sigwait`
waiter ends the process. That is deliberate and the comment there
explains it well. What it also does is reach places it was never meant
to: `fork` copies the calling thread's mask into the child and `execve`
keeps it.
So every pane ran with SIGTERM blocked. `kill`, `pkill`, `timeout`, a
supervisor, a CI cancellation — none of them could stop anything in a
pane, and only `kill -9` would. Measured, not reasoned:
tty7 run -- sh -c 'kill -TERM $$; echo STILL-ALIVE; exit 7'
→ STILL-ALIVE, exit 7
portable-pty does reset the child's SIGTERM disposition to `SIG_DFL`, but
a blocked signal is never delivered for a disposition to apply, so that
was never going to help.
This is the unix half of the Windows line three above it, which resets
the inherited "ignore Ctrl+C" state for the same reason: the daemon's own
signal state is not the pane's. That one cost a pane every Ctrl+C
(#451, #314); this one cost it every kill.
The block is lifted for the spawn alone, because the mask a child forks
with is the one it keeps, and the guard restores the whole saved mask
rather than re-blocking, so a thread that never had it blocked is left as
it was. The cost is a fork-wide window where a SIGTERM meant for the
daemon could land on this thread and end it without the scrollback save;
that is a millisecond against every pane being unstoppable.
Verified against a running daemon, both halves: the command above is now
terminated with nothing printed, and a SIGTERM to the daemon with a live
pane still exits it and still leaves that pane's screen in
`scrollback/`, which is what `serve_sigterm` exists to do.
Checking whether the Windows arm shared the bug fixed in 8959421 found it
already right — and better. `rollback_portable_failure` is one helper
called from both failure sites, so its two arms cannot drift; it removes
the backup only `if restore.is_ok()`; and `recovery_error` ends the
message with "backup preserved at {path}".
macOS said only "restoring the previous app: {restore}". Same situation —
the update failed, the restore failed, there is no app — and the platform
that leaves someone without one was the platform that did not say where
the copy is. Both macOS arms now end the same way, with the same words
Windows uses.
Nobody is reading this message while things are going well. It is what is
left when an update has taken the app and failed to put it back, and a
path in it is the difference between a recoverable morning and a
reinstall.
`replace_and_relaunch` moves the current bundle into the stage, then
renames the replacement into its place. Between those two lines there is
no app at all, and the backup inside the stage is the only copy.
If the second rename failed, the restore was attempted with `let _ =` and
the stage was deleted regardless. A restore that failed therefore took
the backup with it: nothing at `current`, no copy left, and an error that
named only the swap. That is a reinstall.
The launch-failure arm ten lines below already does this correctly — it
matches on the restore, reports it in the message, and leaves the stage
alone so the backup survives. This one now does the same. Two arms of the
same function disagreeing about how much care a missing app deserves is
the whole of the bug.
The new test covers the swap-failure path, which had none: a replacement
that was never extracted fails the second rename, and the previous app
has to be back at `current` with its own marker. Removing the restore
fails it.
What it does not cover is the restore *itself* failing — the seams here
are `launch` and `report`, and neither runs between the two renames, so
forcing that would mean injecting the rename. The correctness of that arm
rests on mirroring the tested one below it.
Checking whether any setting is written but never read turned up one
field named nowhere outside `config.rs` — `agent_commands`. It is read,
through `agent_commands_cached`, which is the point: that is the only
setting behind a `OnceLock`, so a running server keeps the map it built
at startup.
Every other key here is picked up by a reload. This one is not, and
nothing about editing the file says so: a user adding `{"cc": "claude"}`
sees the wrapper go on being unrecognised and has no reason to suspect
the server rather than the spelling.
The cache is right — the map is read on every pane spawn, and the
alternative is `Config::load()` off disk each time a pane starts. So the
row says `tty7 server restart` and the function says why it costs that,
each pointing at the other.
All 77 fields are read; this was the only one worth a word.
`l10n_keys!` carries `#[allow(dead_code)]` for a good reason — whether a
key is reachable is a per-platform question, and a macOS build flags a
dozen that Windows and Linux do use — and it names the audit that gets
around that: clippy per platform, intersected.
A text search over the whole tree answers the same question in one pass
and sees `cfg`-ed-out code too, which the per-platform build cannot. It
leaves 16 keys nothing names. Eight are accounted for: three
`SettingsLanguage*` are `label_key`s in `SUPPORTED_LANGUAGES`, and five
`Scm*` sit in `SCM_KEYS_AWAITING_A_CALLER`, which exists to say exactly
that. The other eight are named by nothing at all:
- six `SwitcherStatus{Restarting,Installing,Connecting,ConnectFailed,
NotConnected,Reconnecting}` — the row shows link state "as the dot's
color" now, so the words they were written for are not rendered
anywhere. `SwitcherStatusTakenOver` beside them is still live, which
is why the family reads as used at a glance.
- `SwitcherConnectToUse`, last touched by "stop opening context menus
with every row greyed out" — the affordance went, the string stayed.
- `AppAgentHooksOpFailed`, never wired since the locale split added it.
Twenty-four translations for nine strings nobody can see. Deleting the
enum variants is what makes it verifiable: the three locale tables match
`L10nKey` exhaustively, so the compiler picked out every arm.
`SCM_KEYS_AWAITING_A_CALLER` is the convention here — a key with no
caller is either named there or gone. These were neither.
`SplitRight` and `SplitDown` were taken out of `HOME_SHORTCUTS` because
both need a pane and the home page is what a window shows without one —
"advertising two chords that do nothing from the only screen that offers
them", as the comment there puts it, with a test to keep them out.
Their labels stayed. `home_shortcut_label` still had an arm for each, and
`HomeSplitRight`/`HomeSplitDown` were carried in three locale tables to
serve those arms and nothing else.
Nothing could point this out. The single caller iterates
`HOME_SHORTCUTS`, so the arms are unreachable — but `_ => action` makes
the match total, so no compiler warning, and the keys are textually
referenced, so `dead_code` sees them as used. Only reading the caller
does it.
Removing the enum variants is what makes this safe rather than hopeful:
the three locale tables match `L10nKey` exhaustively, so the compiler
named every translation to delete and would have caught a fourth.