Skip to content

Add scrollbar, mouse wheel scroll, and paste features#138

Merged
Aaronontheweb merged 10 commits into
devfrom
feature/scrollbar-paste-mouse-scroll
Feb 24, 2026
Merged

Add scrollbar, mouse wheel scroll, and paste features#138
Aaronontheweb merged 10 commits into
devfrom
feature/scrollbar-paste-mouse-scroll

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Owner

Summary

Implements three input/display enhancements to Termina:

  • Scrollbar for StreamingTextNode showing scroll position
  • Bracketed paste mode with Claude Code-style UX (summary placeholder, submit on Enter)
  • Mouse wheel scrolling support for IScrollable components

Changes

Scrollbar

  • ScrollbarOptions record for customization (track/thumb chars and colors, AutoHide)
  • WithScrollbar() fluent methods on StreamingTextNode
  • GetMaxScrollOffset() exposed in PersistedStreamBuffer
  • Visual scrollbar renders on right edge when content exceeds viewport

Paste Mode

  • PasteEvent and IPasteReceiver interfaces
  • TextInputNode implements IPasteReceiver
  • Paste shows summary: [Pasted 500 lines, 12345 chars]
  • Full content submitted on Enter (with newlines preserved)
  • Any edit action clears paste and returns to normal mode
  • AnsiCodes.EnableBracketedPaste / DisableBracketedPaste constants
  • Multi-line pastes no longer trigger individual submissions

Mouse Wheel Scroll

  • MouseScrollEvent input event
  • IScrollable interface for scrollable components
  • Automatic routing of mouse scroll to focused IScrollable
  • EscapeSequenceParser detects SGR mouse scroll sequences

Documentation

  • Updated streaming-text-node.md with scrollbar and mouse scroll sections
  • Updated text-input-node.md with paste handling documentation
  • Updated input-handling.md with PasteEvent and MouseScrollEvent details

Closes #135
Closes #134
Closes #136

)

## StreamingTextNode scrollbar (#135)

- Expose `PersistedStreamBuffer.GetMaxScrollOffset()` as public
- Add `ScrollbarOptions` record (TrackChar, ThumbChar, TrackColor, ThumbColor, AutoHide)
- `StreamingTextNode.WithScrollbar()` / `WithScrollbar(options)` fluent API
- Scrollbar rendered in rightmost column; thumb position inverted to match
  the 0=bottom scroll convention; auto-hides when content fits the viewport

## Bracketed paste mode (#134)

