From d9f55185f0f8ea6046a4bcc23bcd7e175ad4740a Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 3 May 2026 00:44:38 +0900 Subject: [PATCH] =?UTF-8?q?update:=20tui=E3=81=AE=E6=96=87=E5=AD=97?= =?UTF-8?q?=E5=85=A5=E5=8A=9B=E3=81=AECtrl=E3=83=96=E3=83=AD=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 2 + crates/tui/src/app.rs | 10 +--- crates/tui/src/input.rs | 12 +++++ crates/tui/src/main.rs | 103 +++++++++++++++++++++------------------- docs/tui-keybindings.md | 3 +- 5 files changed, 71 insertions(+), 59 deletions(-) diff --git a/TODO.md b/TODO.md index 236be95b..138d4ed5 100644 --- a/TODO.md +++ b/TODO.md @@ -14,6 +14,8 @@ - [ ] フルスクリーン化によるオーバーホール → [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) + - [ ] auto-kick 由来ターンが描画されない → [tickets/tui-pod-event-render.md](tickets/tui-pod-event-render.md) +- [ ] 子 Pod の PodEvent::TurnEnded が親に届かない → [tickets/pod-event-delivery.md](tickets/pod-event-delivery.md) - [ ] サブミット入力 - [ ] FileRef リゾルバ → [tickets/submit-file-ref-resolver.md](tickets/submit-file-ref-resolver.md) - [ ] Manifest: Tool Output / File Upload 上限の分離とデフォルト緩和 → [tickets/manifest-output-upload-limits.md](tickets/manifest-output-upload-limits.md) diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index fcd673c9..38becf98 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -619,20 +619,14 @@ impl App { pub fn delete_char_after(&mut self) { self.input.delete_after(); } - pub fn delete_word_before(&mut self) { - self.input.delete_word_before(); - } pub fn move_cursor_left(&mut self) { self.input.move_left(); } pub fn move_cursor_right(&mut self) { self.input.move_right(); } - pub fn move_cursor_word_left(&mut self) { - self.input.move_word_left(); - } - pub fn move_cursor_word_right(&mut self) { - self.input.move_word_right(); + pub fn move_cursor_start(&mut self) { + self.input.move_start(); } pub fn move_cursor_home(&mut self) { self.input.move_home(); diff --git a/crates/tui/src/input.rs b/crates/tui/src/input.rs index 46fc4f1c..8b1866bb 100644 --- a/crates/tui/src/input.rs +++ b/crates/tui/src/input.rs @@ -345,6 +345,10 @@ impl InputBuffer { } } + pub fn move_start(&mut self) { + self.cursor = 0; + } + pub fn move_home(&mut self) { while self.cursor > 0 { if matches!(self.atoms[self.cursor - 1], Atom::Char('\n')) { @@ -921,6 +925,14 @@ mod word_motion_tests { assert_eq!(cursor(&buf), 0); } + #[test] + fn move_start_lands_at_beginning_of_buffer() { + let mut buf = buf_from("foo\nbar"); + assert_eq!(cursor(&buf), 7); + buf.move_start(); + assert_eq!(cursor(&buf), 0); + } + #[test] fn forward_from_start_lands_after_first_word() { let mut buf = buf_from("foo bar baz"); diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 411f6cfd..f00bbe5c 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -376,16 +376,65 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option { let shift = key.modifiers.contains(KeyModifiers::SHIFT); let alt = key.modifiers.contains(KeyModifiers::ALT); - // Scroll / navigation (history view). - match key.code { + // Modifier-key bindings. + if let Some(method) = match key.code { KeyCode::Up if shift => { app.scroll.scroll_up(1); - return None; + Some(None) } KeyCode::Down if shift => { app.scroll.scroll_down(1); - return None; + Some(None) } + KeyCode::Home if ctrl => { + app.scroll.to_top(); + Some(None) + } + KeyCode::End if ctrl => { + app.scroll.to_bottom(); + Some(None) + } + KeyCode::Char('[') if ctrl => { + app.scroll.jump_prev_turn(); + Some(None) + } + KeyCode::Char(']') if ctrl => { + app.scroll.jump_next_turn(); + Some(None) + } + KeyCode::Char('o') if ctrl => { + app.mode = app.mode.cycle(); + Some(None) + } + KeyCode::Char('a') if ctrl => { + app.move_cursor_start(); + Some(app.refresh_completion()) + } + KeyCode::Char('c') if ctrl => Some(handle_pause_or_quit(app)), + KeyCode::Char('x') if ctrl => Some(if app.running { + Some(Method::Cancel) + } else { + app.push_error("Nothing to cancel (Pod is not running)."); + None + }), + KeyCode::Char('d') if ctrl => Some(handle_shutdown(app)), + KeyCode::Enter if alt => { + app.insert_newline(); + Some(app.refresh_completion()) + } + _ => None, + } { + return method; + } + + // Unbound Ctrl+Char keys are ignored before the text-input path so + // holding Ctrl while typing never inserts control characters. + if ctrl && matches!(key.code, KeyCode::Char(_)) { + return None; + } + + // Scroll / navigation (history view). + match key.code { KeyCode::PageUp => { app.scroll.page_up(); return None; @@ -394,26 +443,6 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option { app.scroll.page_down(); return None; } - KeyCode::Home if ctrl => { - app.scroll.to_top(); - return None; - } - KeyCode::End if ctrl => { - app.scroll.to_bottom(); - return None; - } - KeyCode::Char('[') if ctrl => { - app.scroll.jump_prev_turn(); - return None; - } - KeyCode::Char(']') if ctrl => { - app.scroll.jump_next_turn(); - return None; - } - KeyCode::Char('o') if ctrl => { - app.mode = app.mode.cycle(); - return None; - } _ => {} } @@ -463,31 +492,13 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option { } match key.code { - KeyCode::Char('c') if ctrl => handle_pause_or_quit(app), - KeyCode::Char('x') if ctrl => { - if app.running { - Some(Method::Cancel) - } else { - app.push_error("Nothing to cancel (Pod is not running)."); - None - } - } - KeyCode::Char('d') if ctrl => handle_shutdown(app), KeyCode::Esc => { // Close the popup if it's still showing (covers the // request-in-flight case where `is_active()` was false). app.cancel_completion(); None } - KeyCode::Enter if alt => { - app.insert_newline(); - app.refresh_completion() - } KeyCode::Enter => app.submit_input(), - KeyCode::Backspace if ctrl => { - app.delete_word_before(); - app.refresh_completion() - } KeyCode::Backspace => { app.delete_char_before(); app.refresh_completion() @@ -496,18 +507,10 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option { app.delete_char_after(); app.refresh_completion() } - KeyCode::Left if ctrl => { - app.move_cursor_word_left(); - app.refresh_completion() - } KeyCode::Left => { app.move_cursor_left(); app.refresh_completion() } - KeyCode::Right if ctrl => { - app.move_cursor_word_right(); - app.refresh_completion() - } KeyCode::Right => { app.move_cursor_right(); app.refresh_completion() diff --git a/docs/tui-keybindings.md b/docs/tui-keybindings.md index 9b7133ad..bfe16cb3 100644 --- a/docs/tui-keybindings.md +++ b/docs/tui-keybindings.md @@ -6,7 +6,8 @@ | キー | 動作 | |---|---| -| 文字キー | カーソル位置に挿入 | +| 文字キー | カーソル位置に挿入(未割り当ての `Ctrl`+文字キーは無視) | +| `Ctrl-A` | 入力欄全体の先頭へ | | `Backspace` | カーソル直前を削除(ペーストプレースホルダは 1 回で全削除) | | `Delete` | カーソル直後を削除(同上) | | `Left` / `Right` | カーソル移動 |