Add F6 selection mode to hand mouse back to terminal#194
Closed
Aaronontheweb wants to merge 1 commit into
Closed
Conversation
Termina enables SGR mouse tracking globally to capture scroll-wheel events, which has the side effect of blocking the terminal emulator's native click-and-drag text selection. Users running Termina apps could not copy output from the screen without taking screenshots. F6 now toggles a framework-level selection mode that temporarily disables app-side mouse tracking, letting the terminal handle selection natively with its own visual feedback and OS clipboard integration. A yellow banner across the top of the screen signals the mode is active; Escape or F6 again exits. The key is intercepted before page capture so pages cannot shadow it, and Escape-while-active is swallowed so page-level Escape bindings do not fire on exit. Fixes #192
codymullins
added a commit
to codymullins/termina
that referenced
this pull request
May 19, 2026
…racking Replaces the application's startup call to EnableMouse() (which enabled CSI ?1000h + CSI ?1006h to capture mouse-wheel events) with a new EnableWheelScroll() call that emits CSI ?1007h. While in the alternate screen buffer, the terminal translates wheel events into cursor up/down key sequences without any mouse tracking, so: - Native click-drag text selection works again (fixes Aaronontheweb#192). - Triple-click word/line selection works. - Middle-click paste works. - OS clipboard integration via the terminal works. - PR Aaronontheweb#194's F6 selection-mode toggle becomes unnecessary. - The tmux ESC-keypress crash class that drove the ?1002h → ?1000h migration goes away — the terminal sends no mouse escape sequences. EnableMouse()/DisableMouse() are kept for apps that genuinely need clicks/drags (e.g. an in-app file picker) and can opt in explicitly. EscapeSequenceParser keeps its silent consumption of SGR mouse events as a defensive safety net. Caveats: - Wheel events arrive as bare UpArrow/DownArrow keypresses and are indistinguishable from real keyboard arrows. Apps with always-focused text inputs (e.g. chat) may want to treat Shift+Wheel separately or focus the scrollable explicitly. The Streaming demo's existing MouseScrollEvent subscription will no longer fire under the default wheel-scroll mode; PgUp/PgDn is the documented keyboard fallback. - Legacy conhost.exe ignores ?1007h. Modern Windows Terminal supports it. Affected users keep keyboard scrolling as a fallback. Adds tests for the new VirtualTerminal/DiffingTerminal/AnsiCodes surfaces. Full suite: 1026 / 1026 passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Aaronontheweb
added a commit
that referenced
this pull request
May 23, 2026
…07h) (#215) * Prototype: use xterm alternate-scroll (?1007h) instead of SGR mouse tracking Replaces the application's startup call to EnableMouse() (which enabled CSI ?1000h + CSI ?1006h to capture mouse-wheel events) with a new EnableWheelScroll() call that emits CSI ?1007h. While in the alternate screen buffer, the terminal translates wheel events into cursor up/down key sequences without any mouse tracking, so: - Native click-drag text selection works again (fixes #192). - Triple-click word/line selection works. - Middle-click paste works. - OS clipboard integration via the terminal works. - PR #194's F6 selection-mode toggle becomes unnecessary. - The tmux ESC-keypress crash class that drove the ?1002h → ?1000h migration goes away — the terminal sends no mouse escape sequences. EnableMouse()/DisableMouse() are kept for apps that genuinely need clicks/drags (e.g. an in-app file picker) and can opt in explicitly. EscapeSequenceParser keeps its silent consumption of SGR mouse events as a defensive safety net. Caveats: - Wheel events arrive as bare UpArrow/DownArrow keypresses and are indistinguishable from real keyboard arrows. Apps with always-focused text inputs (e.g. chat) may want to treat Shift+Wheel separately or focus the scrollable explicitly. The Streaming demo's existing MouseScrollEvent subscription will no longer fire under the default wheel-scroll mode; PgUp/PgDn is the documented keyboard fallback. - Legacy conhost.exe ignores ?1007h. Modern Windows Terminal supports it. Affected users keep keyboard scrolling as a fallback. Adds tests for the new VirtualTerminal/DiffingTerminal/AnsiCodes surfaces. Full suite: 1026 / 1026 passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Termina.Demo.WheelAmbiguity demo Demonstrates the wheel-vs-arrow ambiguity introduced by switching from SGR mouse tracking (?1000h + ?1006h) to xterm alternate-scroll mode (?1007h). The terminal translates wheel events into bare Up/Down arrow keypresses, which a focused TextInputNode happily consumes as cursor movement — so spinning the wheel over a scrollable history panel moves the input cursor instead of scrolling the history. Layout: - Top: pre-populated StreamingTextNode (100 lines, scrollable) - Middle: pre-filled TextInputNode (always focused) - Bottom: live counters for bare Up/Down arrows and MouseScrollEvents PgUp/PgDn still scrolls the history (keyboard fallback), Ctrl+Q quits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Initialize demo nodes in OnBound, not OnNavigatedTo BuildLayout() runs inside base.OnNavigatedTo() before any code after the base call executes, so _history/_input were still null when the layout was first built. Move node construction to OnBound() (called at bind time, before navigation) and keep input subscriptions in OnNavigatedTo so they're re-created on each visit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Termina.Demo.WheelScrollWorks: working wheel-scroll demo Counterpart to Termina.Demo.WheelAmbiguity. The ViewModel injects IAnsiTerminal and calls EnableMouse() to re-enable full SGR mouse mode (?1000h + ?1006h) on top of the framework's default ?1007h. The EscapeSequenceParser then emits MouseScrollEvent for wheel ticks, which the page forwards to the (non-focusable) StreamingTextNode. This demonstrates that the framework's auto-routing of MouseScrollEvent only fires when the focused widget is itself IScrollable; for chat-shaped pages where TextInputNode is focused, the page must subscribe and forward manually. Tradeoff documented in code: while EnableMouse() is active, the host terminal stops handling click-drag text selection (Shift/Option to override). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Make ?1007h alternate-scroll mode actually scroll Disambiguate wheel-as-arrow events from real keyboard arrows by also enabling DECCKM (CSI ?1h) when alternate scroll is turned on: - Real keyboard arrows: SS3 form (ESC O A/B), decoded by Console.ReadKey directly to ConsoleKey.UpArrow/DownArrow. Never reach the EscapeSequenceParser. - Mouse wheel under ?1007h: CSI form (ESC [ A/B), unchanged by DECCKM, falls through Console.ReadKey as raw bytes. The parser now recognizes bare CSI A/B in InBracketSequence as MouseScrollEvent(+1)/(-1) — the same event type already routed by TerminaApplication to the focused IScrollable. Pages can keep their existing MouseScrollEvent subscriptions; they will now fire for wheel ticks while native click-drag text selection remains available. - AnsiCodes: add Enable/DisableCursorKeyApplicationMode constants - AnsiTerminal.EnableWheelScroll: emit ?1007h + ?1h (disable both on teardown) - EscapeSequenceParser: recognize ESC[A / ESC[B as MouseScrollEvent - Tests: parser tests for the new wheel-as-CSI-arrow path, AnsiCodes tests for the new constants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Implement UnixConsole with raw stdin reads Console.ReadKey on Unix has hardcoded recognition for both CSI (ESC[A) and SS3 (ESC OA) arrow forms, so DECCKM (?1h) alone can't disambiguate mouse-wheel-as-arrow events from real arrow keypresses — the raw bytes never reach EscapeSequenceParser. To make ?1007h alternate-scroll mode actually scroll, we have to bypass Console.ReadKey entirely. UnixConsole now puts the terminal into raw mode via termios+cfmakeraw (preserving OPOST so logging output isn't staircased), reads bytes directly from Console.OpenStandardInput, and emits one ConsoleKeyEvent per byte. EscapeSequenceParser gets a new InSs3Sequence state so that real arrow keys (which now arrive as raw ESC O A/B/C/D bytes under DECCKM) are reassembled into normal KeyPressed events while wheel ticks (ESC [ A/B) continue to map to MouseScrollEvent. PlatformConsoleFactory wires UnixConsole on Linux/macOS when stdin is a TTY; piped/redirected scenarios still use FallbackConsole. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert UnixConsole raw-stdin path The raw-termios + raw-stdin path made interactive behavior worse on macOS Terminal (per user report). Likely culprits include termios c_cc offsets, OPOST handling, ReadAsync vs VMIN/VTIME interaction, and turning Ctrl+C into a byte (cfmakeraw clears ISIG) which breaks app shutdown for demos that don't bind a quit shortcut. Reverting UnixConsole back to the issue-#80 stub and reverting PlatformConsoleFactory to fall through to FallbackConsole on Unix. The SS3-arrow and CSI-arrow parser cases (and their tests) stay — they're inert until we get a raw-byte input source and are useful for the future fix. Net result for now: ?1007h alternate-scroll mode is still not usable on its own through Console.ReadKey. Use Termina.Demo.WheelScrollWorks (which opts into ?1000h via IAnsiTerminal.EnableMouse) as the recommended wheel-scroll path. A proper Unix raw-stdin implementation remains tracked under issue #80. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Termina.Demo.RawStdinProbe Standalone raw-stdin probe — no Termina dependency — used to verify termios layout and raw byte streams on the user's actual terminal before the real UnixConsole implementation lands. Prints computed VMIN/VTIME/ c_oflag offsets, enters raw mode, reads bytes directly via libc.read(), and annotates recognized wheel/arrow escape sequences so the outcome of ?1007h + DECCKM is readable at a glance. Restores termios on Dispose / ProcessExit / Console.CancelKeyPress / unhandled exception. Quits on 'q' or Ctrl+C (0x03 byte, since cfmakeraw clears ISIG). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Implement UnixConsole raw-stdin reader behind TERMINA_UNIX_RAW_INPUT Replaces the NotImplementedException stub with a real implementation that bypasses Console.ReadKey on Unix so ?1007h alternate-scroll-mode wheel events can be distinguished from real arrow keys (DECCKM SS3 forms). Hidden behind the TERMINA_UNIX_RAW_INPUT=1 env var until the opt-in default is flipped in a later change. Design points learned from the prior reverted attempt (8be2d4e): * Direct libc.read() on a dedicated background thread instead of Console.OpenStandardInput().ReadAsync — the Stream wrapper's interaction with VMIN/VTIME wasn't reliable. * termios offsets (c_oflag, c_cc base, VMIN, VTIME) match what the RawStdinProbe runtime-verifies via tcgetattr readback. * OPOST re-enabled after cfmakeraw so logging isn't staircased. * ISIG cleared by cfmakeraw — Ctrl+C arrives as 0x03 and is mapped to KeyPressed(C, Control), matching FallbackConsole's TreatControlCAsInput behavior (user-confirmed in plan). * UTF-8 multi-byte text input reassembled via System.Text.Decoder so typing non-ASCII chars produces one ConsoleKeyEvent per codepoint; ASCII bytes (incl. all escape-sequence bytes) flow one-per-event so EscapeSequenceParser can keep reassembling CSI/SS3. * Channel<IConsoleInputEvent> for the reader→consumer queue; cancellation flows through Channel.ReadAsync naturally. * termios restored on Dispose, AppDomain.ProcessExit, UnhandledException, and Console.CancelKeyPress. Resize detection is still poll-based at the 100 ms VTIME cadence; SIGWINCH integration is the next change. 26 new tests cover byte→KeyInfo mapping for control/printable/Ctrl+ letter/high-bit bytes, CSI arrow → MouseScrollEvent integration, SS3 arrow → KeyPressed integration, and UTF-8 multi-byte decoding through the same one-byte-at-a-time path the reader thread uses. Full suite: 1062/1062 passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add SIGWINCH-driven resize to UnixConsole Use PosixSignalRegistration.Create(PosixSignal.SIGWINCH, ...) so terminal resizes are observed immediately instead of waiting for the next 100 ms VTIME tick in the reader thread. The handler runs on a thread-pool thread, so CheckResize() now takes a lock to coordinate with the reader thread's opportunistic polling. The VTIME-tick poll is kept as a belt-and-suspenders fallback for environments where the signal registration is unavailable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Probe: enter alt screen (?1049h) so ?1007h actually emits wheel events Per xterm.ctlseqs, ?1007h alternate-scroll-mode only emits CSI A/B for wheel ticks while on the alternate screen buffer. The real Termina framework enters alt screen via ?1049h on startup, so wheel-as-arrow works there; the bare probe was missing it, which is why a Ghostty test run showed arrows + typing working perfectly but wheel emitted nothing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Probe: optional kitty keyboard PUSH via TERMINA_KITTY_FLAGS Adds opt-in 'CSI > <flags> u' push (and matching 'CSI < u' pop on exit) gated on the TERMINA_KITTY_FLAGS env var. Extends AnnotateSequence to recognize: CSI <num>;<mods>[:event] u (kitty CSI-u key events) CSI 1;<mods> [ABCDEFHPQRS] (kitty second-form arrows / Home/End / F1-F4) CSI [ABCD] with no params (wheel under ?1007h OR plain arrow) and decodes the standard kitty PUA functional keycodes (Up=57352..) and modifier bitmask. Default behavior unchanged when env var is unset. Usage: TERMINA_KITTY_FLAGS=8 dotnet run --project demos/Termina.Demo.RawStdinProbe Goal: empirically determine which kitty flag combo produces distinguishable bytes for wheel-up vs Up-arrow in Ghostty under ?1007h. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Parser: kitty keyboard CSI-u + second-form support Add KittyKeyboardActive flag to EscapeSequenceParser. When active: - Bare CSI A/B/C/D/H/F/P-S route as KeyPressed (kitty canonical no-mod form) - SS3 OA/OB invert to MouseScrollEvent (wheel under ?1007h+DECCKM) - SS3 OC/OD stay as KeyPressed (wheel has no horizontal axis) Extend TryParseCsiU with optional :event-type subfield (swallow non-press) and trailing ;text-codepoints. Swallow PUA modifier-alone keycodes (57441-57454). Map PUA functional keycodes (57344-57375) to ConsoleKey. Add TryParseKittySecondForm for CSI 1;<mods>[:<event>] [ABCDEFHPQRS]. 25 new tests in EscapeSequenceParserKittyTests.cs cover bare-CSI routing in both modes, all functional finals, SS3 inversion, second-form modifier combos, event-type swallowing, PUA arrows / F5 / modifier-alone swallow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * UnixConsole: opt-in kitty keyboard protocol via TERMINA_KITTY_KEYBOARD When TERMINA_KITTY_KEYBOARD is set to a positive int (e.g. 8 for report_all_keys), UnixConsole pushes 'CSI > N u' after entering raw mode and pops 'CSI < u' before restoring termios. UnixConsole exposes the state as KittyKeyboardActive; PlatformInputSource propagates it to EscapeSequenceParser.KittyKeyboardActive so the parser routes bare CSI A/B as KeyPressed (real arrows) and SS3 OA/OB as MouseScrollEvent (wheel under ?1007h+DECCKM), giving structurally correct wheel-vs-arrow disambiguation while preserving native click-drag text selection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Demo: Termina.Demo.KittyScroll — wheel-vs-arrow disambiguation via kitty History panel scrolls on wheel, arrow keys only tick counters; native click-drag selection still works because no mouse tracking is enabled. Run: TERMINA_UNIX_RAW_INPUT=1 TERMINA_KITTY_KEYBOARD=8 \ dotnet run --project demos/Termina.Demo.KittyScroll Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * TerminaApplication: double-press Ctrl+C to quit (with hint) First Ctrl+C shows a 'Press Ctrl+C again to quit' toast at the bottom center for 2 seconds; a second Ctrl+C within that window calls Shutdown(). Sits above page / focus handling so users can always exit, even from a focus-trapping input control. Under raw-mode UnixConsole (cfmakeraw clears ISIG) Ctrl+C arrives as a KeyPressed(C, Control) event, which this handler intercepts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * KittyScroll demo: add focused TextInputNode (AI-harness style) History panel + status line + focused input box. Enter echoes the message back into the history. Arrows move the input cursor (focused); wheel still scrolls history (falls through to the page since the input is not IScrollable). Removed the local Ctrl+Q binding — use the new framework Ctrl+C×2 to quit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * KittyScroll demo: auto-focus the input on navigation FocusPolicy.FirstFocusable so the TextInputNode actually receives keystrokes when the page loads. Without this the page defaulted to FocusPolicy.Manual and typing went nowhere visible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove superseded prototype demos (WheelAmbiguity, WheelScrollWorks, RawStdinProbe) Keep only Termina.Demo.KittyScroll — the final prototype demo demonstrating wheel-vs-arrow disambiguation via the kitty keyboard protocol + ?1007h, with a focused TextInput / AI-harness layout and Ctrl+C×2 to quit. The three removed demos were intermediate diagnostic/illustrative projects used during the investigation: - RawStdinProbe: standalone byte-level termios probe - WheelAmbiguity: reproduction of the original ?1007h ambiguity - WheelScrollWorks: intermediate proof that wheel-as-arrow scrolled in non-focus-trapping pages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * add windows support for kkb scroll * update docs for ctrl+c Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Add comment for why 'E' (KP_Begin) is swallowed/unmapped --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Aaron Stannard <aaron@petabridge.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
EnterSelectionMode()/ExitSelectionMode()are also exposed publicly for programmatic control.Fixes #192. Supersedes and closes #193 (the previous PR attempted app-owned selection via
HandleMouseEvent, but the escape-sequence parser silently consumes clicks/drags and the terminal wasn't configured for drag motion, so that code path was unreachable end-to-end on Linux).Why terminal-native selection instead of app-owned
SGR mouse mode is all-or-nothing — there's no way to subscribe only to the scroll wheel without also capturing clicks and drags. The simplest fix for the Netclaw use case is to temporarily hand the mouse back to the terminal on demand, which gets us working selection with zero coordinate math, no tmux passthrough gymnastics, and no custom highlight rendering. The tradeoff is that scroll wheel stops working while selection mode is active — pages with scrollable content already expose PgUp/PgDn keyboard equivalents.
Why F6 and not Ctrl+Shift+S
F6 is a bare function key with a standard escape sequence that
Console.ReadKeyparses universally. It works without the kitty keyboard protocol and survives tmux without requiring passthrough config. Ctrl+Shift+S would only be distinguishable from Ctrl+S on kitty-protocol-capable terminals — in plain GNOME Terminal or tmux it'd collapse to XOFF.Test plan
dotnet test— full suite 1010/1010 passing, including 8 newSelectionModeToggleTestscovering direct API idempotency, F6 toggle throughProcessEvent, Escape-in-mode exits + swallows, Escape-outside still reaches the page, Shift+F6 not intercepted.Termina.Demo.Streaming: press F6, verify banner appears, click-drag selection works in host terminal (GNOME Terminal / Kitty / WezTerm), scroll wheel disabled while mode is active, F6/Esc exits, scroll wheel resumes.