- `AnsiCodes.EnableBracketedPaste` / `DisableBracketedPaste` (ESC[?2004h/l)
- `PasteEvent(string Content) : IInputEvent`
- `IPasteReceiver` interface; `TextInputNode` implements it — pastes insert
  text at cursor without firing `Submitted`, newlines skipped, MaxLength respected
- `TerminaApplication` enables bracketed paste on startup and disables in finally
- `ConsoleInputSource` extended with a 4-state machine (Normal → AfterEscape →
  InBracketSequence → PasteBuffering) with 50 ms ESC timeout
- `TerminaApplication.ProcessEvent` routes `PasteEvent` to focused `IPasteReceiver`
  or falls through to the ViewModel input observable

## Mouse wheel scroll (#136)

- `AnsiCodes.EnableMouseButtonTracking` / `DisableMouseButtonTracking` (ESC[?1002h/l)
- `MouseScrollEvent(int Delta) : IInputEvent` (positive = up / older content)
- `IScrollable` interface (CanScrollUp, CanScrollDown, ScrollUp, ScrollDown)
- `StreamingTextNode` implements `IScrollable`; caches viewport dims in `Render()`
  so scroll calls outside the render loop use the correct wrap width
- `ConsoleInputSource` detects SGR mouse sequences ESC[<64;…M (up) / ESC[<65;…M (down)
- `TerminaApplication` enables mouse button tracking + SGR on startup; routes
  `MouseScrollEvent` to focused `IScrollable`, or falls through to ViewModel

## Streaming demo wired up

- `StreamingChatPage`: `.WithScrollbar()` on chat history, paste subscription,
  mouse scroll subscription via `IScrollable`, updated status bar hints
…se mode

?1002h (EnableMouseButtonTracking) generates click/release events for every
tmux pane interaction, producing escape sequences that could be partially
parsed into stray ESC keypresses and trigger RequestShutdown().

?1000h (EnableMouseNormal) + ?1006h (SGR) is sufficient for scroll wheel
events (button 64/65 in SGR format) without the noisy button tracking noise.
Replace direct Console.Write of ?1002h with _terminal.EnableMouse() which
already uses the correct combination, and update the AnsiCodes doc comment
to warn about the tmux incompatibility.
…arser

PlatformInputSource was wrapping every ConsoleKeyEvent directly as KeyPressed
with no escape sequence detection. Mouse click events (ESC[<0;x;yM) arrived
as a ConsoleKey.Escape followed by raw chars; that bare ESC reached the ViewModel
and triggered RequestShutdown().

Extract EscapeSequenceParser (internal, testable with injectable clock) that
handles:
- SGR mouse scroll (button 64/65) → MouseScrollEvent
- SGR mouse clicks/releases → silently consumed
- Bracketed paste → PasteEvent
- Standalone ESC (50ms timeout) → KeyPressed(Escape)

Wire it into PlatformInputSource so all ConsoleKeyEvents pass through the
parser. When the parser is buffering an escape, ReadInputWithTimeoutAsync
races against a 50ms CTS to detect a standalone ESC without blocking.

Add EscapeSequenceParserTests covering scroll, click consumption, timeout,
paste, and post-event key routing (19 tests).
Update the demo status bar hint to explicitly show Ctrl+Shift+V as the
paste shortcut, clarifying that terminal-native paste (not tmux paste
buffer) is required for bracketed paste mode detection to work.

Add a debug trace in PlatformInputSource when a PasteEvent is emitted,
making it easier to confirm whether ESC[200~...ESC[201~ markers are
reaching the application during diagnostic sessions.
When running inside tmux, the inner-pane ESC[?2004h is intercepted by tmux
and never reaches the outer terminal emulator. This means Ctrl+Shift+V pastes
from the outer terminal arrive as raw text (no ESC[200~...ESC[201~ markers),
so bracketed paste detection never fires.

Fix: detect the $TMUX environment variable and also send ESC[?2004h to the
outer terminal via a DCS passthrough sequence (\ePtmux;\e\e[?2004h\e\\).
Once the outer terminal has bracketed paste mode enabled, it wraps Ctrl+Shift+V
pastes with the markers, which tmux then forwards to the inner pane.

Requires set -g allow-passthrough on in ~/.tmux.conf (tmux 3.3+).

Add AnsiCodes.TmuxPassthrough(string seq) helper that wraps any escape
sequence in the DCS passthrough format with properly doubled ESC bytes.
TextInputNode.HandlePaste previously called string.Insert per character,
causing quadratic performance for large pastes (41K+ chars). Now filters
newlines in a single StringBuilder pass and does one bulk Insert.

Added trace logging to EscapeSequenceParser state transitions for
diagnosing paste/mouse escape sequence processing. Zero overhead when
tracing is disabled (single inlined boolean check).
Paste now shows a summary placeholder (e.g., "[Pasted 500 lines, 12345 chars]")
instead of inserting raw content. The full paste content including newlines is
preserved and submitted verbatim on Enter. Any editing action (typing, backspace,
delete, escape) clears the paste and returns to normal input mode. This prevents
newlines in pasted content from triggering individual submissions.
- streaming-text-node.md: Document WithScrollbar(), ScrollbarOptions,
  mouse wheel scrolling, IScrollable, and CanScrollUp/CanScrollDown
- text-input-node.md: Document paste handling behavior, IPasteReceiver,
  and HandlePaste method
- input-handling.md: Add MouseScrollEvent and PasteEvent to input events,
  document automatic routing to IScrollable/IPasteReceiver components
@Aaronontheweb Aaronontheweb enabled auto-merge (squash) February 24, 2026 02:48
@Aaronontheweb Aaronontheweb enabled auto-merge (squash) February 24, 2026 02:56
@Aaronontheweb Aaronontheweb merged commit 3f3bef2 into dev Feb 24, 2026
7 checks passed
@Aaronontheweb Aaronontheweb deleted the feature/scrollbar-paste-mouse-scroll branch February 24, 2026 02:58
@Aaronontheweb Aaronontheweb mentioned this pull request Feb 24, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant