aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-14 02:17:56 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-14 02:17:56 +0300
commit8cac2c0df47af927b1c467fb8449d8f84e210407 (patch)
tree8682565afbf07b3ad5d4c6811f30c93cbc6e9cd1 /src
parentedaa3a1a092763cb25fd9e21772c95ee56a75a15 (diff)
feat support double prefix passthrough
Diffstat (limited to 'src')
-rw-r--r--src/app/input.rs115
-rw-r--r--src/pane.rs48
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,
+ )
}
}