Skip to content

Add F6 selection mode to hand mouse back to terminal#194

Closed
Aaronontheweb wants to merge 1 commit into
devfrom
feature/selection-mode-toggle
Closed

Add F6 selection mode to hand mouse back to terminal#194
Aaronontheweb wants to merge 1 commit into
devfrom
feature/selection-mode-toggle

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Owner

Summary

  • Framework-level F6 toggle temporarily disables app-side SGR mouse tracking so the terminal emulator can handle native click-and-drag text selection, with its own visual feedback and OS clipboard integration.
  • Yellow banner overlay signals the mode is active; Escape or F6 again exits. Escape is swallowed while active so page-level Escape bindings (e.g. navigate back, cancel generation) don't fire on exit.
  • Key interception runs before page capture so pages cannot shadow it. 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.ReadKey parses 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 new SelectionModeToggleTests covering direct API idempotency, F6 toggle through ProcessEvent, Escape-in-mode exits + swallows, Escape-outside still reaches the page, Shift+F6 not intercepted.
  • Manual test in 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.
  • Manual test inside tmux to confirm F6 reaches the app and mouse hand-off works through tmux's own mouse handling.

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>
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.

Unable to select and copy arbitrary text from chat terminal

1 participant