From 25b016f3dafdd712fec11e5cf126abd94a4a5d98 Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 1 May 2026 23:14:16 +0900 Subject: [PATCH] =?UTF-8?q?feat(tui):=20=E3=83=9E=E3=82=A6=E3=82=B9?= =?UTF-8?q?=E3=83=9B=E3=82=A4=E3=83=BC=E3=83=AB=E3=81=A7=E3=82=B9=E3=82=AF?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=AB=E3=81=99=E3=82=8B=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 1 + crates/tui/src/main.rs | 36 +++++++++++++++++++++++++++++++----- tickets/tui-mouse-scroll.md | 28 ++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 tickets/tui-mouse-scroll.md diff --git a/TODO.md b/TODO.md index fc9e5f48..33d923f1 100644 --- a/TODO.md +++ b/TODO.md @@ -14,6 +14,7 @@ - [ ] フルスクリーン化によるオーバーホール → [tickets/tui-fullscreen-overhaul.md](tickets/tui-fullscreen-overhaul.md) - [ ] Run 中の入力キューイング → [tickets/tui-input-queue.md](tickets/tui-input-queue.md) - [ ] ユーザーマニフェストのモデル設定 wizard → [tickets/tui-user-model-setup.md](tickets/tui-user-model-setup.md) + - [ ] マウスホイールでのヒストリースクロール → [tickets/tui-mouse-scroll.md](tickets/tui-mouse-scroll.md) - [ ] サブミット入力 - [ ] TUI 補完 + 型付き atom 化 → [tickets/submit-tui-completion.md](tickets/submit-tui-completion.md) - [ ] FileRef リゾルバ → [tickets/submit-file-ref-resolver.md](tickets/submit-file-ref-resolver.md) diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 7b33f7cb..411f6cfd 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -15,8 +15,8 @@ use std::process::ExitCode; use std::time::Duration; use crossterm::event::{ - self, DisableBracketedPaste, EnableBracketedPaste, Event as TermEvent, KeyCode, KeyEvent, - KeyModifiers, + self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, + Event as TermEvent, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind, }; use crossterm::execute; use crossterm::terminal::{ @@ -170,7 +170,12 @@ async fn main() -> ExitCode { // shows up cleanly in scrollback rather than inside an active // alternate-screen buffer. let mut stdout = io::stdout(); - let _ = execute!(stdout, LeaveAlternateScreen, DisableBracketedPaste); + let _ = execute!( + stdout, + DisableMouseCapture, + LeaveAlternateScreen, + DisableBracketedPaste + ); let _ = disable_raw_mode(); let _ = execute!(stdout, crossterm::cursor::Show); @@ -229,7 +234,11 @@ async fn run_spawn(resume_from: Option) -> Result<(), Box {} @@ -246,7 +255,7 @@ async fn run_spawn(resume_from: Option) -> Result<(), Box Result>, Box> { let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen)?; + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); Ok(Terminal::new(backend)?) } @@ -302,6 +311,9 @@ async fn run_loop( client.send(&method).await?; } } + TermEvent::Mouse(mouse) => { + handle_mouse(app, mouse); + } TermEvent::Paste(s) => { app.insert_paste(s); } @@ -345,6 +357,20 @@ fn run_disconnected(_app: &mut App) -> Result<(), Box> { Ok(()) } +/// Lines per wheel notch. Faster than Shift+↑/↓ (which is 1 line) so +/// hand-rolling through long histories isn't tedious, but slow enough +/// that a single notch doesn't blow past the section the user is +/// looking for. +const WHEEL_LINES: usize = 3; + +fn handle_mouse(app: &mut App, mouse: MouseEvent) { + match mouse.kind { + MouseEventKind::ScrollUp => app.scroll.scroll_up(WHEEL_LINES), + MouseEventKind::ScrollDown => app.scroll.scroll_down(WHEEL_LINES), + _ => {} + } +} + fn handle_key(app: &mut App, key: KeyEvent) -> Option { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); let shift = key.modifiers.contains(KeyModifiers::SHIFT); diff --git a/tickets/tui-mouse-scroll.md b/tickets/tui-mouse-scroll.md new file mode 100644 index 00000000..b84a8be8 --- /dev/null +++ b/tickets/tui-mouse-scroll.md @@ -0,0 +1,28 @@ +# TUI: マウスホイールでヒストリーをスクロール + +## 背景 + +TUIのヒストリービューはキー操作(Shift+↑/↓、PageUp/PageDown、Ctrl+Home/End、Ctrl+[/])でスクロールできる。スクロール状態(`Scroll` 構造体・follow_tail・top_offset)も既に揃っており、ロジック的には外部から `scroll_up`/`scroll_down` を叩けば任意の手段でスクロール可能になっている。 + +一方で、現状のイベントループは `TermEvent::Key` / `Paste` / `Resize` のみを処理しており、マウスイベントは破棄される。crossterm のマウスキャプチャも有効化されていないため、ターミナル側にホイール入力すら届かない。 + +エディタ的に常駐するTUIとして、マウスホイールで直感的にヒストリーを遡れる体験を提供したい。 + +## 要件 + +- フルスクリーンモード中にマウスホイールでヒストリービューをスクロールできる。 +- ホイール上方向で過去方向(`scroll_up`)、下方向で末尾方向(`scroll_down`)に動く。1 notch あたりの行数は既存のキー操作(Shift+↑/↓)と同じ感覚で良い。 +- マウスキャプチャの有効化はalt-screenに入っているあいだに限定し、終了時に確実に解除する(picker / spawn のインラインフェーズはネイティブのマウス挙動を保つ)。 +- クリック・ドラッグ等のホイール以外のマウスイベントは現状無視で構わないが、最低限イベントループでマッチ漏れによるエラーが出ない形にする。 + +## 完了条件 + +- フルスクリーンTUIでマウスホイールを回すと、ヒストリービューが上下に動く。 +- TUIを正常終了・異常終了どちらでも、ターミナルのマウスキャプチャ状態が残らない。 +- 既存のキー操作によるスクロール挙動に変化がない。 + +## 範囲外 + +- ホイール以外のマウス操作(クリックでフォーカス移動、ドラッグ選択、スクロールバー表示など) +- picker / spawn のインラインフェーズでのマウス対応 +- ホイール感度の設定可能化