aboutsummaryrefslogtreecommitdiff
path: root/src/client/shell/workspace_navigation.rs
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-09-09 02:35:31 +0300
committerGitHub <noreply@github.com>2026-09-09 02:35:31 +0300
commitb9ce96869e89937278d673d70ae4c135dd318469 (patch)
treefea7e39dad42f8cad1f1846af6babda546c1fa78 /src/client/shell/workspace_navigation.rs
parent8be4cf765afddfd9cf3206602ba4dab682a29fa1 (diff)
feat: navigate and highlight workspaces across machines (#3755)
* fix: highlight workspace navigation with saved machines refs #3754 * feat: navigate workspaces across connected machines refs #3754 * feat: navigate workspaces across machines refs #3754 --------- Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com> Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/client/shell/workspace_navigation.rs')
-rw-r--r--src/client/shell/workspace_navigation.rs153
1 files changed, 153 insertions, 0 deletions
diff --git a/src/client/shell/workspace_navigation.rs b/src/client/shell/workspace_navigation.rs
new file mode 100644
index 00000000..6139fe0d
--- /dev/null
+++ b/src/client/shell/workspace_navigation.rs
@@ -0,0 +1,153 @@
+use super::*;
+
+/// A client-only preview. Snapshot identity prevents Enter from using a reused workspace ID.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(super) struct WorkspaceNavigationTarget {
+ pub(super) endpoint_id: ClientEndpointId,
+ pub(super) workspace_id: String,
+ boot_id: String,
+ generation: Option<u64>,
+}
+
+impl WorkspaceNavigationTarget {
+ pub(super) fn matches(&self, endpoint_id: &ClientEndpointId, workspace_id: &str) -> bool {
+ &self.endpoint_id == endpoint_id && self.workspace_id == workspace_id
+ }
+}
+
+impl ClientShellState {
+ pub(super) fn navigation_target(
+ &self,
+ endpoint_id: &ClientEndpointId,
+ workspace_id: &str,
+ ) -> Option<WorkspaceNavigationTarget> {
+ let endpoint = self
+ .endpoints
+ .iter()
+ .find(|entry| &entry.endpoint_id == endpoint_id)?;
+ let snapshot = endpoint.snapshot.as_deref()?;
+ Some(WorkspaceNavigationTarget {
+ endpoint_id: endpoint_id.clone(),
+ workspace_id: workspace_id.to_owned(),
+ boot_id: snapshot.boot_id.clone(),
+ generation: endpoint.snapshot_generation,
+ })
+ }
+
+ pub(super) fn focused_navigation_target(&self) -> Option<WorkspaceNavigationTarget> {
+ let workspace_id = self.snapshot.as_deref()?.focused_workspace_id.as_deref()?;
+ self.navigation_target(&self.active_endpoint_id, workspace_id)
+ }
+
+ pub(super) fn navigation_target_valid(&self, target: &WorkspaceNavigationTarget) -> bool {
+ self.endpoints.iter().any(|endpoint| {
+ endpoint.endpoint_id == target.endpoint_id
+ && endpoint.status == ClientEndpointStatus::Online
+ && endpoint.snapshot_generation == target.generation
+ && endpoint.snapshot.as_deref().is_some_and(|snapshot| {
+ snapshot.boot_id == target.boot_id
+ && snapshot
+ .workspaces
+ .iter()
+ .any(|workspace| workspace.workspace_id == target.workspace_id)
+ })
+ })
+ }
+
+ pub(super) fn workspace_preview_action_blocked(&self) -> bool {
+ self.navigate_workspace_id.as_ref().is_some_and(|target| {
+ target.endpoint_id != self.active_endpoint_id || !self.navigation_target_valid(target)
+ })
+ }
+
+ pub(super) fn move_navigate_workspace(&mut self, delta: isize) {
+ let mobile = self.mobile_layout_active();
+ let surface_available = self.snapshot.is_some() && self.pane_surface.is_some();
+ let empty_collapsed_groups = HashSet::new();
+ let mut targets = Vec::new();
+ for endpoint in &self.endpoints {
+ if endpoint.status != ClientEndpointStatus::Online {
+ continue;
+ }
+ let Some(snapshot) = endpoint.snapshot.as_deref() else {
+ continue;
+ };
+ let entries = if self.sidebar_collapsed && !mobile && surface_available {
+ snapshot
+ .workspaces
+ .iter()
+ .enumerate()
+ .map(|(index, _)| WorkspaceEntry {
+ index,
+ indented: false,
+ last_child: false,
+ })
+ .collect()
+ } else {
+ let collapsed_groups = if mobile && surface_available {
+ &empty_collapsed_groups
+ } else {
+ self.collapsed_groups_for_endpoint(&endpoint.endpoint_id)
+ .unwrap_or(&empty_collapsed_groups)
+ };
+ render::workspace_entries(snapshot, collapsed_groups)
+ };
+ for entry in entries {
+ targets.push(WorkspaceNavigationTarget {
+ endpoint_id: endpoint.endpoint_id.clone(),
+ workspace_id: snapshot.workspaces[entry.index].workspace_id.clone(),
+ boot_id: snapshot.boot_id.clone(),
+ generation: endpoint.snapshot_generation,
+ });
+ }
+ }
+ if targets.is_empty() {
+ return;
+ }
+ let current = self
+ .navigate_workspace_id
+ .as_ref()
+ .and_then(|selected| targets.iter().position(|target| target == selected));
+ let next = match current {
+ Some(current) if mobile => {
+ (current as isize + delta).clamp(0, targets.len() as isize - 1) as usize
+ }
+ Some(current) => (current as isize + delta).rem_euclid(targets.len() as isize) as usize,
+ None if delta < 0 => targets.len() - 1,
+ None => 0,
+ };
+ let target = targets.swap_remove(next);
+ self.collapsed_endpoints.remove(&target.endpoint_id);
+ if self.endpoints.len() == 1 && !mobile {
+ self.reveal_workspace(&target.workspace_id);
+ }
+ self.navigate_workspace_id = Some(target);
+ self.reveal_mobile_workspace = mobile;
+ self.reveal_navigation_workspace =
+ !mobile || self.snapshot.is_none() || self.pane_surface.is_none();
+ }
+
+ pub(super) fn accept_navigate_workspace(&mut self, outcome: &mut ClientShellInput) {
+ let Some(target) = self.navigate_workspace_id.clone() else {
+ self.mode = self.copy_or_terminal_mode();
+ outcome.repaint = true;
+ return;
+ };
+ if !self.navigation_target_valid(&target) {
+ self.receive_endpoint_unavailable(
+ "Workspace is no longer available; select a connected workspace".into(),
+ );
+ outcome.repaint = true;
+ return;
+ }
+ if self.focus_or_activate(
+ target.endpoint_id,
+ ClientEndpointFocusTarget::Workspace(target.workspace_id),
+ outcome,
+ ) {
+ self.mode = ClientShellMode::Terminal;
+ self.navigate_workspace_id = None;
+ }
+ outcome.repaint = true;
+ }
+}