diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-06 02:27:06 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-06 02:27:06 +0300 |
| commit | 9e9bc8a144667a6b7debfd4f0c7a31ff0e11493a (patch) | |
| tree | 03f1259f91fd9edeacfd4b904f2e84d4acf25232 /src/client/shell/endpoint_navigation.rs | |
| parent | 35b0dff96579f099e0436290085024424ffb289c (diff) | |
feat: manage multiple ssh machines from one client (#3670)
Diffstat (limited to 'src/client/shell/endpoint_navigation.rs')
| -rw-r--r-- | src/client/shell/endpoint_navigation.rs | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/src/client/shell/endpoint_navigation.rs b/src/client/shell/endpoint_navigation.rs new file mode 100644 index 00000000..adb29c93 --- /dev/null +++ b/src/client/shell/endpoint_navigation.rs @@ -0,0 +1,290 @@ +use super::*; + +impl ClientShellState { + pub(super) fn active_endpoint_workspace_at(&self, point: (u16, u16)) -> Option<String> { + self.hits + .workspaces + .iter() + .find(|hit| { + hit.endpoint_id == self.active_endpoint_id && super::contains(hit.rect, point) + }) + .map(|hit| hit.workspace_id.clone()) + } + + pub(super) fn endpoint_workspace_is_draggable(&self, press: &ClientWorkspacePress) -> bool { + press.endpoint_id == self.active_endpoint_id + && self + .snapshot + .as_deref() + .and_then(|snapshot| { + snapshot + .workspaces + .iter() + .find(|workspace| workspace.workspace_id == press.workspace_id) + }) + .is_some_and(|workspace| { + !workspace + .worktree + .as_ref() + .is_some_and(|worktree| worktree.is_linked_worktree) + }) + } + + pub(super) fn finish_endpoint_workspace_press( + &mut self, + press: ClientWorkspacePress, + outcome: &mut ClientShellInput, + ) { + if press.endpoint_id == self.active_endpoint_id { + self.push_endpoint_method( + crate::api::schema::Method::WorkspaceFocus(crate::api::schema::WorkspaceTarget { + workspace_id: press.workspace_id, + }), + outcome, + ); + } else if self.endpoint_is_online(&press.endpoint_id) { + outcome.actions.push(ClientShellAction::ActivateEndpoint { + endpoint_id: press.endpoint_id, + target: Some(ClientEndpointFocusTarget::Workspace(press.workspace_id)), + }); + } else { + let label = self.endpoint_label(&press.endpoint_id).to_owned(); + self.receive_endpoint_unavailable(format!("{label} is not ready")); + outcome.repaint = true; + } + } + + pub(super) fn handle_endpoint_machine_click( + &mut self, + point: (u16, u16), + outcome: &mut ClientShellInput, + ) -> bool { + let Some(endpoint_id) = self + .hits + .machines + .iter() + .find(|hit| super::contains(hit.rect, point)) + .map(|hit| hit.endpoint_id.clone()) + else { + return false; + }; + if endpoint_id == self.active_endpoint_id { + if !self.collapsed_endpoints.remove(&endpoint_id) { + self.collapsed_endpoints.insert(endpoint_id); + } + outcome.repaint = true; + } else if self.endpoint_is_online(&endpoint_id) { + outcome.actions.push(ClientShellAction::ActivateEndpoint { + endpoint_id, + target: None, + }); + } else { + let label = self.endpoint_label(&endpoint_id).to_owned(); + self.receive_endpoint_unavailable(format!("{label} is not ready")); + outcome.repaint = true; + } + true + } + + pub(super) fn handle_endpoint_agent_click( + &mut self, + point: (u16, u16), + outcome: &mut ClientShellInput, + ) -> bool { + let Some((endpoint_id, pane_id)) = self + .hits + .endpoint_agents + .iter() + .find(|(rect, _, _)| super::contains(*rect, point)) + .map(|(_, endpoint_id, pane_id)| (endpoint_id.clone(), pane_id.clone())) + else { + return false; + }; + if !self.endpoint_is_online(&endpoint_id) { + let label = self.endpoint_label(&endpoint_id).to_owned(); + self.receive_endpoint_unavailable(format!("{label} is reconnecting")); + outcome.repaint = true; + } else if endpoint_id == self.active_endpoint_id { + self.push_endpoint_method( + crate::api::schema::Method::PaneFocus(crate::api::schema::PaneTarget { pane_id }), + outcome, + ); + } else { + outcome.actions.push(ClientShellAction::ActivateEndpoint { + endpoint_id, + target: Some(ClientEndpointFocusTarget::Pane(pane_id)), + }); + } + true + } + + pub(super) fn handle_endpoint_navigation( + &mut self, + action: crate::input::KeybindAction, + outcome: &mut ClientShellInput, + ) -> bool { + use crate::input::KeybindAction; + if !self.multi_endpoint_active() { + return false; + } + if matches!( + action, + KeybindAction::PreviousWorkspace | KeybindAction::NextWorkspace + ) { + let workspaces = self + .endpoints + .iter() + .filter(|endpoint| endpoint.status == ClientEndpointStatus::Online) + .flat_map(|endpoint| { + endpoint + .snapshot + .as_deref() + .map_or_else(Vec::new, |snapshot| { + render::workspace_entries(snapshot, &HashSet::new()) + .into_iter() + .filter_map(|entry| { + snapshot.workspaces.get(entry.index).map(|workspace| { + ( + endpoint.endpoint_id.clone(), + workspace.workspace_id.clone(), + ) + }) + }) + .collect() + }) + }) + .collect::<Vec<_>>(); + if workspaces.is_empty() { + return true; + } + let focused = self + .snapshot + .as_deref() + .and_then(|snapshot| snapshot.focused_workspace_id.as_deref()); + let current = workspaces.iter().position(|(endpoint_id, workspace_id)| { + endpoint_id == &self.active_endpoint_id && Some(workspace_id.as_str()) == focused + }); + let next = match (current, action) { + (Some(index), KeybindAction::PreviousWorkspace) => { + (index + workspaces.len() - 1) % workspaces.len() + } + (Some(index), KeybindAction::NextWorkspace) => (index + 1) % workspaces.len(), + (None, KeybindAction::PreviousWorkspace) => workspaces.len() - 1, + (None, KeybindAction::NextWorkspace) => 0, + _ => unreachable!("endpoint workspace navigation"), + }; + let (endpoint_id, workspace_id) = workspaces[next].clone(); + self.focus_or_activate( + endpoint_id, + ClientEndpointFocusTarget::Workspace(workspace_id), + outcome, + ); + return true; + } + if matches!( + action, + KeybindAction::PreviousAgent | KeybindAction::NextAgent | KeybindAction::FocusAgent(_) + ) { + let agents = super::aggregate_navigation::online_agent_targets( + &self.endpoints, + self.config.agent_panel_sort, + ); + if agents.is_empty() { + return true; + } + let next = match action { + KeybindAction::FocusAgent(index) => { + if index >= agents.len() { + return true; + } + index + } + KeybindAction::PreviousAgent | KeybindAction::NextAgent => { + let focused = self + .snapshot + .as_deref() + .and_then(|snapshot| snapshot.focused_pane_id.as_deref()); + let current = agents.iter().position(|target| { + target.endpoint_id == self.active_endpoint_id + && Some(target.pane_id.as_str()) == focused + }); + match (current, action) { + (Some(index), KeybindAction::PreviousAgent) => { + (index + agents.len() - 1) % agents.len() + } + (Some(index), KeybindAction::NextAgent) => (index + 1) % agents.len(), + (None, KeybindAction::PreviousAgent) => agents.len() - 1, + _ => 0, + } + } + _ => unreachable!("endpoint agent navigation"), + }; + let target = &agents[next]; + self.focus_or_activate( + target.endpoint_id.clone(), + ClientEndpointFocusTarget::Pane(target.pane_id.clone()), + outcome, + ); + return true; + } + false + } + + pub(super) fn activate_endpoint( + &mut self, + endpoint_id: ClientEndpointId, + outcome: &mut ClientShellInput, + ) -> bool { + if !self.endpoint_is_online(&endpoint_id) { + let label = self.endpoint_label(&endpoint_id).to_owned(); + self.receive_endpoint_unavailable(format!("{label} is not ready")); + outcome.repaint = true; + return false; + } + if endpoint_id != self.active_endpoint_id { + outcome.actions.push(ClientShellAction::ActivateEndpoint { + endpoint_id, + target: None, + }); + } + true + } + + pub(super) fn focus_or_activate( + &mut self, + endpoint_id: ClientEndpointId, + target: ClientEndpointFocusTarget, + outcome: &mut ClientShellInput, + ) -> bool { + if !self.endpoint_is_online(&endpoint_id) { + let label = self.endpoint_label(&endpoint_id).to_owned(); + self.receive_endpoint_unavailable(format!("{label} is not ready")); + outcome.repaint = true; + return false; + } + if endpoint_id == self.active_endpoint_id { + let method = match target { + ClientEndpointFocusTarget::Workspace(workspace_id) => { + crate::api::schema::Method::WorkspaceFocus( + crate::api::schema::WorkspaceTarget { workspace_id }, + ) + } + ClientEndpointFocusTarget::Tab(tab_id) => { + crate::api::schema::Method::TabFocus(crate::api::schema::TabTarget { tab_id }) + } + ClientEndpointFocusTarget::Pane(pane_id) => { + crate::api::schema::Method::PaneFocus(crate::api::schema::PaneTarget { + pane_id, + }) + } + }; + self.push_endpoint_method(method, outcome); + } else { + outcome.actions.push(ClientShellAction::ActivateEndpoint { + endpoint_id, + target: Some(target), + }); + } + true + } +} |
