streampager + curses_compat: fix Shift keys (G, @, capitals, A) broken in Ghostty#1364
streampager + curses_compat: fix Shift keys (G, @, capitals, A) broken in Ghostty#1364vegerot wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes streampager key handling under Ghostty/libghostty when modifyOtherKeys=2 is enabled by normalizing Shift-modified Char(...) inputs so existing keymap bindings (which are stored as Char(...)+NONE) still match.
Changes:
- Normalize
KeyEventcandidates inScreen::dispatch_keyto try Shift-stripped variants forChar(...)keys. - Apply the same Shift normalization behavior in
Prompt::dispatch_keyso shifted text keys (e.g.,@, capital letters) are accepted in the prompt.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| eden/scm/lib/third-party/streampager/src/screen.rs | Adds key-candidate normalization in pager navigation dispatch to handle Ghostty’s Shift-modified text key encoding. |
| eden/scm/lib/third-party/streampager/src/prompt.rs | Extends prompt key dispatch to try Shift-stripped character variants so shifted characters can be entered. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
This pull request has been imported. If you are a Meta employee, you can view this in D113135744. (Because this pull request was imported automatically, there will not be any future comments.) |
|
@vegerot has updated the pull request. You must reimport the pull request before landing. |
|
@vegerot has updated the pull request. You must reimport the pull request before landing. |
|
@vegerot has updated the pull request. You must reimport the pull request before landing. |
|
@vegerot has updated the pull request. You must reimport the pull request before landing. |
|
@vegerot has updated the pull request. You must reimport the pull request before landing. |
|
@vegerot has updated the pull request. You must reimport the pull request before landing. |
|
@vegerot has updated the pull request. You must reimport the pull request before landing. |
…n in Ghostty
Summary:
When running under Ghostty (and cmux which uses libghostty) outside of tmux,
eden/scm's internal pager and interactive commit UI did not register any
Shift-modified key.
Root cause:
- `termwiz` `UnixTerminal::set_raw_mode()` enables `modifyOtherKeys=2` (`ESC[>4;2m`).
- Ghostty's `key_encode.zig` correctly implements this: `Shift+G` is sent as
`ESC[27;2;71~` (where 71 is uppercase G) and `Shift+@` as `ESC[27;2;64~`.
- `termwiz` `InputParser` decodes these as `Char('G')+SHIFT` and `Char('@')+SHIFT`.
Two separate code paths were affected:
1. **streampager** (Rust, used by `sl log` etc.):
The keymap stores `'G'` as `(NONE, Char('G'))`. The lookup used exact
modifier match, so `(SHIFT, Char('G'))` never matched — `G` was silently dropped.
2. **curses_compat** (Python, used by `sl commit --interactive` / crecord):
`curses_compat.py:getch()` only handled `Modifiers.NONE` and `Modifiers.CTRL`.
When `SHIFT` was set, it fell through to `return -1`, silently dropping the key
so `A` (toggle all), `G`, `J`, `H`, `F` etc. did not work.
Additionally, the CTRL check used `Modifiers.CTRL or Modifiers.RIGHT_CTRL` (Python
`or` always evaluates to `Modifiers.CTRL`); fixed to use bitwise `|` with `&`.
tmux masks this because `send-keys -l` sends plain `0x47` (`G+NONE`). Raw hex
injection via tmux reproduces the bug:
```
tmux send-keys -H '1b5b32373b323b37317e' # ESC[27;2;71~ Shift+G
```
Old `sl` stays at top, new `sl` goes to bottom.
Fix:
Strip the SHIFT modifier and retry the lookup:
1. **streampager**: In `Screen::dispatch_key` and `Prompt::dispatch_key`, build a
candidate list that includes the original key plus a SHIFT-stripped variant, then
try each against the keymap/prompt match. `modifyOtherKeys=2` already sends the
shifted character (uppercase), so stripping SHIFT is sufficient.
2. **curses_compat**: In `getch()`, add a `modifiers & SHIFT` branch that returns
`ord(ch.upper())`. Also fix the CTRL modifier check from `or` to bitwise `|`.
Test Plan:
- `make oss` - builds (but requires facebook#1365)
- tmux test:
```bash
tmux new -d -s t; tmux send-keys -t t './out/sl log -l 5000 --pager=always'
# Check plain G goes to bottom, g to top.
tmux send-keys -t t -H '1b5b32373b323b37317e' # Shift+G Ghostty modifyOtherKeys
# => now goes to bottom (was stuck at top)
tmux send-keys -t t -l '/' ; send -H '1b5b32373b323b36347e' # Shift+@ in prompt
# => now shows Search: @ (was empty)
```
Verified old `/usr/local/bin/sl` fails, new `out/sl` passes.
- `cargo test -p sapling-streampager` still 5 passed.
- `python tests/run-tests.py tests/test-commit-interactive-curses.t` passes.
- Manual in Ghostty.app outside tmux: `G` jumps to bottom, `/` `@` search works,
capital letters searchable (previously broken). `sl commit --interactive` `A`
toggles all files (previously broken).
Fixes facebook#1363
Co-authored-by: Opencode <opencode@anomalyco.dev>
|
@vegerot has updated the pull request. You must reimport the pull request before landing. |
| if modifiers & Modifiers.SHIFT: | ||
| # Ghostty with modifyOtherKeys=2 sends Shift+A as | ||
| # Char('a')+SHIFT instead of plain 'A' (0x41). | ||
| # Normalize to uppercase so crecord keybindings like | ||
| # 'A' (toggle all), 'G', 'J', 'H', 'F' work. | ||
| return ord(ch.upper()) | ||
| if modifiers & (Modifiers.CTRL | Modifiers.RIGHT_CTRL): | ||
| # emulate curses behavior, ord(upper) & 0x1f | ||
| return ord(ch.upper()) & 0x1F |
Summary:
When running under Ghostty (and cmux which uses libghostty) outside of tmux,
eden/scm's internal pager and interactive commit UI did not register any
Shift-modified key.
Root cause:
termwizUnixTerminal::set_raw_mode()enablesmodifyOtherKeys=2(ESC[>4;2m).key_encode.zigcorrectly implements this:Shift+Gis sent asESC[27;2;71~(where 71 is uppercase G) andShift+@asESC[27;2;64~.termwizInputParserdecodes these asChar('G')+SHIFTandChar('@')+SHIFT.Two separate code paths were affected:
streampager (Rust, used by
sl logetc.):The keymap stores
'G'as(NONE, Char('G')). The lookup used exactmodifier match, so
(SHIFT, Char('G'))never matched —Gwas silently dropped.curses_compat (Python, used by
sl commit --interactive/ crecord):curses_compat.py:getch()only handledModifiers.NONEandModifiers.CTRL.When
SHIFTwas set, it fell through toreturn -1, silently dropping the keyso
A(toggle all),G,J,H,Fetc. did not work.Additionally, the CTRL check used
Modifiers.CTRL or Modifiers.RIGHT_CTRL(Pythonoralways evaluates toModifiers.CTRL); fixed to use bitwise|with&.tmux masks this because
send-keys -lsends plain0x47(G+NONE). Raw hexinjection via tmux reproduces the bug:
Old
slstays at top, newslgoes to bottom.Fix:
Strip the SHIFT modifier and retry the lookup:
streampager: In
Screen::dispatch_keyandPrompt::dispatch_key, build acandidate list that includes the original key plus a SHIFT-stripped variant, then
try each against the keymap/prompt match.
modifyOtherKeys=2already sends theshifted character (uppercase), so stripping SHIFT is sufficient.
curses_compat: In
getch(), add amodifiers & SHIFTbranch that returnsord(ch.upper()). Also fix the CTRL modifier check fromorto bitwise|.Test Plan:
make oss- builds (but requires fix(oss-build): allow nightly features on stable via RUSTC_BOOTSTRAP #1365)/usr/local/bin/slfails, newout/slpasses.cargo test -p sapling-streampagerstill 5 passed.python tests/run-tests.py tests/test-commit-interactive-curses.tpasses.Gjumps to bottom,/@search works,capital letters searchable (previously broken).
sl commit --interactiveAtoggles all files (previously broken).
Fixes #1363
Co-authored-by: Opencode opencode@anomalyco.dev