diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-14 02:17:56 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-14 02:17:56 +0300 |
| commit | 8cac2c0df47af927b1c467fb8449d8f84e210407 (patch) | |
| tree | 8682565afbf07b3ad5d4c6811f30c93cbc6e9cd1 /src | |
| parent | edaa3a1a092763cb25fd9e21772c95ee56a75a15 (diff) | |
feat support double prefix passthrough
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/input.rs | 115 | ||||
| -rw-r--r-- | src/pane.rs | 48 |
2 files changed, 143 insertions, 20 deletions
diff --git a/src/app/input.rs b/src/app/input.rs index c3717439..347d8e41 100644 --- a/src/app/input.rs +++ b/src/app/input.rs @@ -164,12 +164,12 @@ impl App { pub(super) async fn handle_key(&mut self, key: TerminalKey) { match self.state.mode { Mode::Terminal => self.handle_terminal_key(key).await, + Mode::Navigate => self.handle_navigate_key(key), _ => { let key = AppKey::from(key); match self.state.mode { Mode::Onboarding => self.handle_onboarding_key(key), Mode::ReleaseNotes => self.handle_release_notes_key(key), - Mode::Navigate => self.handle_navigate_key(key), Mode::RenameWorkspace | Mode::RenameTab => { handle_rename_key(&mut self.state, key) } @@ -179,7 +179,7 @@ impl App { Mode::Settings => self.handle_settings_key(key), Mode::GlobalMenu => handle_global_menu_key(&mut self.state, key), Mode::KeybindHelp => handle_keybind_help_key(&mut self.state, key), - Mode::Terminal => unreachable!(), + Mode::Terminal | Mode::Navigate => unreachable!(), } } } @@ -261,12 +261,18 @@ impl App { } } - fn handle_navigate_key(&mut self, key: AppKey) { + fn handle_navigate_key(&mut self, raw_key: TerminalKey) { + let key = AppKey::from(raw_key); self.state.update_dismissed = true; - if key.matches_binding(self.state.prefix_code, self.state.prefix_mods) - || key.code() == KeyCode::Esc - { + if key.matches_binding(self.state.prefix_code, self.state.prefix_mods) { + if !self.pass_through_key_to_focused_pane(raw_key) { + leave_navigate_mode(&mut self.state); + } + return; + } + + if key.code() == KeyCode::Esc { leave_navigate_mode(&mut self.state); return; } @@ -424,6 +430,26 @@ impl App { } } + fn pass_through_key_to_focused_pane(&mut self, key: TerminalKey) -> bool { + let Some(ws) = self.state.active.and_then(|i| self.state.workspaces.get(i)) else { + return false; + }; + let Some(rt) = ws.focused_runtime() else { + return false; + }; + + let bytes = rt.encode_terminal_key(key); + if bytes.is_empty() { + return false; + } + if rt.try_send_bytes(Bytes::from(bytes)).is_err() { + return false; + } + + self.state.mode = Mode::Terminal; + true + } + async fn handle_terminal_key(&mut self, key: TerminalKey) { self.state.clear_selection(); self.state.update_dismissed = true; @@ -3609,6 +3635,83 @@ mod tests { } #[tokio::test] + async fn double_prefix_passes_prefix_through_to_focused_pane() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + None, + api_rx, + crate::api::EventHub::default(), + ); + let mut workspace = Workspace::test_new("test"); + let pane_id = workspace.tabs[0].root_pane; + let (runtime, mut rx) = + crate::pane::PaneRuntime::test_with_screen_bytes_and_receiver(20, 5, b""); + let expected = runtime + .encode_terminal_key(TerminalKey::new(KeyCode::Char('a'), KeyModifiers::CONTROL)); + workspace.tabs[0].runtimes.insert(pane_id, runtime); + + app.state.workspaces = vec![workspace]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + app.state.prefix_code = KeyCode::Char('a'); + app.state.prefix_mods = KeyModifiers::CONTROL; + + app.handle_key(TerminalKey::new(KeyCode::Char('a'), KeyModifiers::CONTROL)) + .await; + assert_eq!(app.state.mode, Mode::Navigate); + + app.handle_key(TerminalKey::new(KeyCode::Char('a'), KeyModifiers::CONTROL)) + .await; + assert_eq!(app.state.mode, Mode::Terminal); + + let forwarded = rx.try_recv().expect("prefix bytes should be forwarded"); + assert_eq!(forwarded.as_ref(), expected.as_slice()); + } + + #[tokio::test] + async fn double_shifted_prefix_preserves_shifted_codepoint_when_forwarded() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + None, + api_rx, + crate::api::EventHub::default(), + ); + let mut workspace = Workspace::test_new("test"); + let pane_id = workspace.tabs[0].root_pane; + let (runtime, mut rx) = + crate::pane::PaneRuntime::test_with_screen_bytes_and_receiver(20, 5, b""); + let prefix = TerminalKey::new(KeyCode::Char('/'), KeyModifiers::SHIFT) + .with_shifted_codepoint('?' as u32); + let expected = runtime.encode_terminal_key(prefix); + workspace.tabs[0].runtimes.insert(pane_id, runtime); + + app.state.workspaces = vec![workspace]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + app.state.prefix_code = KeyCode::Char('?'); + app.state.prefix_mods = KeyModifiers::empty(); + + app.handle_key(prefix).await; + assert_eq!(app.state.mode, Mode::Navigate); + + app.handle_key(prefix).await; + assert_eq!(app.state.mode, Mode::Terminal); + + let forwarded = rx + .try_recv() + .expect("shifted prefix bytes should be forwarded"); + assert_eq!(forwarded.as_ref(), expected.as_slice()); + } + + #[tokio::test] async fn custom_command_runs_from_prefix_key_in_navigate_mode() { let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); let mut app = App::new( diff --git a/src/pane.rs b/src/pane.rs index 74fec4e4..5e71e6c9 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -2387,31 +2387,51 @@ impl PaneRuntime { Self::test_with_scrollback_bytes(cols, rows, 0, bytes) } + pub(crate) fn test_with_screen_bytes_and_receiver( + cols: u16, + rows: u16, + bytes: &[u8], + ) -> (Self, mpsc::Receiver<Bytes>) { + Self::test_with_scrollback_bytes_and_receiver(cols, rows, 0, bytes) + } + pub(crate) fn test_with_scrollback_bytes( cols: u16, rows: u16, scrollback_limit_bytes: usize, bytes: &[u8], ) -> Self { - let (tx, _rx) = mpsc::channel(4); + Self::test_with_scrollback_bytes_and_receiver(cols, rows, scrollback_limit_bytes, bytes).0 + } + + fn test_with_scrollback_bytes_and_receiver( + cols: u16, + rows: u16, + scrollback_limit_bytes: usize, + bytes: &[u8], + ) -> (Self, mpsc::Receiver<Bytes>) { + let (tx, rx) = mpsc::channel(4); let (resize_tx, _resize_rx) = mpsc::channel(1); let mut terminal = crate::ghostty::Terminal::new(cols, rows, scrollback_limit_bytes).unwrap(); terminal.write(bytes); - Self { - terminal: Arc::new(PaneTerminal { - ghostty: GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(), - }), - sender: tx, - resize_tx, - current_size: Cell::new((rows, cols)), - child_pid: Arc::new(AtomicU32::new(0)), - kitty_keyboard_flags: Arc::new(AtomicU16::new(0)), - detect_reset_notify: Arc::new(Notify::new()), - pending_release: Arc::new(Mutex::new(None)), - detect_handle: tokio::spawn(async {}).abort_handle(), - } + ( + Self { + terminal: Arc::new(PaneTerminal { + ghostty: GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(), + }), + sender: tx, + resize_tx, + current_size: Cell::new((rows, cols)), + child_pid: Arc::new(AtomicU32::new(0)), + kitty_keyboard_flags: Arc::new(AtomicU16::new(0)), + detect_reset_notify: Arc::new(Notify::new()), + pending_release: Arc::new(Mutex::new(None)), + detect_handle: tokio::spawn(async {}).abort_handle(), + }, + rx, + ) } } |
