Skip to content

feat(prompt): selection at a prompt types over and deletes - #4

Draft
DananzMolt wants to merge 28 commits into
ivrix-bidi-hyphen-fixfrom
ivrix-prompt-selection-edit-app
Draft

feat(prompt): selection at a prompt types over and deletes#4
DananzMolt wants to merge 28 commits into
ivrix-bidi-hyphen-fixfrom
ivrix-prompt-selection-edit-app

Conversation

@DananzMolt

Copy link
Copy Markdown
Owner

Stacked on #3. Base is ivrix-bidi-hyphen-fix; retarget to ivrix-hebrew-font once 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-move already uses:

  1. arrow keys to move the cursor to the start of the selection
  2. one forward delete per selected position
  3. the original key encodes as normal, inserting where the selection was

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.promptInputCellCount counts editable positions using exactly the accounting promptClickMove uses, 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:

  • no OSC 133 shell integration advertising cl (a click_events shell has no ranged-delete equivalent)
  • cursor not on input. This one is subtle: promptClickMove returns no-movement both when it succeeds trivially and when it refuses, and deleting on the second would delete from wherever the cursor happens to be
  • either selection end outside the current prompt's input, so command output and older prompts stay copy-only
  • rectangle selections, which are not a run of input positions
  • an application owning the screen
  • right-to-left rows. A visually contiguous selection on a Hebrew row is not a contiguous run of logical positions. Enabling it there without the visual-to-logical mouse map would delete the wrong text

Config: selection-edit-at-prompt, default true.

Prerequisite fix, separate commit

promptClickMove counted 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

filter result
prompt selection edit 74/74
promptInput 76/76
promptClickMove 90/90
bidi 109/109

Baseline with a filter matching nothing is 72, so the new tests genuinely execute rather than being silently skipped.

New coverage: 4 promptInputCellCount cases (inclusive range, prompt cells excluded, wide character counted once, reversed range), 1 wide-character promptClickMove case, 2 key-classifier cases covering text, shift, backspace, delete, repeat, ctrl/alt/super, arrows, enter, tab, escape.

The runtime path in Surface.maybePromptSelectionEdit is not unit-testable without a Surface harness, so it is covered by construction and dogfood. That is the weak spot in this PR.

zig build test unfiltered still does not compile on this line, from the same pre-existing src/font/shaper/coretext.zig:1360 break noted in #3.

Submodule

ghostty f64bbae..6dda887, pushed to DananzMolt/ghostty as ivrix-prompt-selection-edit.

https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb

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
@DananzMolt

Copy link
Copy Markdown
Owner Author

Follow-up: it was inert on fish, and the obvious fix was also wrong

Dogfood found the feature did nothing. Two bugs.

1. Wrong gate. The original commit required the shell to advertise the OSC 133 cl click option. zsh does via cmux's integration; fish 4 advertises click_events=1 instead, and the code bailed on that branch. Every fish user got nothing at all.

2. Supporting click_events would not have fixed it either. Before adding that branch I injected the exact byte sequence into a live fish 4.6 prompt to check the mechanism:

ESC[<0;12;3M  ESC[3~ x4      ->  "echo AAAABBBBCCCC[<0;12;3M"   (click ignored, escape body inserted as text)
ESC[D x8      ESC[3~ x4      ->  "echo AAAACCCC"                (arrows + deletes work)

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 Screen.promptLineMove always computes the arrow motion; promptClickMove keeps deferring to click_events shells, which is correct for an actual click, so the shipped click path is untouched.

This also drops the click-option requirement entirely. All the feature needs now is OSC 133 B input marking, so it works on shells that mark input but advertise no click support at all. The config docs are updated accordingly.

New tests assert the bug directly: for a click_events screen, promptClickMove returns zero while promptLineMove still returns the correct motion, in both directions.

filter result
promptLineMove 74/74
prompt selection edit 74/74
promptInput 76/76
promptClickMove 90/90

Baseline for a filter matching nothing is 72.

Worth noting for separate triage: ghostty's own cursor-click-to-move very likely does not work on fish either, for the same reason. That is upstream behaviour and out of scope here.

ghostty 6dda887..89127d8.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant