use super::*; impl ClientContextMenuOverlay { pub(super) fn items(&self) -> Vec { use ClientContextMenuAction as Action; let item = |label, action| ClientContextMenuItem { label, action }; match &self.target { ClientContextMenuTarget::Workspace { is_git: false, .. } => { vec![item("Rename", Action::Rename), item("Close", Action::Close)] } ClientContextMenuTarget::Workspace { is_linked_worktree: false, has_worktree_children: false, .. } => vec![ item("Rename", Action::Rename), item("Close", Action::Close), item("New worktree", Action::NewWorktree), item("Open worktree...", Action::OpenWorktree), ], ClientContextMenuTarget::Workspace { is_linked_worktree: true, .. } => vec![ item("Rename", Action::Rename), item("Close", Action::Close), item("Delete worktree checkout...", Action::RemoveWorktree), ], ClientContextMenuTarget::Workspace { has_worktree_children: true, collapsed, .. } => vec![ item("Rename", Action::Rename), item("Close group", Action::Close), item("New worktree", Action::NewWorktree), item("Open worktree...", Action::OpenWorktree), item( if *collapsed { "Expand" } else { "Collapse" }, Action::ToggleGroup, ), ], ClientContextMenuTarget::Tab { .. } => vec![ item("New tab", Action::NewTab), item("Rename", Action::Rename), item("Close", Action::Close), ], ClientContextMenuTarget::Pane { source_pane_id, has_manual_label, right_click_passthrough, .. } => { let mut items = vec![item("Rename pane", Action::RenamePane)]; if *has_manual_label { items.push(item("Clear pane name", Action::ClearPaneName)); } if source_pane_id.is_some() { items.push(item("Swap with focused pane", Action::SwapWithFocusedPane)); } items.extend([ item("Split right", Action::SplitRight), item("Split down", Action::SplitDown), item("Zoom", Action::Zoom), item( if *right_click_passthrough { "Use Herdr right-click menu" } else { "Send right-clicks to pane" }, Action::ToggleRightClickPassthrough, ), item("Close pane", Action::ClosePane), ]); items } } } } impl ClientShellState { pub(super) fn open_workspace_context_menu(&mut self, workspace_id: String, x: u16, y: u16) { let Some(snapshot) = self.snapshot.as_deref() else { return; }; let Some(workspace) = snapshot .workspaces .iter() .find(|workspace| workspace.workspace_id == workspace_id) else { return; }; let worktree = workspace.worktree.as_ref(); let has_worktree_children = worktree.is_some_and(|worktree| { !worktree.is_linked_worktree && snapshot .workspaces .iter() .filter(|candidate| { candidate .worktree .as_ref() .is_some_and(|candidate| candidate.key == worktree.key) }) .count() >= 2 }); let collapsed = worktree.is_some_and(|worktree| self.collapsed_groups.contains(&worktree.key)); self.overlay = Some(ClientShellOverlay::ContextMenu(ClientContextMenuOverlay { target: ClientContextMenuTarget::Workspace { workspace_id, is_git: worktree.is_some() || workspace.branch.is_some(), is_linked_worktree: worktree.is_some_and(|worktree| worktree.is_linked_worktree), has_worktree_children, collapsed, }, x, y, highlighted: 0, })); } pub(super) fn open_tab_context_menu(&mut self, tab_id: String, x: u16, y: u16) { let Some(tab) = self .snapshot .as_deref() .and_then(|snapshot| snapshot.tabs.iter().find(|tab| tab.tab_id == tab_id)) else { return; }; self.overlay = Some(ClientShellOverlay::ContextMenu(ClientContextMenuOverlay { target: ClientContextMenuTarget::Tab { tab_id, workspace_id: tab.workspace_id.clone(), }, x, y, highlighted: 0, })); } pub(super) fn open_pane_context_menu(&mut self, pane_id: String, x: u16, y: u16) { let Some(snapshot) = self.snapshot.as_deref() else { return; }; let Some(pane) = snapshot.panes.iter().find(|pane| pane.pane_id == pane_id) else { return; }; let source_pane_id = snapshot .focused_pane_id .clone() .filter(|focused| focused != &pane_id); self.overlay = Some(ClientShellOverlay::ContextMenu(ClientContextMenuOverlay { target: ClientContextMenuTarget::Pane { pane_id, workspace_id: pane.workspace_id.clone(), source_pane_id, has_manual_label: pane.label.is_some(), right_click_passthrough: pane.right_click_passthrough, }, x, y, highlighted: 0, })); } pub(super) fn move_context_menu_selection(&mut self, delta: isize) { let Some(ClientShellOverlay::ContextMenu(menu)) = self.overlay.as_mut() else { return; }; let item_count = menu.items().len(); if item_count == 0 { return; } menu.highlighted = (menu.highlighted as isize + delta) .clamp(0, item_count.saturating_sub(1) as isize) as usize; } pub(super) fn activate_context_menu_item( &mut self, index: usize, outcome: &mut ClientShellInput, ) { let Some(ClientShellOverlay::ContextMenu(menu)) = self.overlay.take() else { return; }; let Some(action) = menu.items().get(index).map(|item| item.action) else { outcome.repaint = true; return; }; match menu.target { ClientContextMenuTarget::Workspace { workspace_id, .. } => { self.activate_workspace_context_action(workspace_id, action, outcome) } ClientContextMenuTarget::Tab { tab_id, workspace_id, } => self.activate_tab_context_action(tab_id, workspace_id, action, outcome), ClientContextMenuTarget::Pane { pane_id, workspace_id, source_pane_id, right_click_passthrough, .. } => self.activate_pane_context_action( pane_id, workspace_id, source_pane_id, right_click_passthrough, action, outcome, ), } outcome.repaint = true; } fn activate_workspace_context_action( &mut self, workspace_id: String, action: ClientContextMenuAction, outcome: &mut ClientShellInput, ) { use crate::input::KeybindAction; match action { ClientContextMenuAction::Rename => { let label = self .snapshot .as_deref() .and_then(|snapshot| { snapshot .workspaces .iter() .find(|workspace| workspace.workspace_id == workspace_id) }) .map(|workspace| workspace.label.clone()); if let Some(label) = label { self.overlay = Some(ClientShellOverlay::Rename(ClientRenameOverlay { title: "rename workspace", input: label, replace_on_type: false, target: ClientRenameTarget::Workspace { workspace_id }, })); } } ClientContextMenuAction::Close => { if self.config.confirm_close { self.open_confirm_close_overlay(workspace_id); } else { self.push_endpoint_method( crate::api::schema::Method::WorkspaceClose( crate::api::schema::WorkspaceCloseParams { workspace_id, close_group: true, }, ), outcome, ); } } ClientContextMenuAction::NewWorktree => { self.begin_worktree_action_for(KeybindAction::NewWorktree, workspace_id, outcome) } ClientContextMenuAction::OpenWorktree => { self.begin_worktree_action_for(KeybindAction::OpenWorktree, workspace_id, outcome) } ClientContextMenuAction::RemoveWorktree => { self.begin_worktree_action_for(KeybindAction::RemoveWorktree, workspace_id, outcome) } ClientContextMenuAction::ToggleGroup => { let key = self.snapshot.as_deref().and_then(|snapshot| { snapshot .workspaces .iter() .find(|workspace| workspace.workspace_id == workspace_id) .and_then(|workspace| workspace.worktree.as_ref()) .map(|worktree| worktree.key.clone()) }); if let Some(key) = key { if !self.collapsed_groups.remove(&key) { self.collapsed_groups.insert(key); } self.persist_chrome_preferences(outcome); } } _ => {} } } fn activate_tab_context_action( &mut self, tab_id: String, workspace_id: String, action: ClientContextMenuAction, outcome: &mut ClientShellInput, ) { use crate::api::schema::{Method, TabTarget}; self.push_endpoint_method( Method::TabFocus(TabTarget { tab_id: tab_id.clone(), }), outcome, ); match action { ClientContextMenuAction::NewTab => { if self.config.prompt_new_tab_name { let default_name = (self .snapshot .as_deref() .map(|snapshot| { snapshot .tabs .iter() .filter(|tab| tab.workspace_id == workspace_id) .count() }) .unwrap_or(0) + 1) .to_string(); self.overlay = Some(ClientShellOverlay::Rename(ClientRenameOverlay { title: "new tab", input: default_name.clone(), replace_on_type: true, target: ClientRenameTarget::NewTab { workspace_id, default_name, }, })); } else { self.push_endpoint_method( Method::TabCreate(crate::api::schema::TabCreateParams { workspace_id: Some(workspace_id), cwd: None, focus: true, label: None, env: Default::default(), }), outcome, ); } } ClientContextMenuAction::Rename => { let tab = self .snapshot .as_deref() .and_then(|snapshot| snapshot.tabs.iter().find(|tab| tab.tab_id == tab_id)); if let Some(tab) = tab { self.overlay = Some(ClientShellOverlay::Rename(ClientRenameOverlay { title: "rename tab", input: tab.label.clone(), replace_on_type: false, target: ClientRenameTarget::Tab { tab_id, auto_name: !tab.custom_label, original_name: tab.label.clone(), }, })); } } ClientContextMenuAction::Close => { self.push_endpoint_method(Method::TabClose(TabTarget { tab_id }), outcome); } _ => {} } } fn activate_pane_context_action( &mut self, pane_id: String, workspace_id: String, source_pane_id: Option, right_click_passthrough: bool, action: ClientContextMenuAction, outcome: &mut ClientShellInput, ) { use crate::api::schema::{ Method, PaneInputSetParams, PaneRenameParams, PaneRightClickTarget, PaneSplitParams, PaneSwapParams, PaneTarget, PaneZoomMode, PaneZoomParams, SplitDirection, }; match action { ClientContextMenuAction::RenamePane => { let label = self.snapshot.as_deref().and_then(|snapshot| { snapshot .panes .iter() .find(|pane| pane.pane_id == pane_id) .and_then(|pane| pane.label.clone()) }); self.overlay = Some(ClientShellOverlay::Rename(ClientRenameOverlay { title: "rename pane", input: label.clone().unwrap_or_default(), replace_on_type: label.is_none(), target: ClientRenameTarget::Pane { pane_id }, })); } ClientContextMenuAction::ClearPaneName => self.push_endpoint_method( Method::PaneRename(PaneRenameParams { pane_id, label: None, }), outcome, ), ClientContextMenuAction::SwapWithFocusedPane => { if let Some(source_pane_id) = source_pane_id { self.push_endpoint_method( Method::PaneSwap(PaneSwapParams { pane_id: None, direction: None, source_pane_id: Some(source_pane_id.clone()), target_pane_id: Some(pane_id), }), outcome, ); self.push_endpoint_method( Method::PaneFocus(PaneTarget { pane_id: source_pane_id, }), outcome, ); } } ClientContextMenuAction::SplitRight | ClientContextMenuAction::SplitDown => { self.push_endpoint_method( Method::PaneSplit(PaneSplitParams { workspace_id: Some(workspace_id), target_pane_id: Some(pane_id), direction: if action == ClientContextMenuAction::SplitRight { SplitDirection::Right } else { SplitDirection::Down }, ratio: None, cwd: None, focus: true, right_click: Default::default(), env: Default::default(), }), outcome, ); } ClientContextMenuAction::Zoom => self.push_endpoint_method( Method::PaneZoom(PaneZoomParams { pane_id: Some(pane_id), mode: PaneZoomMode::Toggle, }), outcome, ), ClientContextMenuAction::ToggleRightClickPassthrough => self.push_endpoint_method( Method::PaneInputSet(PaneInputSetParams { pane_id, right_click: if right_click_passthrough { PaneRightClickTarget::Herdr } else { PaneRightClickTarget::Pane }, }), outcome, ), ClientContextMenuAction::ClosePane => { self.push_endpoint_method(Method::PaneClose(PaneTarget { pane_id }), outcome) } _ => {} } } }