Track DECSET/DECRST 1007 (Alternate Scroll Mode) - #633
Conversation
Private mode 1007 is xterm's Alternate Scroll Mode, which corresponds to its alternateScroll resource: while the alternate screen is active and the application has not enabled mouse tracking, the terminal translates wheel input into cursor up/down keys, so full-screen programs that do not read the mouse (less, vim without mouse=a) still respond to scrolling. SwiftTerm currently drops 1007 into the "Unhandled DEC Private Mode" branch, so an embedder that implements this translation has no way to let an application turn it off, and DECRQM cannot answer a query about it. This only tracks the state and exposes it as Terminal.alternateScrollMode; translating wheel events stays with the host view, as with the other mouse state SwiftTerm tracks. The mode is wired into DECSET, DECRST, DECRQM and RIS, matching how 1004 is handled. One deliberate choice worth a second opinion: the default here is true, matching Ghostty (src/terminal/modes.zig marks mouse_alternate_scroll default true), while xterm's own alternateScroll resource defaults to false. True keeps the wheel working out of the box in less/vim, which is what users on macOS terminals tend to expect, but I am happy to flip it to false to match xterm if you would rather follow the reference implementation.
|
What can I do to experiment with this with different terminal emulators? |
|
Here's a way to poke at it directly. What each emulator thinks the mode is. DECRQM ( old=$(stty -g); stty raw -echo min 0 time 3
printf '\033[?1007$p'; dd bs=32 count=1 2>/dev/null | cat -v; stty "$old"On this Mac:
Terminal.app doesn't answer DECRQM at all, so it can't tell us anything here. Ghostty reports the mode as set out of the box. SwiftTerm answers for the neighbouring modes but not for 1007, which is what this PR changes. What the mode actually does. This puts the terminal on the alternate screen with mouse tracking off, prints whatever arrives, and toggles 1007 with old=$(stty -g); trap 'stty "$old"; printf "\033[?1049l"' EXIT
printf '\033[?1049h\033[?1000l\033[?1002l\033[?1003l\033[?1006l'
stty raw -echo min 0 time 1; on=1
while :; do c=$(dd bs=64 count=1 2>/dev/null); [ -z "$c" ] && continue
case "$c" in q*) break;; t*) [ $on -eq 1 ] && { printf '\033[?1007l'; on=0; } || { printf '\033[?1007h'; on=1; }; continue;; esac
printf 'got: %s\r\n' "$(printf '%s' "$c" | cat -v)"; doneIn Ghostty 1.3.1, scrolling with the mode set gives you On the default value I have no strong opinion and it's your call — Ghostty ships |
|
Thanks, will try |
|
ok, this changes the state management, but does not change the actual handling of the scroll wheel - I do have a system to test, but I suspect you might. The issue is this: That code likely needs to check the new setting - so we are sending the events regardless of the setting. I think the test needs to be update to include "&& terminal.alternateScrollMode" |
Tracking the mode is not enough: TerminalView translated the wheel into cursor keys whenever the alternate screen was active, so an application could set or reset 1007 and see no difference. Gate that translation on terminal.alternateScrollMode; with the mode reset the wheel produces nothing, matching xterm (the alternate buffer has no scrollback to move either). The check sits before the delta accumulator rather than inside the alternate-screen branch. Sub-line trackpad deltas return early at `lines == 0`, so a check further down never sees them and their remainder stays banked in the accumulator — five suppressed sub-line events followed by DECSET 1007 then made the next event emit a cursor key it had not earned. Deciding the route first also lets the existing mouse-reporting condition be named once instead of twice. Also documents 1007 in the DECRST comment list, which the previous commit only added to the DECSET side. Tests: the wheel emits a cursor key with the mode set and nothing with it reset, and motion accumulated while suppressed is not spent once the mode returns.
|
You're right, and thanks — the view kept sending cursor keys either way, so an application could set or reset 1007 and see exactly nothing. Fixed in b00cc89. One deliberate difference from what you sketched: the check sits a bit earlier instead of being Two tests came along: the wheel emits a cursor key with the mode set and nothing with it reset, and motion accumulated while suppressed isn't spent once the mode comes back. Both fail without the fix. Full suite is at 704 passing. The default is still |
|
Thank you! |
What
Private mode 1007 is xterm's Alternate Scroll Mode, which
ctlseqsdocuments as corresponding to thealternateScrollresource: while the alternate screen is active and the application has not enabled mouse tracking, the terminal translates wheel input into cursor up/down keys, so full-screen programs that do not read the mouse (less,vimwithoutmouse=a) still respond to the scroll wheel.SwiftTerm currently drops 1007 into the
Unhandled DEC Private Mode Set/Resetbranch. That has two consequences for an embedder that implements the translation (as SwiftTerm's own macOS view effectively does today, unconditionally): an application has no way to turn it off, andDECRQMcannot answer a query about it even though the neighbouring mouse modes 1000–1006 all can.What this does
Tracks the state only, exposed as
Terminal.alternateScrollMode. Translating wheel events stays with the host view, consistent with how the other mouse-related state is handled. Wired intoDECSET,DECRST,DECRQMandRIS, mirroring how 1004 is done.One choice worth a second opinion
The default here is true, matching Ghostty (
src/terminal/modes.zigmarksmouse_alternate_scrolldefault true), while xterm's ownalternateScrollresource defaults to false.truekeeps the wheel working out of the box inless/vim, which is what users of macOS terminals tend to expect, and it preserves SwiftTerm's current behaviour for hosts that already translate unconditionally. If you would rather follow the reference implementation, flipping it tofalseis a one-line change — happy to do that.Tests
Tests/SwiftTermTests/AlternateScrollModeTests.swift: default value, DECSET/DECRST toggling,DECRQMreporting1$y/2$y,RISrestoring the default, and independence from alt-screen / mouse-tracking state.Full suite: 704 tests in 62 suites, all passing.