diff --git a/src/app/actions.rs b/src/app/actions.rs index 5c263e7e6b..bd4a6e94c7 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -355,8 +355,10 @@ impl AppState { self.navigator.scroll = 0; self.navigator.expanded_workspaces.clear(); - for ws in &self.workspaces { - self.navigator.expanded_workspaces.insert(ws.id.clone()); + if !self.navigator_collapse_workspaces { + for ws in &self.workspaces { + self.navigator.expanded_workspaces.insert(ws.id.clone()); + } } self.mode = Mode::Navigator; @@ -3419,7 +3421,7 @@ mod tests { } #[test] - fn opening_navigator_selects_current_pane_and_expands_attention_workspaces() { + fn opening_navigator_selects_current_pane_and_expands_workspaces_by_default() { let mut state = app_with_workspaces(&["one", "two"]); let blocked = state.workspaces[1].tabs[0].root_pane; let blocked_terminal_id = state.workspaces[1].terminal_id(blocked).cloned().unwrap(); @@ -3443,6 +3445,49 @@ mod tests { .contains(&state.workspaces[1].id)); } + #[test] + fn navigator_collapse_workspaces_option_opens_collapsed_rows() { + let mut state = app_with_workspaces(&["one", "two"]); + state.navigator_collapse_workspaces = true; + state.open_navigator(); + + assert!(state.navigator.expanded_workspaces.is_empty()); + let rows = state.navigator_rows(); + assert!(rows.iter().all(|row| row.is_workspace)); + assert!(!rows.iter().any(|row| matches!( + row.target, + crate::app::state::NavigatorTarget::Pane { .. } + | crate::app::state::NavigatorTarget::Tab { .. } + ))); + let selected = rows[state.navigator.selected].clone(); + assert!(selected.is_workspace); + assert!(selected.is_current); + } + + #[test] + fn navigator_query_expands_collapsed_workspaces_to_matching_panes() { + let mut state = app_with_workspaces(&["one"]); + let root = state.workspaces[0].tabs[0].root_pane; + let terminal_id = state.workspaces[0].terminal_id(root).cloned().unwrap(); + state + .terminals + .get_mut(&terminal_id) + .unwrap() + .set_manual_label("weekly review".into()); + + state.navigator_collapse_workspaces = true; + state.open_navigator(); + assert!(state.navigator.expanded_workspaces.is_empty()); + assert!(state.navigator_rows().iter().all(|row| row.is_workspace)); + + state.navigator.query = "weekly".into(); + let rows = state.navigator_rows(); + assert!(rows.iter().any(|row| matches!( + row.target, + crate::app::state::NavigatorTarget::Pane { .. } + ) && row.label.contains("weekly"))); + } + #[test] fn accepting_navigator_pane_switches_workspace_tab_and_focus() { let mut state = app_with_workspaces(&["one", "two"]); diff --git a/src/app/mod.rs b/src/app/mod.rs index 1a0c77a3ea..86b7361eb3 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -601,6 +601,7 @@ impl App { next_agent_state_change_seq: 0, mouse_capture: config.ui.mouse_capture, copy_on_select: config.ui.copy_on_select, + navigator_collapse_workspaces: config.ui.navigator_collapse_workspaces, right_click_passthrough_modifiers: config.ui.right_click_passthrough_modifiers(), right_click_passthrough: None, redraw_on_focus_gained: config.ui.redraw_on_focus_gained, @@ -1360,6 +1361,7 @@ impl App { .clamp(self.state.sidebar_min_width, self.state.sidebar_max_width); self.state.mouse_capture = config.ui.mouse_capture; self.state.copy_on_select = config.ui.copy_on_select; + self.state.navigator_collapse_workspaces = config.ui.navigator_collapse_workspaces; if !self.state.copy_on_select { if self.state.mode == Mode::Copy { self.state.stop_selection_autoscroll_state(); diff --git a/src/app/state.rs b/src/app/state.rs index be858678da..4b99d3ad39 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1408,6 +1408,7 @@ pub struct AppState { /// captures mouse while the focused pane app requests mouse reporting. pub mouse_capture: bool, pub copy_on_select: bool, + pub navigator_collapse_workspaces: bool, pub right_click_passthrough_modifiers: Option, pub right_click_passthrough: Option, pub redraw_on_focus_gained: bool, @@ -1767,6 +1768,7 @@ impl AppState { next_agent_state_change_seq: 0, mouse_capture: true, copy_on_select: true, + navigator_collapse_workspaces: false, right_click_passthrough_modifiers: None, right_click_passthrough: None, redraw_on_focus_gained: true, diff --git a/src/config/model.rs b/src/config/model.rs index 16bc595df5..0021605c0a 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -788,6 +788,9 @@ pub struct UiConfig { pub mouse_capture: bool, /// Copy text selected with the mouse. Default: true. pub copy_on_select: bool, + /// Open the navigator (goto) collapsed to the workspace level instead of + /// expanding every workspace to its panes on open. Default: false. + pub navigator_collapse_workspaces: bool, /// Host cursor policy. Default: auto. pub host_cursor: HostCursorModeConfig, /// Modifier that lets right-click gestures pass through to pane apps. Empty disables it. @@ -992,6 +995,7 @@ impl Default for UiConfig { mobile_width_threshold: DEFAULT_MOBILE_WIDTH_THRESHOLD, mouse_capture: true, copy_on_select: true, + navigator_collapse_workspaces: false, host_cursor: HostCursorModeConfig::Auto, right_click_passthrough_modifier: RightClickPassthroughModifierConfig::default(), redraw_on_focus_gained: true, diff --git a/src/main.rs b/src/main.rs index 57ca4dc22c..5d98b8475f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -263,6 +263,10 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration # Set false to disable mouse text selection and copying. # copy_on_select = true +# Open the navigator (goto) collapsed to the workspace level instead of +# expanding every workspace to its panes on open. +# navigator_collapse_workspaces = false + # Host cursor policy: "auto", "native", or "drawn". # "auto" draws Herdr's own cursor on native Windows builds and WSL to avoid ConPTY cursor flicker, and uses the native terminal cursor elsewhere. # "native" always uses the outer terminal cursor. "drawn" always draws Herdr's cursor as terminal cell content.