feat(prompt): selection at a prompt types over and deletes - #4
feat(prompt): selection at a prompt types over and deletes#4DananzMolt wants to merge 28 commits into
Conversation
Selecting at a prompt and typing replaces the selection; backspace and delete remove it. Also fixes prompt click-to-move spending two arrow keys on a wide character. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
Selection editing was inert on fish, which advertises click_events rather than cl. It now places the cursor with arrow keys instead of relying on the shell's click support, which fish advertises but does not honor for injected events. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
Follow-up: it was inert on fish, and the obvious fix was also wrongDogfood found the feature did nothing. Two bugs. 1. Wrong gate. The original commit required the shell to advertise the OSC 133 2. Supporting Fish consumes the escape and inserts the remainder literally. It advertises click handling it does not honor for an injected event. Shipping the "obvious" fix would have produced a second dud. Fix: stop asking the shell where to put its cursor. The terminal already knows the distance in input cells, and arrow keys are understood by every line editor. New This also drops the click-option requirement entirely. All the feature needs now is OSC 133 New tests assert the bug directly: for a
Baseline for a filter matching nothing is 72. Worth noting for separate triage: ghostty's own ghostty |
Tagged dev builds identified themselves as "cmux DEV <tag>", which is the upstream's name, not this fork's. Split the visible label out of APP_NAME into DISPLAY_NAME and set only CFBundleName / CFBundleDisplayName from it. APP_NAME still names the .app directory, because that has to keep matching what xcodebuild emits (via BASE_APP_NAME) and what the rest of the dev tooling greps for: cleanup scripts, the debug CLI, the UI tests and the docs all match on "cmux DEV". Renaming the directory is a separate, wider change. An explicit --name still wins for both, so a build is never called one thing on disk and another in the Dock.
Shift+Left/Right selects by character, Shift+Option+arrow by word, with the direction mapped by row so it reads correctly in Hebrew. Also fixes cell backgrounds, selection highlight and decorations being painted at the logical column instead of the visual one on RTL rows. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
Mouse positions now invert the row's bidi order, so selecting, clicking and link hit-testing land on the cell under the pointer in Hebrew. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
Prompt selection editing, Shift+arrow keyboard selection, and the bidi fixes that make both correct on Hebrew rows. Also gives the app its own version. Ivrix 1.0.0 shipped carrying cmux's 0.64.20, so nothing could compare Ivrix releases to each other. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
The sidebar button runs Sparkle against SUFeedURL, which named upstream cmux.
An Ivrix build asking upstream for updates is offered upstream's releases, and
because they are different applications, accepting one replaces Ivrix with
cmux and loses the Hebrew build. Ivrix 1.1.0 shipped in that state.
Three places had to change, and the second is the one that would have bitten
again later:
- Resources/Info.plist SUFeedURL now names this repo.
- UpdateFeedResolver's default fallback did too, and it is used whenever the
plist value is missing or empty. Fixing only the plist would have left a
silent path back to upstream.
- build-ivrix.sh stamps the feed URL and the EdDSA public key into the built
bundle, and refuses to build if the feed still points upstream or if the
public key is missing or is upstream's. A wrong value here is worse than
having no updater, so it fails the build rather than warning.
Adds scripts/ivrix-appcast.sh to generate and sign the appcast a release needs;
without that asset the feed URL 404s and no update is ever offered. The signing
key is read from the login Keychain by sign_update and never touches the repo.
Still needed before an Ivrix build can actually self-update: generate the
keypair once with Sparkle's generate_keys and rebuild with its public half.
macOS ships bash 3.2, where `set -u` treats an empty array expansion as unbound, so the appcast script aborted before signing.
Reverts the gating. In a right-to-left session, going 'back' with the arrow that points along the reading direction is the wanted behaviour, including inside a full-screen application. The mismatch with an application's own 'press <-' hint is a display problem, not a key-handling one. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
The RTL/LTR control in the titlebar was the only way to flip the terminal print direction. Add a `toggleTextDirection` shortcut action, bound to Ctrl+Cmd+H by default, plus a matching View menu item. All four entrypoints (titlebar control, toolbar segmented control, menu item, shortcut) now flip through one path, `TerminalTextDirectionSettings.toggleDirection()`, instead of each site computing the next direction itself. Ctrl+Cmd+H is free of AppKit reservations and of every cmux default; Cmd+H alone is Hide Application, so the Ctrl variant does not shadow it. Per the shortcut policy the action is in `KeyboardShortcutSettings`, editable in Settings > Keyboard Shortcuts, settable as `shortcuts.bindings.toggleTextDirection` in `~/.config/cmux/cmux.json`, and documented in the shortcut docs and the cmux-settings skill reference. Localized across all 20 supported locales.
Hebrew layouts put HEBREW PUNCTUATION GERESH (U+05F3) and GERSHAYIM (U+05F4) on the apostrophe and quote keys, so `echo "hi"` typed in Hebrew arrives as `echo ״hi״` and the shell never sees a quote. Rewrite those two scalars to ASCII `'` and `"` on the way into the terminal, on both typed-text paths: the `insertText` accumulator and the `textForKeyEvent` fallback. The rewrite is gated on `keyTextAccumulator` being non-nil, so it only applies to live keystrokes — paste, dictation, and programmatic NSTextInputClient callers keep their text verbatim. `normalized` scans for the two scalars before reading UserDefaults, so ordinary typing pays one pass over a one-scalar string and nothing else. On by default, since shell quoting is the common case. Settings > Terminal > ASCII Quotes on Hebrew Layout turns it off for typing Hebrew acronyms such as צה״ל, which need the real gershayim; also settable as `terminal.hebrewAsciiQuotes` in `~/.config/cmux/cmux.json`. Localized across all 20 supported locales. Also adds the direction-toggle coverage for the shared `TerminalTextDirectionSettings.toggleDirection()` path added in the previous commit.
The titlebar RTL/LTR button's hover tooltip named only the current direction, so the new Ctrl+Cmd+H binding was undiscoverable from the control it drives. Route it through `Action.tooltip(_:)`, the same helper the sidebar, notifications, and focus-history buttons already use, so the tooltip reads "Right-to-left (⌃⌘H)" and follows a rebind.
Selection editing at a prompt no longer refuses on the sole grounds that an application owns the screen. It now requires what it actually needs: cells marked as input by OSC 133 `B`. No behavior change today. Claude Code, and every other full-screen application I know of, emits no OSC 133, so their composers are still copy-only. This makes Ivrix ready for the ones that mark, and turns the open question into an upstream ask rather than a terminal-side guess about where somebody else's input box begins.
Keeps the terminal-side half and the upstream request that unblocks it in one place, so the next person to touch selection editing can see why it stops at the composer of a full-screen application.
…tes on a Hebrew layout
Stacked on #3. Base is
ivrix-bidi-hyphen-fix; retarget toivrix-hebrew-fontonce that merges.What
Select text at a prompt, type, and the selection is replaced. Press backspace or delete and it is removed. Editor behaviour, at the shell.
How, and why it looks like this
There is no terminal protocol for this. The selection lives entirely in the terminal, the shell has never heard of it, and no escape sequence expresses "replace the selection". Every terminal that appears to do this is either an application implementing its own editor (Claude Code, vim) or is not doing it at all.
So it is emulated on the mechanism
cursor-click-to-movealready uses:Backspace and delete are consumed at step 3 instead, since removing the selection is already what they asked for. The full sequence is one write, not one message per keystroke, so the shell cannot redraw partway through and invalidate the counts.
New
Screen.promptInputCellCountcounts editable positions using exactly the accountingpromptClickMoveuses, so the delete count is directly comparable to the arrow count. That equivalence is the whole correctness argument.Deliberately gated off
Acting on a selection the terminal cannot verify would corrupt the user's command line, so each of these bails rather than guessing:
cl(aclick_eventsshell has no ranged-delete equivalent)promptClickMovereturns no-movement both when it succeeds trivially and when it refuses, and deleting on the second would delete from wherever the cursor happens to beConfig:
selection-edit-at-prompt, default true.Prerequisite fix, separate commit
promptClickMovecounted screen cells, but a wide character is two cells and one editor position, so clicking past CJK or emoji emitted an extra arrow per wide character and overshot. This is an upstream ghostty bug, not an Ivrix one, but it had to be fixed here or the delete count would not match the arrow count.Known limitation
zsh-autosuggestions and similar render ghost text into cells that are not in the edit buffer. A selection extending into that region counts cells the shell will not delete. Documented in the config docs and the commit message; no way to detect it from the screen alone.
Verification
prompt selection editpromptInputpromptClickMovebidiBaseline with a filter matching nothing is 72, so the new tests genuinely execute rather than being silently skipped.
New coverage: 4
promptInputCellCountcases (inclusive range, prompt cells excluded, wide character counted once, reversed range), 1 wide-characterpromptClickMovecase, 2 key-classifier cases covering text, shift, backspace, delete, repeat, ctrl/alt/super, arrows, enter, tab, escape.The runtime path in
Surface.maybePromptSelectionEditis not unit-testable without aSurfaceharness, so it is covered by construction and dogfood. That is the weak spot in this PR.zig build testunfiltered still does not compile on this line, from the same pre-existingsrc/font/shaper/coretext.zig:1360break noted in #3.Submodule
ghostty
f64bbae..6dda887, pushed toDananzMolt/ghosttyasivrix-prompt-selection-edit.https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb