Skip to content

refactor(ui)!: remove alternate screen, mouse capture, and in-app scrolling - #243

Open
wowi42 wants to merge 6 commits into
gi-dellav:mainfrom
wowi42:refactor/remove-mouse-capture
Open

refactor(ui)!: remove alternate screen, mouse capture, and in-app scrolling#243
wowi42 wants to merge 6 commits into
gi-dellav:mainfrom
wowi42:refactor/remove-mouse-capture

Conversation

@wowi42

@wowi42 wowi42 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Why

Two terminal-takeover behaviors break the host terminal's native affordances:

  1. Mouse capture hijacks the terminal emulator's native text selection — users cannot select/copy text with the mouse while the app runs.
  2. The alternate screen (vim/neovim-style) disables the terminal's native scrollback, forcing zerostack to re-implement scrolling in-app — duplicating what the terminal already does for free. As the maintainer put it: the shell doesn't implement scrolling, the terminal does.

Two related input-box UX issues are fixed along the way (same terminal-native theme): long input lines horizontally scrolled (the whole line visibly shifted while typing at the window edge), and cursor placement for edits was char-by-char only.

What is removed

  • Mouse support — capture, wheel scroll, click-to-place-cursor, click-to-open links, drag-selection (~520 lines).
  • The alternate screen — output prints inline like a normal shell command; finalized lines are printed once (block/line watermark) and scroll into native terminal history.
  • In-app scrolling — scroll_offset machinery, PageUp/PageDown/Home/End handlers, scroll indicators. Native terminal scrollback replaces it.
  • Horizontal input scrolling — long input lines now soft-wrap to the window width instead of shifting sideways.
  • The alt-screen dances in sub-shell escapes ($EDITOR, lazygit, /init, /tutor, Ctrl+G) — replaced by suspend/redraw of the live block.

What is added

  • Word-wise cursor movement: Alt+←/→ and Ctrl+←/→ jump by word (alongside the existing Alt+B/F), Ctrl+Backspace deletes the previous word (yankable with Ctrl+Y). Home/End now reach the input editor (buffer start/end) since they no longer scroll the chat. Note: mouse click-to-position cannot come back — it requires the same mouse reporting that breaks native selection.
  • TUI-safe logging: warn-and-above logs no longer print to the terminal mid-session (a stray write corrupted the inline display). Interactive mode never logs to stderr; --log-level / RUST_LOG are redirected to a timestamped log file. Print mode (-p) keeps stderr logging.
  • Picker rendering fixed: / command picker, models picker, etc. now render inside the live block so their rows are erased on close. Painting directly to stdout above the live block left permanent remnants in the inline model.

What stays the same

Input box, statusline, streaming, spinners, permission/chain prompts, picker UX — a small live block at the bottom redraws in place as before. Links remain clickable via terminal-native OSC8. /clear behaves like shell Ctrl-L; /undo prints a rewind banner instead of un-printing.

Trade-offs

  • Printed lines are immutable: no re-wrap on terminal resize (same as the shell).
  • No in-app scrollback: you scroll with the terminal's own scrollbar/wheel — that is the point.

Verification

  • cargo test: 783 passed / 0 failed (default) — incl. reworked headless loop tests, watermark unit tests, input-wrap, keybinding, logging-routing and picker-erase tests
  • cargo clippy --all-features / --no-default-features: zero warnings; CI green on all 12 jobs
  • Pty smoke runs: no alt-screen sequences emitted; transcript prints inline; 100-char input soft-wraps; /+Esc and /help leave no picker remnants; clean exit

Enabling crossterm mouse capture hijacks the terminal emulator's native
text selection: users cannot select text with the mouse while the app
runs. Wheel-scroll events require the same mouse-reporting mode, so
there is no partial fix — capture stays on or goes off entirely.

Decision: drop mouse capture and all mouse-driven features (wheel
scroll, click-to-place-cursor, drag selection/copy, click-to-open
links) to shrink the codebase and restore native terminal selection
and OSC8 link clicking handled by the terminal emulator itself.

Keyboard scrolling (PageUp/PageDown/Home/End) is unchanged, and the
multiline input viewport still follows the cursor.
@gi-dellav

Copy link
Copy Markdown
Owner

Can you pls rebase on last commit? Everything looks ready to merge

wowi42 added 2 commits August 3, 2026 00:16
…se-capture

Keep the headless test infrastructure from gi-dellav#241 (RenderBackend/FakeBackend,
tui_loop_tests) alongside the mouse-support removal; the scroll/resize loop
test now drives scrolling through PageUp/PageDown keys since the wheel
UserEvent variants are gone.
Remove the alternate screen so the terminal's native scrollback and
mouse text selection work like a normal shell, and delete the in-app
scroll feature (PageUp/PageDown/Home/End handlers, scroll_offset
machinery, SCROLL indicators) that native scrollback replaces.

