aboutsummaryrefslogtreecommitdiff
path: root/src/app/input/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/input/mod.rs')
-rw-r--r--src/app/input/mod.rs231
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));
+ }
+}