Scroll the transcript the way a Mac scrolls

3d188862756c · AtlantisPleb · · parent 2eb078e18749

Scroll the transcript the way a Mac scrolls

`PageUp` scrolled it all along, which on a Mac laptop is `Fn+Up` -- a chord
nobody should have to know to read what they just did. `Up` reaches the
transcript only after the input history is exhausted, so on a fresh session it
looks like nothing happens.

The session now captures mouse events, so a two-finger trackpad scroll and a
wheel move the transcript. Capture is released before leaving the alternate
screen, so the terminal gets its own selection back on exit.

The event loop also stopped discarding every non-key event unread; it dispatches
mouse events and ignores the rest explicitly, which is the same shape.

Checked under a pty: the binary emits the mouse-reporting enables (1000, 1002,
1003, 1006, 1015) on startup, and repaints when an SGR wheel-up arrives. The
key list now names the trackpad first and says PageUp is Fn+Up on a Mac, since
a list naming a key nobody can press is the defect that list exists to prevent.

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified crates/coder-lite/src/commands.rs
  • modified crates/coder-lite/src/interactive.rs

Diff

2 files changed, +22 -5

crates/coder-lite/src/commands.rs modified +2 -1

@@ -50,7 +50,8 @@ const KEYS: &[(&str, &str)] = &[

50 50
    ("Enter", "send"),
51 51
    ("Alt+Enter / Ctrl+J", "newline"),
52 52
    ("Up / Down", "move the caret, then walk history, then scroll"),
53
    ("PageUp / PageDown", "scroll the transcript"),
53
    ("Scroll wheel / trackpad", "scroll the transcript"),
54
    ("PageUp / PageDown", "scroll a page (Fn+Up / Fn+Down on a Mac)"),
54 55
    ("Tab", "complete a command or a path"),
55 56
    (
56 57
        "Ctrl+A / Ctrl+E / Ctrl+W / Ctrl+K / Ctrl+U / Alt+B / Alt+F",
crates/coder-lite/src/interactive.rs modified +20 -4

@@ -20,8 +20,8 @@ use crate::tui::{CoderUi, Entry, Role, ToolCall};

20 20
use crossterm::{
21 21
    ExecutableCommand,
22 22
    event::{
23
        self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, PopKeyboardEnhancementFlags,
24
        PushKeyboardEnhancementFlags,
23
        self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
24
        KeyModifiers, MouseEventKind, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
25 25
    },
26 26
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
27 27
};

@@ -129,6 +129,11 @@ pub async fn run_tui(options: SessionOptions) -> Result<(), Box<dyn std::error::

129 129
    enable_raw_mode()?;
130 130
    let mut stdout = stdout();
131 131
    stdout.execute(EnterAlternateScreen)?;
132
    // Two-finger trackpad scroll is how a Mac scrolls. Without capture those
133
    // events go to the terminal emulator, which in the alternate screen has
134
    // nothing to scroll, so the transcript looked frozen. `PageUp` worked all
135
    // along -- as Fn+Up, which is not a thing anyone should have to know.
136
    let _ = stdout.execute(EnableMouseCapture);
132 137
133 138
    let mut stderr = stderr();
134 139
    let flags = event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES

@@ -164,8 +169,18 @@ pub async fn run_tui(options: SessionOptions) -> Result<(), Box<dyn std::error::

164 169
        if !event::poll(Duration::from_millis(50))? {
165 170
            continue;
166 171
        }
167
        let Event::Key(key) = event::read()? else {
168
            continue;
172
        let key = match event::read()? {
173
            Event::Key(key) => key,
174
            Event::Mouse(mouse) => {
175
                // Three rows a notch, which is what a terminal scrolls.
176
                match mouse.kind {
177
                    MouseEventKind::ScrollUp => ui.scroll_by(-3),
178
                    MouseEventKind::ScrollDown => ui.scroll_by(3),
179
                    _ => {}
180
                }
181
                continue;
182
            }
183
            _ => continue,
169 184
        };
170 185
        if key.kind != KeyEventKind::Press {
171 186
            continue;

@@ -238,6 +253,7 @@ pub async fn run_tui(options: SessionOptions) -> Result<(), Box<dyn std::error::

238 253
239 254
    terminal.hide_cursor()?;
240 255
    let _ = crossterm::execute!(stderr, PopKeyboardEnhancementFlags);
256
    let _ = std::io::stdout().execute(DisableMouseCapture);
241 257
    disable_raw_mode()?;
242 258
    std::io::stdout().execute(LeaveAlternateScreen)?;
243 259

This page updates live while a promote is in flight · changelog