f8e2a59 added `OpenInFileManagerFailed` in all three locales for exactly
this case and then only wired its sibling, so the key sat dead — clippy
was reporting it as never constructed. The follow-up fix invented a second
key rather than using it.
Now the notification reads "Could not open /…/themes", naming the path
instead of a category, and `ThemeFolderFailed` is gone. Clippy's
never-constructed list is back to what it is on main.
`open_themes_folder` created the directory with `let _ =` and then handed
the path to the system file manager regardless. On the one click that can
actually fail — the first one on a fresh install, where the folder does
not exist yet — a read-only or permission-denied config directory left the
button doing nothing at all, with no message.
It now reports the failure through the same `notify_err` path as the rest
of the theme editor, and stops instead of opening a path that is not there.
The `themes_dir() == None` branch stays a log line: without a config
directory there is no theme editor to have clicked from.
`complete()` decided "is this a command position?" with
`chars[..word_start].all(is_whitespace)` — true only at the very start of
the line. Two lines below, `current_command()` already knew better: it
splits the prefix at the last `| & ; \n (` and looks only at the segment
holding the cursor.
So `ls | gre<Tab>` asked for files named `gre*` in the cwd instead of
offering `grep`, and the same for `a && b`, `a; b`. Every shell treats the
first word of each pipeline segment as a command.
`remote_path_request()` carried the identical check, so over SSH the same
keystroke also spent a round trip listing the remote cwd for a word that
was never a path.
Both now go through one `at_command_position()` helper, and the segment
split that was copy-pasted in two places is a `segment_start()` helper.
`ls |gre` — no space — is unchanged: `shell_word_start` only breaks words
on whitespace, so the word is `|gre`. Teaching it to break on
metacharacters has to respect quoting first, or `grep "a|b"` starts
completing `b"`.
The close question names what is running, not how long it has been —
carrying the elapsed seconds through the type invited someone to believe
it reached the screen. Command is now a plain name.
A TUI can park the cursor on the trailing spacer of a wide character.
Inverting that cell fills the right half with the caret while the glyph,
which lives on the lead cell, keeps its own colour across both halves —
an opaque block over half a character.
The reverse-video pass now steps back to the cell that owns the glyph,
which also lets paint_backgrounds absorb the spacer into its run and
cover both columns. A spacer in column 0 has no lead to step back to and
is left alone rather than wrapping to the previous row.
Found reviewing my own diff; the translucent block this replaced had the
same half-coverage but stayed readable through it.
The unsaved-changes sheet listed [Save, Discard, Cancel]. The platform
draws the first entry as the default and lays the rest out beside it, so
that rendered as "Cancel | Discard | Save" — with Discard directly
adjacent to the key Return lands on. Apple puts Cancel between them for
exactly this reason.
Reordered to [Save, Cancel, Discard], which renders "Discard | Cancel |
Save", and moved the discard arm to index 2 to match.
Tested: the index mapping reads 0 = Save, 1 = Cancel (falls through to
no-op), 2 = Discard. The right-to-left rendering was confirmed on the
two-button prompts, where array index 0 is drawn rightmost and takes
Return; the three-button sheet itself was not driven on screen.
The tree search stops at SEARCH_LIMIT and returned the prefix with no
mark, so a query with more matches than that looked like it had found
all of them — and the ones you were looking for were simply not there.
A trailing row now reads "First 200 matches", using the same placeholder
rows that already explain an empty or unreadable directory.
Verified against a 260-file directory: the row appears; on this repo,
where no query reaches the ceiling, it does not.
The scrollback scan stops at 10,000 matches. The count rendered that as
"1/10000", which reads as the whole truth — a search for a common
letter looked like it had found every occurrence and settled on a round
number.
It now reads "1/10000+" once the ceiling is reached, the same way every
search that caps says so.
"Duplicate to edit" logged its failure and did nothing else — a button
that does nothing is indistinguishable from having clicked the wrong
thing. Worse, every colour edit runs through mutate_active_theme, which
returned early on a write failure: the picker moved, the theme did not,
and nothing said why.
Both now report, through the same notify_err the rest of the app uses.
On a themes folder the user cannot write, that reads "Could not
duplicate the theme: You do not have permission." rather than
"Permission denied (os error 13)", or nothing at all.
Restarting the background server is something the user explicitly asks
for, and a failure logged the reason and left them on the home page with
nothing on it — which looks exactly like the restart working and taking
every tab with it.
The reason now lands on the home page, in the same slot a failed first
terminal uses.
A restored tab whose panes could not be started was dropped with a log
line and nothing else. The layout coming back is the product's headline
claim, so a tab quietly not coming back is exactly the thing that needs
saying — a broken shell or an unreachable remote silently thinned the
window and left no trace on screen.
The restore now counts what it dropped. On launch the count lands on the
home screen next to whatever else went wrong; switching workspaces
raises it as a notification, since there is already a window to read.
A theme file that fails to parse logged a warning and then simply was
not in the list. The themes folder is one the app tells you to open and
drop files into — "it isn't there" needs a reason attached to it, and a
log file is not where anyone looks for one.
The loader now carries its rejections out with the themes, and the theme
picker lists each file name with the parser's reason above the presets:
broken.yaml — missing field `accent`.
The worktree prompt and the SSH auth prompt each cover the window with a
full-bleed layer that swallows every click — and drew nothing, so the
app simply stopped responding with no sign of why. The palette and the
switcher already answer this with presets::scrim_fill; these two just
never used it.
Both now dim what is behind them. The worktree prompt also cancels on a
click outside, the same gesture the palette and switcher take. The auth
prompt deliberately does not: a mis-aimed click would abandon a
handshake mid-flight, so Escape stays its only way out.
SSH is one of the reasons to pick tty7 and it had no entry in the menu
bar at all — the only routes were ⌘P and Settings, both of which you
have to already know about. Manage Profiles and Reconnect now sit in
File beside the other things that open a pane; Remote Files and Port
Forwarding sit in View beside the other panel toggles. Same labels as
the command palette, so there is still one name per thing.
AppKit adds its own "Enter Full Screen" to the bottom of any menu named
View, so ours sat directly above it: the same command listed twice under
two different shortcuts. Ours is gone; ⌘↵ still works and is listed on
the Keybindings page. Drops the AppMenuEnterFullscreen key with it.
Each sidebar row shows a truncated title and a branch. The filter read
only the truncated title, so typing the branch you can see — or the part
of the path the row elided — matched nothing, and the list emptied out
on a query that is plainly on screen.
It now matches the full title and the branch as well as the label.
ShowRightPanelInfo, ShowRightPanelChanges and ShowRightPanelFiles have a
make_binding arm and are dispatched like any other action — but they had
no slot in default_bindings, which is the only list set_binding writes
into. So `"ShowRightPanelInfo": "ctrl-1"` in a hand-edited config.json
went into a branch that did nothing, and the Keybindings page, which
reads that same list, never showed them at all.
They now have empty default slots, so they appear under View and can be
bound like anything else. A binding naming an action that does not exist
— a typo, or a name from an older build — now says so in the log instead
of vanishing.
every_dispatchable_action_has_a_slot_to_bind_it_in keeps the two lists
from drifting apart again.
Every host operation that fails renders as "{context}: {raw io::Error}".
Display on an io::Error answers "what happened" for someone reading a
log; it does not answer "what now" for the person who just lost a save,
and "Permission denied (os error 13)" is the shape of that gap.
The six kinds that change what you would do next — denied, gone, no
space, read-only, busy, timed out — now read as one authored sentence in
all three locales. Everything else keeps its raw detail rather than
losing it to a vague house message.
Also: "Save failed" did not say which file, and with more than one
editor tab open that is the first thing you need to know. It is now
"Could not save {name}".