diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-20 18:55:41 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-20 18:55:41 +0300 |
| commit | 836fd7e585af15cc1ccb0d4f04586ffec28d2a3c (patch) | |
| tree | a3bf07e7af7ffd884a38fce3d65f8c2243bc0615 /src/app/input/terminal.rs | |
| parent | be66f33d2acde30d25794bcead991e5e7b7eaff8 (diff) | |
feat: add keybinding v2
refs #154
refs #201
refs #202
refs #219
Diffstat (limited to 'src/app/input/terminal.rs')
| -rw-r--r-- | src/app/input/terminal.rs | 174 |
1 files changed, 165 insertions, 9 deletions
diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs index 12fb67b2..41a62e82 100644 --- a/src/app/input/terminal.rs +++ b/src/app/input/terminal.rs @@ -34,21 +34,44 @@ impl App { let key_event = key.as_key_event(); - if let Some(action) = super::terminal_direct_navigation_action(&self.state, &key_event) { + if let Some(action) = super::terminal_direct_navigation_action(&self.state, key) { debug!( code = ?key_event.code, modifiers = ?key_event.modifiers, kind = ?key_event.kind, action = ?action, - "intercepted terminal direct navigation key before forwarding to pane" + "intercepted terminal direct keybinding before forwarding to pane" ); - super::navigate::execute_navigate_action(&mut self.state, action); + if action == super::navigate::NavigateAction::EditScrollback { + self.launch_focused_scrollback_editor(); + } else { + super::navigate::execute_navigate_action_in_context( + &mut self.state, + action, + super::navigate::ActionContext::Direct, + ); + } + return None; + } + + if let Some(binding) = super::navigate::command_for_key( + &self.state, + key, + super::navigate::BindingDispatch::Direct, + ) { + debug!( + code = ?key_event.code, + modifiers = ?key_event.modifiers, + kind = ?key_event.kind, + command = %binding.label, + "intercepted terminal direct custom command before forwarding to pane" + ); + self.launch_custom_command(binding, super::navigate::ActionContext::Direct); return None; } - if self.state.is_prefix(&key_event) { - self.state.mobile_switcher_scroll = 0; - self.state.mode = Mode::Navigate; + if self.state.is_prefix_key(key) { + self.state.mode = Mode::Prefix; return None; } @@ -169,7 +192,9 @@ mod tests { use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind}; use ratatui::layout::Rect; - use super::super::{app_for_mouse_test, mouse, numbered_lines_bytes}; + use super::super::{ + app_for_mouse_test, mouse, numbered_lines_bytes, unique_temp_path, wait_for_file, + }; use super::*; use crate::{config::Config, workspace::Workspace}; @@ -475,8 +500,7 @@ mod tests { .layout .panes(Rect::new(0, 0, 80, 24)); let focused_before = app.state.workspaces[0].layout.focused(); - app.state.keybinds.focus_pane_left = Some((KeyCode::Char('h'), KeyModifiers::ALT)); - app.state.keybinds.focus_pane_left_label = Some("alt+h".into()); + app.state.keybinds.focus_pane_left = crate::config::ActionKeybinds::direct("alt+h"); app.handle_terminal_key(TerminalKey::new(KeyCode::Char('h'), KeyModifiers::ALT)) .await; @@ -486,6 +510,138 @@ mod tests { } #[tokio::test] + async fn terminal_direct_edit_scrollback_opens_editor_pane() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + let mut workspace = Workspace::test_new("test"); + let root_pane = workspace.tabs[0].root_pane; + workspace.tabs[0].runtimes.insert( + root_pane, + crate::pane::PaneRuntime::test_with_scrollback_bytes(20, 5, 4096, b"alpha\nbeta\n"), + ); + app.state.workspaces = vec![workspace]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + + let output_path = unique_temp_path("direct-edit-scrollback"); + let previous_editor = std::env::var_os("EDITOR"); + std::env::set_var( + "EDITOR", + format!("sh -c 'cp \"$1\" {}' sh", output_path.display()), + ); + app.state.keybinds.edit_scrollback = crate::config::ActionKeybinds::direct("ctrl+alt+e"); + + app.handle_terminal_key(TerminalKey::new( + KeyCode::Char('e'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )) + .await; + + match previous_editor { + Some(value) => std::env::set_var("EDITOR", value), + None => std::env::remove_var("EDITOR"), + } + + let content = wait_for_file(&output_path); + assert!(content.contains("alpha")); + assert!(content.contains("beta")); + assert_eq!(app.state.mode, Mode::Terminal); + + let _ = std::fs::remove_file(output_path); + } + + #[tokio::test] + async fn direct_custom_command_runs_before_forwarding_to_pane() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + app.state.workspaces = vec![Workspace::test_new("test")]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + + let output_path = unique_temp_path("direct-custom-command"); + let command = format!("printf direct > '{}'", output_path.display()); + app.state.keybinds.custom_commands = vec![crate::config::CustomCommandKeybind { + bindings: crate::config::ActionKeybinds::direct("ctrl+alt+g"), + label: "ctrl+alt+g".into(), + command, + action: crate::config::CustomCommandAction::Shell, + }]; + + app.handle_terminal_key(TerminalKey::new( + KeyCode::Char('g'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )) + .await; + + assert_eq!(wait_for_file(&output_path), "direct"); + assert_eq!(app.state.mode, Mode::Terminal); + let _ = std::fs::remove_file(output_path); + } + + #[tokio::test] + async fn direct_custom_pane_command_opens_overlay_pane() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + let (workspace, terminal, runtime) = Workspace::new( + std::env::current_dir().unwrap_or_else(|_| "/".into()), + 24, + 80, + app.state.pane_scrollback_limit_bytes, + app.state.host_terminal_theme, + &app.state.default_shell, + app.event_tx.clone(), + app.render_notify.clone(), + app.render_dirty.clone(), + ) + .expect("workspace should spawn"); + app.state.workspaces = vec![workspace]; + app.state + .terminal_runtimes + .insert(terminal.id.clone(), runtime); + app.state.terminals.insert(terminal.id.clone(), terminal); + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + + app.state.keybinds.custom_commands = vec![crate::config::CustomCommandKeybind { + bindings: crate::config::ActionKeybinds::direct("ctrl+alt+g"), + label: "ctrl+alt+g".into(), + command: "printf direct-pane".into(), + action: crate::config::CustomCommandAction::Pane, + }]; + + app.handle_terminal_key(TerminalKey::new( + KeyCode::Char('g'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )) + .await; + + assert_eq!(app.state.workspaces[0].tabs[0].layout.pane_count(), 2); + assert!(app.state.workspaces[0].tabs[0].zoomed); + assert_eq!(app.state.mode, Mode::Terminal); + } + + #[tokio::test] async fn alt_backspace_is_forwarded_to_focused_pane() { let mut app = app_for_mouse_test(); let mut ws = Workspace::test_new("test"); |
