aboutsummaryrefslogtreecommitdiff
path: root/src/client/shell/context_menu.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/shell/context_menu.rs')
-rw-r--r--src/client/shell/context_menu.rs469
1 files changed, 469 insertions, 0 deletions
diff --git a/src/client/shell/context_menu.rs b/src/client/shell/context_menu.rs
new file mode 100644
index 00000000..af271bb6
--- /dev/null
+++ b/src/client/shell/context_menu.rs
@@ -0,0 +1,469 @@
+use super::*;
+
+impl ClientContextMenuOverlay {
+ pub(super) fn items(&self) -> Vec<ClientContextMenuItem> {
+ 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<String>,
+ 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)
+ }
+ _ => {}
+ }
+ }
+}