diff options
Diffstat (limited to 'src/app/input/mod.rs')
| -rw-r--r-- | src/app/input/mod.rs | 231 |
1 files changed, 201 insertions, 30 deletions
diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index 1fc7b0f9..1b5dfefe 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -57,6 +57,7 @@ pub(crate) use self::{ modal::{ handle_confirm_close_key, handle_context_menu_key, handle_global_menu_key, handle_keybind_help_key, handle_navigator_key, handle_rename_key, handle_resize_key, + insert_navigator_search_text, insert_rename_input_text, }, navigate::terminal_direct_navigation_action, settings::open_settings_at, @@ -76,49 +77,56 @@ use super::App; impl App { pub(super) async fn handle_key(&mut self, key: TerminalKey) { + let key_event = key.as_key_event(); + if modal_paste_target_active(&self.state) && is_modal_paste_shortcut(&key_event) { + if let Some(text) = crate::platform::read_clipboard_text() { + self.paste_into_active_text_input(&text); + } + return; + } + match self.state.mode { Mode::Terminal => self.handle_terminal_key(key).await, Mode::Prefix => self.handle_prefix_key(key), Mode::Navigate => self.handle_navigate_key(key), Mode::Copy => self.handle_copy_mode_key(key), - _ => { - let key_event = key.as_key_event(); - match self.state.mode { - Mode::Onboarding => self.handle_onboarding_key(key_event), - Mode::ReleaseNotes => self.handle_release_notes_key(key_event), - Mode::ProductAnnouncement => self.handle_product_announcement_key(key_event), - Mode::Prefix | Mode::Navigate | Mode::Copy => unreachable!(), - Mode::RenameWorkspace | Mode::RenameTab | Mode::RenamePane => { - handle_rename_key(&mut self.state, key_event) - } - Mode::NewLinkedWorktree => self.handle_worktree_create_key(key_event), - Mode::OpenExistingWorktree => self.handle_worktree_open_key(key_event), - Mode::ConfirmRemoveWorktree => self.handle_worktree_remove_key(key_event), - Mode::Resize => handle_resize_key(&mut self.state, key), - Mode::ConfirmClose => handle_confirm_close_key(&mut self.state, key_event), - Mode::ContextMenu => { - handle_context_menu_key( - &mut self.state, - &mut self.terminal_runtimes, - key_event, - ); - } - Mode::Settings => self.handle_settings_key(key_event), - Mode::GlobalMenu => handle_global_menu_key(&mut self.state, key_event), - Mode::KeybindHelp => handle_keybind_help_key(&mut self.state, key_event), - Mode::Navigator => { - handle_navigator_key(&mut self.state, &self.terminal_runtimes, key_event) - } - Mode::Terminal => unreachable!(), + _ => match self.state.mode { + Mode::Onboarding => self.handle_onboarding_key(key_event), + Mode::ReleaseNotes => self.handle_release_notes_key(key_event), + Mode::ProductAnnouncement => self.handle_product_announcement_key(key_event), + Mode::Prefix | Mode::Navigate | Mode::Copy => unreachable!(), + Mode::RenameWorkspace | Mode::RenameTab | Mode::RenamePane => { + handle_rename_key(&mut self.state, key_event) } - } + Mode::NewLinkedWorktree => self.handle_worktree_create_key(key_event), + Mode::OpenExistingWorktree => self.handle_worktree_open_key(key_event), + Mode::ConfirmRemoveWorktree => self.handle_worktree_remove_key(key_event), + Mode::Resize => handle_resize_key(&mut self.state, key), + Mode::ConfirmClose => handle_confirm_close_key(&mut self.state, key_event), + Mode::ContextMenu => { + handle_context_menu_key( + &mut self.state, + &mut self.terminal_runtimes, + key_event, + ); + } + Mode::Settings => self.handle_settings_key(key_event), + Mode::GlobalMenu => handle_global_menu_key(&mut self.state, key_event), + Mode::KeybindHelp => handle_keybind_help_key(&mut self.state, key_event), + Mode::Navigator => { + handle_navigator_key(&mut self.state, &self.terminal_runtimes, key_event) + } + Mode::Terminal => unreachable!(), + }, } } pub(super) async fn handle_paste(&mut self, text: String) { if self.state.mode != Mode::Terminal { + self.paste_into_active_text_input(&text); return; } + if let Some(ws_idx) = self.state.active { if let Some(rt) = self .state @@ -129,6 +137,39 @@ impl App { } } + pub(crate) fn paste_into_active_text_input(&mut self, text: &str) -> bool { + match self.state.mode { + Mode::RenameWorkspace | Mode::RenameTab | Mode::RenamePane => { + insert_rename_input_text(&mut self.state, text); + true + } + Mode::NewLinkedWorktree => { + self.insert_worktree_create_text(text); + true + } + Mode::OpenExistingWorktree => { + if !self + .state + .worktree_open + .as_ref() + .is_some_and(|open| open.search_focused) + { + return false; + } + self.insert_worktree_open_search_text(text); + true + } + Mode::Navigator => { + if !self.state.navigator.search_focused { + return false; + } + insert_navigator_search_text(&mut self.state, &self.terminal_runtimes, text); + true + } + _ => false, + } + } + pub(crate) fn handle_onboarding_key(&mut self, key: KeyEvent) { match key.code { KeyCode::Right | KeyCode::Char('l') => self.open_settings_from_onboarding(), @@ -400,6 +441,36 @@ impl App { } } +pub(crate) fn is_modal_paste_shortcut(key: &KeyEvent) -> bool { + if !matches!(key.code, KeyCode::Char('v' | 'V')) { + return false; + } + + #[cfg(target_os = "macos")] + { + key.modifiers.contains(KeyModifiers::SUPER) || key.modifiers.contains(KeyModifiers::CONTROL) + } + + #[cfg(not(target_os = "macos"))] + { + key.modifiers.contains(KeyModifiers::CONTROL) + } +} + +pub(crate) fn modal_paste_target_active(state: &AppState) -> bool { + match state.mode { + Mode::RenameWorkspace | Mode::RenameTab | Mode::RenamePane | Mode::NewLinkedWorktree => { + true + } + Mode::OpenExistingWorktree => state + .worktree_open + .as_ref() + .is_some_and(|open| open.search_focused), + Mode::Navigator => state.navigator.search_focused, + _ => false, + } +} + // --------------------------------------------------------------------------- // Mouse handling // --------------------------------------------------------------------------- @@ -557,3 +628,103 @@ fn wait_for_file(path: &std::path::Path) -> String { } panic!("timed out waiting for {}", path.display()); } + +#[cfg(test)] +mod tests { + use super::*; + + fn test_app() -> App { + App::new( + &crate::config::Config::default(), + true, + None, + tokio::sync::mpsc::unbounded_channel().1, + crate::api::EventHub::default(), + ) + } + + #[tokio::test] + async fn paste_routes_to_rename_modal_input() { + let mut app = test_app(); + app.state.workspaces = vec![crate::workspace::Workspace::test_new("test")]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::RenameTab; + app.state.name_input = "2".into(); + app.state.name_input_replace_on_type = true; + + app.handle_paste("feature/logs".into()).await; + + assert_eq!(app.state.name_input, "feature/logs"); + assert!(!app.state.name_input_replace_on_type); + } + + #[tokio::test] + async fn paste_routes_to_new_linked_worktree_input() { + let mut app = test_app(); + app.state.mode = Mode::NewLinkedWorktree; + app.state.name_input = "generated-branch".into(); + app.state.name_input_replace_on_type = true; + app.state.worktree_create = Some(crate::app::state::WorktreeCreateState { + source_workspace_id: "source".into(), + source_checkout_path: "/repo/herdr".into(), + source_existing_membership: None, + source_repo_root: "/repo/herdr".into(), + repo_key: "repo-key".into(), + repo_name: "herdr".into(), + branch: "generated-branch".into(), + checkout_path: "/repo/herdr-generated-branch".into(), + error: None, + creating: false, + }); + + app.handle_paste("feature/linear-302".into()).await; + + assert_eq!(app.state.name_input, "feature/linear-302"); + assert_eq!( + app.state + .worktree_create + .as_ref() + .map(|create| create.branch.as_str()), + Some("feature/linear-302") + ); + } + + #[test] + fn modal_paste_shortcut_matches_platform_primary_v() { + #[cfg(target_os = "macos")] + let modifiers = KeyModifiers::SUPER; + #[cfg(not(target_os = "macos"))] + let modifiers = KeyModifiers::CONTROL; + + assert!(is_modal_paste_shortcut(&KeyEvent::new( + KeyCode::Char('v'), + modifiers + ))); + assert!(is_modal_paste_shortcut(&KeyEvent::new( + KeyCode::Char('V'), + modifiers | KeyModifiers::SHIFT + ))); + assert!(!is_modal_paste_shortcut(&KeyEvent::new( + KeyCode::Char('v'), + KeyModifiers::ALT + ))); + } + + #[test] + fn modal_paste_target_is_active_only_for_text_inputs() { + let mut state = AppState::test_new(); + + state.mode = Mode::RenameTab; + assert!(modal_paste_target_active(&state)); + + state.mode = Mode::Navigator; + state.navigator.search_focused = false; + assert!(!modal_paste_target_active(&state)); + state.navigator.search_focused = true; + assert!(modal_paste_target_active(&state)); + + state.mode = Mode::ConfirmClose; + assert!(!modal_paste_target_active(&state)); + } +} |