New rendering model (renderer.rs):
- Committed lines: finalized feed content is printed exactly once into
  native scrollback, tracked by a (block, line, width) print watermark
  with pure advance/clamp/commit-count math (unit-tested).
- Live block: the only redrawn region, bottom-anchored — streaming
  region (tail of the running feed block + partial scratch) above the
  input box and statusline. Redraws are relative (cursor up, clear
  below); the cursor never moves above the block start.
- Over-tall running blocks spill their top lines into scrollback;
  resize keeps printed lines' old wrap and remaps the watermark to the
  same (block, line); /clear wipes the screen shell-style and resets
  the watermark; feed rebuilds (undo/redo/rewind) clamp the watermark
  so nothing reprints.
- Sub-shell escapes ($EDITOR, Ctrl+G editor, Ctrl+H lazygit, /init,
  /tutor) become suspend (erase live block, raw off) / resume (raw on,
  redraw block) instead of alt-screen dances; /docs prints inline.
- Pickers paint just above the live block using its current height.

TerminalGuard keeps raw mode, bracketed paste and keyboard enhancement
flags, and anchors the UI to the bottom row at startup; teardown erases
the live block and leaves the cursor on a fresh line.
@wowi42 wowi42 changed the title refactor(ui)!: remove mouse capture to restore native terminal selection refactor(ui)!: terminal-native UI — drop mouse capture, alternate screen, and in-app scrolling Aug 3, 2026
@wowi42 wowi42 changed the title refactor(ui)!: terminal-native UI — drop mouse capture, alternate screen, and in-app scrolling refactor(ui)!: remove alternate screen, mouse capture, and in-app scrolling Aug 3, 2026
wowi42 added 3 commits August 3, 2026 09:35
Long input lines no longer scroll horizontally: each hard line is
soft-wrapped at the text width (cols minus prompt) and occupies 1..N
display rows, with the existing vertical windowing (input capped to
~30% of rows) and caret mapping now operating on wrapped rows. The
input_scroll_offset machinery is gone; wrap math lives in pure
pub(crate) fns wrap_input_segments / wrapped_cursor with unit tests.

New keybindings in the input editor:
- Alt+Left / Alt+Right: previous word start / next word end
  (same as the existing Alt+B / Alt+F).
- Ctrl+Left / Ctrl+Right: same word jumps.
- Ctrl+Backspace: delete previous word into the kill ring.
Plain and Shift+ arrows keep char-wise movement.

/help keys section and docs/COMMANDS.md list the new bindings.
The stderr tracing layer (default filter warn) printed log lines straight
to the terminal mid-session. The alternate screen used to hide this behind
full repaints; with inline rendering a stray write shifts the screen and
corrupts the live block's cursor math.

Interactive mode now never attaches the stderr layer; explicitly requested
console logging (--log-level / RUST_LOG) is redirected to a timestamped
log file with the path printed at startup. Print mode (-p) keeps stderr
logging.
In the inline-rendering model there is no alternate screen and the live
block is the only redrawn region. Pickers used to paint directly to
stdout at absolute rows above the block; the block's relative erase
could never reach those rows, so closing a slash-command picker (or the
models/file/rewind pickers) left permanent remnants on screen.

Fix: make the picker a section of the live block.

- Add renderer types PickerRow/PickerView; draw_live_block now accepts an
  optional picker view and renders it between the streaming region and
  the input separator.
- Replace each picker's draw() method with a view() that returns a pure
  PickerView (rows + optional header). The list-windowing logic is now a
  pure helper in pickers/mod.rs.
- Pickers no longer write to stdout directly; their rows flow through the
  same RenderBackend as the rest of the UI, so the headless test guard
  remains intact and FakeBackend can capture picker output.
- snapshot/LiveArgs now include the picker view, so closing/shrinking a
  picker naturally triggers a full block redraw and erases the old rows.
- Remove the separate renderer.invalidate() hack that used to paper over
  the direct-stdout picker painting.
- Drop per-picker monochrome fields/plumbing; the renderer's own
  monochrome flag resolves Green/DarkGrey for picker rows.

Regression tests: bottom_snapshot dirty tests plus FakeBackend tests for
picker painting, close-erase, shrink-erase, and header-row accounting;
pure picker-window tests in pickers/mod.rs.

ARCHITECTURE.md updated to note that the live block includes the picker
overlay.
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.

2 participants