aboutsummaryrefslogtreecommitdiff
path: root/src/app/state.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-01 15:48:22 +0300
committerGitHub <noreply@github.com>2026-09-01 15:48:22 +0300
commit207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch)
treed7b037ecdf629224a048c841d11659efdc23507d /src/app/state.rs
parent8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff)
refactor: render the shell in the client (#3487)
* feat: add opt-in client-rendered shell * feat: add client-owned shell mouse controls * feat: complete client-owned shell chrome * feat: add client-owned shell settings and notifications * feat: run client shell custom commands * feat: render popup terminals in client shell * feat: add client-owned scrollback and copy mode * fix: preserve client worktree trust defaults * feat: render startup diagnostics in client shell * feat: render onboarding in client shell * feat: render product announcements in client shell * feat: render release notes in client shell * feat: render mobile switcher in client shell * feat: render kitty graphics in client shell * fix: preserve client shell lifecycle coherence * refactor: route client shell commands through endpoint connections * refactor: make the client-rendered shell the default * refactor: remove monolithic launch mode * refactor: complete client-rendered shell cutover * perf: retain client pane surface updates * perf: scale retained pane surface updates * perf: remove remote pane input latency * test: make client shell checks platform-independent * fix: prevent client timer starvation * test: fix client shell platform gates * test: accept retained surfaces in cross-area checks * test: make client mode assertions platform-neutral * fix: badge only outdated integrations
Diffstat (limited to 'src/app/state.rs')
-rw-r--r--src/app/state.rs1240
1 files changed, 47 insertions, 1193 deletions
diff --git a/src/app/state.rs b/src/app/state.rs
index 782b8ae5..0ce9fb6d 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -1,13 +1,10 @@
-use crate::config::{
- Keybinds, NewTerminalCwdConfig, SoundConfig, TabBarPositionConfig, ToastConfig, ToastDelivery,
-};
+use crate::config::{Keybinds, NewTerminalCwdConfig, SoundConfig, ToastConfig};
use crossterm::event::{KeyCode, KeyModifiers};
-use ratatui::layout::{Direction, Rect};
+use ratatui::layout::Rect;
use ratatui::style::Color;
use crate::detect::AgentState;
-use crate::layout::{PaneId, PaneInfo, SplitBorder};
-use crate::selection::Selection;
+use crate::layout::{PaneId, PaneInfo};
pub(crate) type InstalledPluginRegistry =
std::collections::HashMap<String, crate::api::schema::InstalledPluginInfo>;
@@ -25,36 +22,6 @@ pub(crate) struct PopupPaneState {
pub height: Option<crate::popup_size::PopupSize>,
}
-// ---------------------------------------------------------------------------
-// Selection autoscroll types
-// ---------------------------------------------------------------------------
-
-/// Direction of automatic scrolling during text selection drag.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub(crate) enum SelectionAutoscrollDirection {
- Up,
- Down,
-}
-
-/// State for automatic scrolling during text selection drag.
-///
-/// When the cursor hovers in the 1-row hot zone at the top or bottom edge
-/// of a pane (or outside the pane), this struct captures the direction and
-/// last known mouse position so a recurring 30ms tick can continue scrolling
-/// and extending the selection even when the mouse is not moving.
-#[derive(Clone, Debug)]
-pub(crate) struct SelectionAutoscroll {
- pub direction: SelectionAutoscrollDirection,
- pub last_mouse_screen_col: u16,
- pub last_mouse_screen_row: u16,
- pub inner_rect: Rect,
-}
-
-#[derive(Clone)]
-pub(crate) struct RightClickPassthroughGesture {
- pub pane_info: PaneInfo,
- pub modifiers: KeyModifiers,
-}
use crate::terminal_theme::{HostAppearance, TerminalTheme};
use crate::workspace::Workspace;
@@ -65,7 +32,6 @@ use crate::workspace::Workspace;
/// All colors used by the UI. Derived from a base accent color for now,
/// but structured so a full theme system can replace it later.
#[derive(Debug, Clone, PartialEq, Eq)]
-#[allow(dead_code)] // all fields defined for theming — some used later
pub struct Palette {
/// Primary accent (highlight, active borders).
pub accent: Color,
@@ -709,364 +675,16 @@ impl Palette {
}
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct WorkspaceCardArea {
- pub ws_idx: usize,
- pub rect: Rect,
- pub indented: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct WorktreeCreateState {
- pub source_workspace_id: String,
- pub source_checkout_path: std::path::PathBuf,
- pub source_existing_membership: Option<crate::workspace::WorktreeSpaceMembership>,
- pub source_repo_root: std::path::PathBuf,
- pub repo_key: String,
- pub repo_name: String,
- pub branch: String,
- pub checkout_path: std::path::PathBuf,
- pub error: Option<String>,
- pub creating: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct WorktreeRemoveState {
- pub workspace_id: String,
- pub repo_root: std::path::PathBuf,
- pub path: std::path::PathBuf,
- pub error: Option<String>,
- pub removing: bool,
- pub force_confirmation: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct WorktreeOpenEntry {
- pub path: std::path::PathBuf,
- pub branch: Option<String>,
- pub is_linked_worktree: bool,
- pub already_open_ws_idx: Option<usize>,
-}
-
-impl WorktreeOpenEntry {
- pub(crate) fn display_name(&self) -> String {
- self.branch.clone().unwrap_or_else(|| {
- self.path
- .file_name()
- .and_then(|name| name.to_str())
- .map(str::to_owned)
- .unwrap_or_else(|| self.path.display().to_string())
- })
- }
-
- pub(crate) fn status_label(&self) -> &'static str {
- if self.already_open_ws_idx.is_some() {
- "open"
- } else if self.branch.is_some() {
- ""
- } else if self.is_linked_worktree {
- "detached"
- } else {
- "root"
- }
- }
-
- fn search_text(&self) -> String {
- format!(
- "{} {} {} {}",
- self.display_name(),
- self.path
- .file_name()
- .and_then(|name| name.to_str())
- .unwrap_or_default(),
- self.path.display(),
- self.status_label()
- )
- .to_lowercase()
- }
-
- fn matches_query(&self, query: &str) -> bool {
- text_matches_query(query, &self.search_text())
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct WorktreeOpenState {
- pub source_workspace_id: String,
- pub source_existing_membership: Option<crate::workspace::WorktreeSpaceMembership>,
- pub source_checkout_path: std::path::PathBuf,
- pub source_repo_root: std::path::PathBuf,
- pub repo_key: String,
- pub repo_name: String,
- pub entries: Vec<WorktreeOpenEntry>,
- pub selected: usize,
- pub query: String,
- pub search_focused: bool,
- pub error: Option<String>,
-}
-
-impl WorktreeOpenState {
- pub(crate) fn filtered_indices(&self) -> Vec<usize> {
- let query = self.query.trim();
- self.entries
- .iter()
- .enumerate()
- .filter_map(|(idx, entry)| {
- (query.is_empty() || entry.matches_query(query)).then_some(idx)
- })
- .collect()
- }
-
- pub(crate) fn selected_entry_index(&self) -> Option<usize> {
- let indices = self.filtered_indices();
- if indices.contains(&self.selected) {
- Some(self.selected)
- } else {
- indices.first().copied()
- }
- }
-
- pub(crate) fn normalize_selection(&mut self) {
- if let Some(selected) = self.selected_entry_index() {
- self.selected = selected;
- }
- }
-
- pub(crate) fn select_previous_filtered(&mut self) {
- let indices = self.filtered_indices();
- let Some(current) = self.selected_entry_index() else {
- return;
- };
- let pos = indices.iter().position(|idx| *idx == current).unwrap_or(0);
- self.selected = indices[pos.saturating_sub(1)];
- }
-
- pub(crate) fn select_next_filtered(&mut self) {
- let indices = self.filtered_indices();
- let Some(current) = self.selected_entry_index() else {
- return;
- };
- let pos = indices.iter().position(|idx| *idx == current).unwrap_or(0);
- self.selected = indices[(pos + 1).min(indices.len().saturating_sub(1))];
- }
-}
-
-pub(crate) fn text_matches_query(query: &str, text: &str) -> bool {
- let haystack = text.to_lowercase();
- query
- .to_lowercase()
- .split_whitespace()
- .all(|needle| haystack.contains(needle))
-}
-
-/// Computed view geometry — derived from AppState + terminal size.
-/// Updated before each render, consumed by render and mouse handling.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ViewLayout {
- Desktop,
- Mobile,
-}
-
+/// Geometry for the server-rendered active-tab pane surface.
pub struct ViewState {
- pub layout: ViewLayout,
- pub sidebar_rect: Rect,
- pub workspace_card_areas: Vec<WorkspaceCardArea>,
- pub tab_bar_rect: Rect,
- pub tab_hit_areas: Vec<Rect>,
- pub tab_scroll_left_hit_area: Rect,
- pub tab_scroll_right_hit_area: Rect,
- pub new_tab_hit_area: Rect,
pub terminal_area: Rect,
- pub mobile_header_rect: Rect,
- pub mobile_menu_hit_area: Rect,
- pub toast_hit_area: Rect,
pub pane_infos: Vec<PaneInfo>,
- pub split_borders: Vec<SplitBorder>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
- Onboarding,
- ReleaseNotes,
- ProductAnnouncement,
Navigate,
- Prefix,
- Copy,
Terminal,
- RenameWorkspace,
- RenameTab,
- RenamePane,
- NewLinkedWorktree,
- OpenExistingWorktree,
- ConfirmRemoveWorktree,
- Resize,
- ConfirmClose,
- ContextMenu,
- Settings,
- GlobalMenu,
- KeybindHelp,
- Navigator,
-}
-
-impl Mode {
- pub(crate) fn mouse_motion_changes_view(self) -> bool {
- matches!(self, Self::GlobalMenu | Self::ContextMenu | Self::Navigator)
- }
-
- /// Whether keys in this mode are commands/navigation (an ASCII input source is wanted) rather
- /// than free text. This is an explicit **allowlist** of the prefix command/navigation realm:
- /// any mode NOT listed defaults to leaving the user's IME alone (the safe default), so adding a
- /// new text-entry or overlay mode can never silently force ASCII. Used by
- /// `sync_prefix_input_source` (gated by `switch_ascii_input_source_in_prefix`) so multi-level
- /// prefix commands keep ASCII until they return to the terminal.
- ///
- /// Known limitation: the search boxes in `Navigator` and `KeybindHelp` are also held on ASCII,
- /// since this `Mode`-level predicate can't see `search_focused` (non-ASCII filtering there
- /// would need a runtime check).
- pub(crate) fn wants_ascii_input(self) -> bool {
- matches!(
- self,
- Mode::Prefix
- | Mode::Navigate
- | Mode::Navigator
- | Mode::Copy
- | Mode::Resize
- | Mode::ConfirmClose
- | Mode::ConfirmRemoveWorktree
- | Mode::ContextMenu
- | Mode::GlobalMenu
- | Mode::KeybindHelp
- )
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) enum NavigatorTarget {
- Workspace {
- ws_idx: usize,
- },
- Tab {
- ws_idx: usize,
- tab_idx: usize,
- },
- Pane {
- ws_idx: usize,
- tab_idx: usize,
- pane_id: PaneId,
- },
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) struct NavigatorRow {
- pub target: NavigatorTarget,
- pub depth: u8,
- pub label: String,
- pub meta: String,
- pub status: AgentState,
- pub seen: bool,
- pub is_current: bool,
- pub is_workspace: bool,
- pub is_tab: bool,
- pub expanded: bool,
- pub search_text: String,
- /// Whether this row itself matched the active query/state filter, as
- /// opposed to being included as ancestor context or cascaded subtree of a
- /// matching workspace or tab. Always true when no filter is active.
- pub matched: bool,
-}
-
-/// One rendered line in the navigator body. Spacer lines separate workspace
-/// groups visually and are not selectable.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum NavigatorDisplayLine {
- Spacer,
- Row(usize),
-}
-
-pub(crate) fn navigator_display_lines(rows: &[NavigatorRow]) -> Vec<NavigatorDisplayLine> {
- let mut lines = Vec::with_capacity(rows.len().saturating_mul(2));
- for (idx, row) in rows.iter().enumerate() {
- if row.is_workspace && !lines.is_empty() {
- lines.push(NavigatorDisplayLine::Spacer);
- }
- lines.push(NavigatorDisplayLine::Row(idx));
- }
- lines
-}
-
-pub(crate) fn navigator_display_index_of_row(
- lines: &[NavigatorDisplayLine],
- row_idx: usize,
-) -> Option<usize> {
- lines
- .iter()
- .position(|line| *line == NavigatorDisplayLine::Row(row_idx))
-}
-
-pub(crate) fn navigator_first_row_at_or_after(
- lines: &[NavigatorDisplayLine],
- line_idx: usize,
-) -> Option<usize> {
- lines.get(line_idx..)?.iter().find_map(|line| match line {
- NavigatorDisplayLine::Row(idx) => Some(*idx),
- NavigatorDisplayLine::Spacer => None,
- })
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum NavigatorStateFilter {
- Blocked,
- Working,
- Idle,
- Done,
-}
-
-#[derive(Debug, Clone, Default)]
-pub(crate) struct NavigatorState {
- pub query: String,
- pub selected: usize,
- pub scroll: usize,
- pub search_focused: bool,
- pub state_filter: Option<NavigatorStateFilter>,
- pub expanded_workspaces: std::collections::HashSet<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) struct CopyModeState {
- pub pane_id: PaneId,
- pub cursor_row: u16,
- pub cursor_col: u16,
- pub entry_offset_from_bottom: usize,
- pub selection: Option<CopyModeSelection>,
- pub search: CopyModeSearchState,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum CopyModeSelection {
- Character,
- Linewise { anchor_row: u32 },
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum CopyModeSearchDirection {
- Forward,
- Backward,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) struct CopyModeSearchPrompt {
- pub direction: CopyModeSearchDirection,
- pub query: String,
-}
-
-#[derive(Debug, Clone, Default, PartialEq, Eq)]
-pub(crate) struct CopyModeSearchState {
- pub prompt: Option<CopyModeSearchPrompt>,
- pub query: String,
- pub direction: Option<CopyModeSearchDirection>,
- pub matches: Vec<crate::pane::TerminalTextMatch>,
- pub current: Option<usize>,
- pub geometry: Option<(u16, u16)>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
@@ -1076,98 +694,6 @@ pub enum AgentPanelSort {
Priority,
}
-// ---------------------------------------------------------------------------
-// Settings UI state
-// ---------------------------------------------------------------------------
-
-/// Which section of the settings panel is focused.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum SettingsSection {
- Theme,
- Indicators,
- Sound,
- Toast,
- PaneLabels,
- Integrations,
-}
-
-impl SettingsSection {
- pub const ALL: &[Self] = &[
- Self::Theme,
- Self::Indicators,
- Self::Sound,
- Self::Toast,
- Self::PaneLabels,
- Self::Integrations,
- ];
-
- pub fn label(self) -> &'static str {
- match self {
- Self::Theme => "theme",
- Self::Indicators => "indicators",
- Self::Sound => "sound",
- Self::Toast => "toasts",
- Self::PaneLabels => "pane labels",
- Self::Integrations => "integrations",
- }
- }
-}
-
-/// All built-in theme names in display order.
-pub const THEME_NAMES: &[&str] = crate::config::THEME_NAMES;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct MenuListState {
- pub highlighted: usize,
-}
-
-impl MenuListState {
- pub fn new(highlighted: usize) -> Self {
- Self { highlighted }
- }
-
- pub fn move_prev(&mut self) {
- self.highlighted = self.highlighted.saturating_sub(1);
- }
-
- pub fn move_next(&mut self, item_count: usize) {
- if item_count > 0 {
- self.highlighted = (self.highlighted + 1).min(item_count - 1);
- }
- }
-
- pub fn hover(&mut self, idx: Option<usize>) {
- if let Some(idx) = idx {
- self.highlighted = idx;
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct SelectionListState {
- pub selected: usize,
-}
-
-impl SelectionListState {
- pub fn new(selected: usize) -> Self {
- Self { selected }
- }
-
- pub fn move_prev(&mut self) {
- self.selected = self.selected.saturating_sub(1);
- }
-
- pub fn move_next(&mut self, item_count: usize) {
- if item_count > 0 {
- self.selected = (self.selected + 1).min(item_count - 1);
- }
- }
-
- pub fn select(&mut self, idx: usize) {
- self.selected = idx;
- }
-}
-
#[derive(Debug, Clone)]
pub struct ThemeRuntimeConfig {
pub manual_name: String,
@@ -1178,167 +704,6 @@ pub struct ThemeRuntimeConfig {
pub legacy_accent: Option<String>,
}
-pub struct SettingsState {
- /// Which section tab is active.
- pub section: SettingsSection,
- /// Selected item index within the current section.
- pub list: SelectionListState,
- /// The palette before opening settings (for cancel/restore).
- pub original_palette: Option<Palette>,
- /// The theme name before opening settings.
- pub original_theme: Option<String>,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum WorkspaceDropTarget {
- Before(usize),
- End,
-}
-
-pub(crate) enum DragTarget {
- WorkspaceReorder {
- source_id: crate::app::InputSourceId,
- source_ws_idx: usize,
- drop_target: Option<WorkspaceDropTarget>,
- },
- TabReorder {
- source_id: crate::app::InputSourceId,
- ws_idx: usize,
- source_tab_idx: usize,
- insert_idx: Option<usize>,
- },
- WorkspaceListScrollbar {
- grab_row_offset: u16,
- },
- AgentPanelScrollbar {
- grab_row_offset: u16,
- },
- PaneSplit {
- path: Vec<bool>,
- direction: Direction,
- area: Rect,
- grab_offset: u16,
- },
- PaneScrollbar {
- pane_id: crate::layout::PaneId,
- grab_row_offset: u16,
- },
- ReleaseNotesScrollbar {
- grab_row_offset: u16,
- },
- ProductAnnouncementScrollbar {
- grab_row_offset: u16,
- },
- KeybindHelpScrollbar {
- grab_row_offset: u16,
- },
- SidebarDivider,
- SidebarSectionDivider,
-}
-
-/// Active mouse drag on a split border or sidebar divider.
-pub(crate) struct DragState {
- pub target: DragTarget,
-}
-
-pub(crate) struct WorkspacePressState {
- pub ws_idx: usize,
- pub start_col: u16,
- pub start_row: u16,
-}
-
-pub(crate) struct TabPressState {
- pub ws_idx: usize,
- pub tab_idx: usize,
- pub start_col: u16,
- pub start_row: u16,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum ContextMenuKind {
- Workspace {
- ws_idx: usize,
- },
- GitWorkspace {
- ws_idx: usize,
- is_linked_worktree: bool,
- has_worktree_children: bool,
- collapsed: bool,
- },
- Tab {
- ws_idx: usize,
- tab_idx: usize,
- },
- Pane {
- ws_idx: usize,
- tab_idx: usize,
- pane_id: PaneId,
- source_pane_id: Option<PaneId>,
- has_manual_label: bool,
- right_click_passthrough: bool,
- },
-}
-
-/// Right-click context menu state.
-pub struct ContextMenuState {
- pub kind: ContextMenuKind,
- pub x: u16,
- pub y: u16,
- pub list: MenuListState,
-}
-
-impl ContextMenuState {
- pub fn items(&self) -> Vec<&'static str> {
- match self.kind {
- ContextMenuKind::Workspace { .. } => vec!["Rename", "Close"],
- ContextMenuKind::GitWorkspace {
- is_linked_worktree: false,
- has_worktree_children: false,
- ..
- } => vec!["Rename", "Close", "New worktree", "Open worktree..."],
- ContextMenuKind::GitWorkspace {
- is_linked_worktree: true,
- ..
- } => vec!["Rename", "Close", "Delete worktree checkout..."],
- ContextMenuKind::GitWorkspace {
- is_linked_worktree: false,
- has_worktree_children: true,
- collapsed,
- ..
- } => vec![
- "Rename",
- "Close group",
- "New worktree",
- "Open worktree...",
- if collapsed { "Expand" } else { "Collapse" },
- ],
- ContextMenuKind::Tab { .. } => vec!["New tab", "Rename", "Close"],
- ContextMenuKind::Pane {
- source_pane_id,
- has_manual_label,
- right_click_passthrough,
- ..
- } => {
- let mut items = vec!["Rename pane"];
- if has_manual_label {
- items.push("Clear pane name");
- }
- if source_pane_id.is_some() {
- items.push("Swap with focused pane");
- }
- items.extend(["Split right", "Split down", "Zoom"]);
- items.push(if right_click_passthrough {
- "Use Herdr right-click menu"
- } else {
- "Send right-clicks to pane"
- });
- items.push("Close pane");
- items
- }
- }
- }
-}
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToastKind {
NeedsAttention,
@@ -1389,6 +754,7 @@ pub struct CopyFeedback {
pub message: String,
}
+#[derive(Debug)]
pub struct ReleaseNotesState {
pub version: String,
pub body: String,
@@ -1396,6 +762,7 @@ pub struct ReleaseNotesState {
pub preview: bool,
}
+#[derive(Debug)]
pub struct ProductAnnouncementState {
pub version: String,
pub id: String,
@@ -1405,20 +772,6 @@ pub struct ProductAnnouncementState {
pub preview: bool,
}
-#[derive(Default)]
-pub struct KeybindHelpState {
- pub scroll: u16,
- pub query: String,
- pub search_focused: bool,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum SidebarWidthSource {
- ConfigDefault,
- Persisted,
- Manual,
-}
-
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PaneFocusTarget {
pub workspace_id: String,
@@ -1445,61 +798,16 @@ pub struct AppState {
pub(crate) previous_pane_focus: Option<PaneFocusTarget>,
pub selected: usize,
pub mode: Mode,
- /// Stable workspace identity captured when the close confirmation opens.
- pub(crate) confirm_close_workspace_id: Option<String>,
pub should_quit: bool,
- /// In monolithic --no-session mode, detach exits the app because there is no server to detach from.
- pub detach_exits: bool,
- /// Set when the current client should detach from the persistent session.
- /// The server's event loop checks this and handles client detach.
- pub detach_requested: bool,
- pub request_new_workspace: bool,
- pub request_new_tab: bool,
- pub request_new_linked_worktree: Option<usize>,
- pub request_open_existing_worktree: Option<usize>,
- pub request_new_workspace_cwd: Option<std::path::PathBuf>,
- pub request_remove_linked_worktree: Option<usize>,
- pub request_submit_worktree_create: bool,
- pub request_submit_worktree_open: bool,
- pub request_submit_worktree_remove: bool,
- pub request_reload_config: bool,
/// Set when the headless server should ask attached clients to reload
/// their client-local sound config from disk.
pub request_client_config_reload: bool,
- /// Set when UI interaction requested a clipboard write that must be
- /// handled by the outer App/event loop instead of directly from AppState.
- pub request_clipboard_write: Option<Vec<u8>>,
- pub creating_new_tab: bool,
- pub requested_new_tab_name: Option<String>,
- pub pending_workspace_create_cwd: Option<std::path::PathBuf>,
- pub rename_pane_target: Option<PaneId>,
- pub worktree_create: Option<WorktreeCreateState>,
- pub worktree_open: Option<WorktreeOpenState>,
- pub worktree_remove: Option<WorktreeRemoveState>,
pub worktree_directory: std::path::PathBuf,
- pub collapsed_space_keys: std::collections::HashSet<String>,
- pub request_complete_onboarding: bool,
- pub name_input: String,
- pub name_input_replace_on_type: bool,
- pub release_notes: Option<ReleaseNotesState>,
+ /// Latest endpoint-owned release notes, cached outside render paths.
+ pub latest_release_notes: Option<crate::release_notes::ReleaseNotes>,
pub product_announcement: Option<ProductAnnouncementState>,
- pub keybind_help: KeybindHelpState,
- pub navigator: NavigatorState,
- pub copy_mode: Option<CopyModeState>,
- pub workspace_scroll: usize,
- pub agent_panel_scroll: usize,
- pub tab_scroll: usize,
- pub tab_scroll_follow_active: bool,
- pub mobile_switcher_scroll: usize,
- // View geometry (computed before render, consumed by render + mouse)
+ // Geometry of the most recently computed server pane surface.
pub view: ViewState,
- pub(crate) drag: Option<DragState>,
- pub(crate) workspace_presses:
- std::collections::HashMap<crate::app::InputSourceId, WorkspacePressState>,
- pub(crate) tab_presses: std::collections::HashMap<crate::app::InputSourceId, TabPressState>,
- pub selection: Option<Selection>,
- pub selection_autoscroll: Option<SelectionAutoscroll>,
- pub context_menu: Option<ContextMenuState>,
// Notifications
pub update_available: Option<String>,
pub update_install_command: String,
@@ -1508,7 +816,6 @@ pub struct AppState {
pub config_diagnostic: Option<String>,
pub toast: Option<ToastNotification>,
pub pending_agent_notifications: std::collections::HashMap<PaneId, PendingAgentNotification>,
- pub copy_feedback: Option<CopyFeedback>,
/// Last reported focus state for the outer terminal hosting herdr.
/// None means unsupported or not yet reported, which preserves active-pane suppression.
pub outer_terminal_focus: Option<bool>,
@@ -1517,45 +824,20 @@ pub struct AppState {
pub prefix_mods: KeyModifiers,
/// Virtual terminal size (columns, rows) used when no client is attached.
pub(crate) headless_size: (u16, u16),
- pub default_sidebar_width: u16,
- pub sidebar_width: u16,
- pub sidebar_min_width: u16,
- pub sidebar_max_width: u16,
- pub mobile_width_threshold: u16,
- pub sidebar_width_source: SidebarWidthSource,
- pub sidebar_width_auto: bool,
- pub sidebar_collapsed: bool,
- pub sidebar_collapsed_mode: crate::config::SidebarCollapsedModeConfig,
- /// Ratio of sidebar height allocated to the workspaces section.
- pub sidebar_section_split: f32,
pub agent_panel_sort: AgentPanelSort,
- pub status_indicators: crate::config::StatusIndicatorStyle,
/// Transient session-wide projection override for the built-in Agents view.
pub agent_view_override: Option<crate::api::schema::AgentViewSetParams>,
pub sidebar_agents: crate::config::AgentsSidebarConfig,
pub sidebar_spaces: crate::config::SpacesSidebarConfig,
pub next_agent_state_change_seq: u64,
- /// Capture mouse input for Herdr's own mouse UI. When false, Herdr only
- /// captures mouse while the focused pane app requests mouse reporting.
- pub mouse_capture: bool,
- pub copy_on_select: bool,
- pub right_click_passthrough_modifiers: Option<KeyModifiers>,
- pub right_click_passthrough: Option<RightClickPassthroughGesture>,
- pub redraw_on_focus_gained: bool,
- pub mouse_scroll_lines: usize,
pub confirm_close: bool,
- pub prompt_new_tab_name: bool,
- pub prompt_new_workspace_name: bool,
pub pane_borders: bool,
pub pane_outer_borders: bool,
pub pane_scrollbars: bool,
pub pane_gaps: bool,
pub show_agent_labels_on_pane_borders: bool,
- pub hide_tab_bar_when_single_tab: bool,
- pub tab_bar_position: TabBarPositionConfig,
pub tab_bar_right: Vec<TabBarStatusSegment>,
pub tab_bar_right_separator: String,
- pub pane_history_persistence: bool,
/// Expose the focused pane's cursor anchor to the outer terminal even when
/// the pane requested `?25l`. See `[experimental] reveal_hidden_cursor_for_cjk_ime`.
pub reveal_hidden_cursor_for_cjk_ime: bool,
@@ -1565,20 +847,12 @@ pub struct AppState {
pub cjk_ime_agents: Vec<crate::detect::Agent>,
/// DECSCUSR shape parameter (1–6) for the IME anchor cursor.
pub cjk_ime_cursor_shape: u8,
- /// While prefix mode is active, switch the macOS host input source to an
- /// ASCII-capable layout so prefix commands register as ASCII even when a
- /// CJK IME is active. macOS only; a no-op elsewhere. See
- /// `[experimental] switch_ascii_input_source_in_prefix`.
- pub switch_ascii_input_source_in_prefix: bool,
pub kitty_graphics_enabled: bool,
pub default_shell: String,
pub shell_mode: crate::config::ShellModeConfig,
pub new_terminal_cwd: NewTerminalCwdConfig,
pub pane_scrollback_limit_bytes: usize,
- #[allow(dead_code)] // kept for backward compat; palette.accent is the source of truth
- pub accent: Color,
pub sound: SoundConfig,
- pub local_sound_playback: bool,
pub toast_config: ToastConfig,
pub keybinds: Keybinds,
/// UI color palette — all sidebar/UI colors centralized for theming.
@@ -1591,16 +865,11 @@ pub struct AppState {
pub host_terminal_appearance: Option<HostAppearance>,
/// True when the foreground host explicitly reported appearance via Mode 2031.
pub host_terminal_appearance_explicit: bool,
- /// Settings panel state.
- pub settings: SettingsState,
- /// Cached integration recommendations for onboarding/settings UI.
+ /// Cached integration recommendations and detection manifest summaries.
pub integration_recommendations: Vec<crate::integration::IntegrationRecommendation>,
- /// Cached detection manifest source/version summaries for runtime/API status.
pub agent_manifest_summaries: Vec<crate::detect::manifest::AgentManifestSummary>,
/// Cached remote detection manifest update diagnostics for runtime/API status.
pub agent_manifest_update_status: crate::detect::manifest_update::ManifestUpdateStatus,
- /// Result messages from the latest integration install action.
- pub integration_install_messages: Vec<String>,
/// Installed or linked plugins known to this running Herdr instance.
pub(crate) installed_plugins: InstalledPluginRegistry,
/// Pane ids opened through the plugin pane API.
@@ -1611,14 +880,10 @@ pub struct AppState {
pub(crate) plugin_command_logs: Vec<crate::api::schema::PluginCommandLogInfo>,
pub(crate) next_plugin_command_log_id: u64,
pub(crate) plugin_commands_in_flight: usize,
- /// Highlight state for the bottom-right global launcher menu.
- pub global_menu: MenuListState,
/// Resolved host terminal default colors for theming embedded panes.
pub host_terminal_theme: TerminalTheme,
/// Last known foreground host terminal cell size in pixels.
pub(crate) host_cell_size: crate::kitty_graphics::HostCellSize,
- /// Exact pixel provenance only while one confirmed SGR report is dispatched.
- pub(crate) host_mouse_pixels: Option<crate::input::mouse::HostPixels>,
/// Set when a persisted session snapshot would change.
pub session_dirty: bool,
/// Terminal runtimes that should be shut down by the app/runtime layer
@@ -1635,18 +900,6 @@ impl AppState {
self.pane_id_aliases.remove(&pane_id.raw());
}
- pub fn sound_enabled(&self) -> bool {
- self.sound.enabled
- }
-
- pub fn toast_delivery(&self) -> ToastDelivery {
- self.toast_config.delivery
- }
-
- pub fn agent_border_labels_enabled(&self) -> bool {
- self.show_agent_labels_on_pane_borders
- }
-
pub(crate) fn pane_exposes_host_cursor(
&self,
_ws_idx: usize,
@@ -1655,27 +908,17 @@ impl AppState {
true
}
- pub(crate) fn integration_updates_available(&self) -> bool {
- self.integration_recommendations
- .iter()
- .any(|item| item.state == crate::integration::IntegrationStatusKind::Outdated)
- }
-
pub(crate) fn refresh_agent_manifest_summaries(&mut self) {
self.agent_manifest_summaries = crate::detect::manifest::manifest_summaries();
}
- pub(crate) fn global_menu_attention_badge_visible(&self) -> bool {
- self.update_available.is_some() || self.integration_updates_available()
- }
-
- pub(crate) fn global_menu_item_has_badge(&self, item: &str) -> bool {
- (item == "update ready" && self.update_available.is_some())
- || (item == "settings" && self.integration_updates_available())
- }
-
- pub(crate) fn settings_section_has_badge(&self, section: SettingsSection) -> bool {
- section == SettingsSection::Integrations && self.integration_updates_available()
+ pub(crate) fn integration_updates_available(&self) -> bool {
+ self.integration_recommendations
+ .iter()
+ .any(|recommendation| {
+ recommendation.state == crate::integration::IntegrationStatusKind::Outdated
+ && recommendation.needs_install()
+ })
}
pub(crate) fn app_surface_pane_ids(&self) -> std::collections::HashSet<PaneId> {
@@ -1702,24 +945,9 @@ impl AppState {
&self,
terminal_runtimes: &crate::terminal::TerminalRuntimeRegistry,
) -> bool {
- self.mode == Mode::Terminal
- && self
- .active
- .and_then(|idx| self.focused_runtime_in_workspace(terminal_runtimes, idx))
- .is_some_and(crate::terminal::TerminalRuntime::mouse_reporting_enabled)
- }
-
- pub(crate) fn should_capture_host_mouse_from(
- &self,
- terminal_runtimes: &crate::terminal::TerminalRuntimeRegistry,
- ) -> bool {
- self.mouse_capture
- || self.popup_pane.is_some()
- || self.focused_pane_requests_mouse_capture_from(terminal_runtimes)
- }
-
- pub fn is_prefix_key(&self, key: &crate::input::TerminalKey) -> bool {
- crate::config::terminal_key_matches_combo(key, (self.prefix_code, self.prefix_mods))
+ self.active
+ .and_then(|idx| self.focused_runtime_in_workspace(terminal_runtimes, idx))
+ .is_some_and(crate::terminal::TerminalRuntime::mouse_reporting_enabled)
}
pub fn estimate_pane_size(&self) -> (u16, u16) {
@@ -1756,26 +984,6 @@ impl AppState {
terminal_runtimes.get(terminal_id)
}
- #[cfg(test)]
- pub(crate) fn runtime_for_pane<'a>(
- &'a self,
- terminal_runtimes: &'a crate::terminal::TerminalRuntimeRegistry,
- pane_id: crate::layout::PaneId,
- ) -> Option<&'a crate::terminal::TerminalRuntime> {
- self.workspaces.iter().find_map(|ws| {
- #[cfg(test)]
- if let Some(runtime) = ws.test_runtimes.get(&pane_id) {
- return Some(runtime);
- }
- #[cfg(test)]
- if let Some(runtime) = ws.tabs.iter().find_map(|tab| tab.runtimes.get(&pane_id)) {
- return Some(runtime);
- }
- let terminal_id = ws.terminal_id(pane_id)?;
- terminal_runtimes.get(terminal_id)
- })
- }
-
pub(crate) fn focused_runtime_in_workspace<'a>(
&'a self,
terminal_runtimes: &'a crate::terminal::TerminalRuntimeRegistry,
@@ -1786,6 +994,28 @@ impl AppState {
self.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, pane_id)
}
+ pub(crate) fn pane_visible_on_active_surface(
+ &self,
+ ws_idx: usize,
+ pane_id: crate::layout::PaneId,
+ ) -> bool {
+ if self.active != Some(ws_idx) {
+ return false;
+ }
+ let Some(tab) = self
+ .workspaces
+ .get(ws_idx)
+ .and_then(|workspace| workspace.active_tab())
+ else {
+ return false;
+ };
+ if tab.zoomed {
+ tab.layout.focused() == pane_id
+ } else {
+ tab.layout.pane_ids().contains(&pane_id)
+ }
+ }
+
pub fn is_active_pane(
&self,
ws_idx: usize,
@@ -1838,66 +1068,15 @@ impl AppState {
previous_pane_focus: None,
selected: 0,
mode: Mode::Navigate,
- confirm_close_workspace_id: None,
should_quit: false,
- detach_exits: false,
- detach_requested: false,
- request_new_workspace: false,
- request_new_tab: false,
- request_new_linked_worktree: None,
- request_open_existing_worktree: None,
- request_new_workspace_cwd: None,
- request_remove_linked_worktree: None,
- request_submit_worktree_create: false,
- request_submit_worktree_open: false,
- request_submit_worktree_remove: false,
- request_reload_config: false,
request_client_config_reload: false,
- request_clipboard_write: None,
- creating_new_tab: false,
- requested_new_tab_name: None,
- pending_workspace_create_cwd: None,
- rename_pane_target: None,
- worktree_create: None,
- worktree_open: None,
- worktree_remove: None,
worktree_directory: std::path::PathBuf::from("/tmp/herdr-worktrees"),
- collapsed_space_keys: std::collections::HashSet::new(),
- request_complete_onboarding: false,
- name_input: String::new(),
- name_input_replace_on_type: false,
- release_notes: None,
+ latest_release_notes: None,
product_announcement: None,
- keybind_help: KeybindHelpState::default(),
- navigator: NavigatorState::default(),
- copy_mode: None,
- workspace_scroll: 0,
- agent_panel_scroll: 0,
- tab_scroll: 0,
- tab_scroll_follow_active: true,
- mobile_switcher_scroll: 0,
view: ViewState {
- layout: ViewLayout::Desktop,
- sidebar_rect: Rect::default(),
- workspace_card_areas: Vec::new(),
- tab_bar_rect: Rect::default(),
- tab_hit_areas: Vec::new(),
- tab_scroll_left_hit_area: Rect::default(),
- tab_scroll_right_hit_area: Rect::default(),
- new_tab_hit_area: Rect::default(),
terminal_area: Rect::default(),
- mobile_header_rect: Rect::default(),
- mobile_menu_hit_area: Rect::default(),
- toast_hit_area: Rect::default(),
pane_infos: Vec::new(),
- split_borders: Vec::new(),
},
- drag: None,
- workspace_presses: std::collections::HashMap::new(),
- tab_presses: std::collections::HashMap::new(),
- selection: None,
- selection_autoscroll: None,
- context_menu: None,
update_available: None,
update_install_command: "herdr update".into(),
latest_release_notes_available: false,
@@ -1905,7 +1084,6 @@ impl AppState {
config_diagnostic: None,
toast: None,
pending_agent_notifications: std::collections::HashMap::new(),
- copy_feedback: None,
outer_terminal_focus: None,
prefix_code: KeyCode::Char('b'),
prefix_mods: KeyModifiers::CONTROL,
@@ -1913,57 +1091,32 @@ impl AppState {
crate::config::DEFAULT_HEADLESS_COLS,
crate::config::DEFAULT_HEADLESS_ROWS,
),
- default_sidebar_width: 26,
- sidebar_width: 26,
- sidebar_min_width: 18,
- sidebar_max_width: 36,
- mobile_width_threshold: crate::config::DEFAULT_MOBILE_WIDTH_THRESHOLD,
- sidebar_width_source: SidebarWidthSource::ConfigDefault,
- sidebar_width_auto: false,
- sidebar_collapsed: false,
- sidebar_collapsed_mode: crate::config::SidebarCollapsedModeConfig::Compact,
- sidebar_section_split: 0.5,
agent_panel_sort: AgentPanelSort::Spaces,
- status_indicators: crate::config::StatusIndicatorStyle::Dots,
agent_view_override: None,
sidebar_agents: crate::config::AgentsSidebarConfig::default(),
sidebar_spaces: crate::config::SpacesSidebarConfig::default(),
next_agent_state_change_seq: 0,
- mouse_capture: true,
- copy_on_select: true,
- right_click_passthrough_modifiers: None,
- right_click_passthrough: None,
- redraw_on_focus_gained: true,
- mouse_scroll_lines: crate::config::DEFAULT_MOUSE_SCROLL_LINES,
confirm_close: true,
- prompt_new_tab_name: true,
- prompt_new_workspace_name: false,
pane_borders: true,
pane_outer_borders: true,
pane_scrollbars: true,
pane_gaps: false,
show_agent_labels_on_pane_borders: false,
- hide_tab_bar_when_single_tab: false,
- tab_bar_position: TabBarPositionConfig::Top,
tab_bar_right: Vec::new(),
tab_bar_right_separator: " ".into(),
- pane_history_persistence: false,
reveal_hidden_cursor_for_cjk_ime: false,
cjk_ime_agent_filter_configured: false,
cjk_ime_agents: Vec::new(),
cjk_ime_cursor_shape: 2, // steady_block
- switch_ascii_input_source_in_prefix: false,
kitty_graphics_enabled: false,
default_shell: String::new(),
shell_mode: crate::config::ShellModeConfig::Auto,
new_terminal_cwd: NewTerminalCwdConfig::Follow,
pane_scrollback_limit_bytes: crate::config::DEFAULT_SCROLLBACK_LIMIT_BYTES,
- accent: Color::Cyan,
sound: SoundConfig {
enabled: false,
..SoundConfig::default()
},
- local_sound_playback: false,
toast_config: ToastConfig::default(),
keybinds: Keybinds::default(),
palette: Palette::catppuccin(),
@@ -1978,27 +1131,18 @@ impl AppState {
},
host_terminal_appearance: None,
host_terminal_appearance_explicit: false,
- settings: SettingsState {
- section: SettingsSection::Theme,
- list: SelectionListState::new(0),
- original_palette: None,
- original_theme: None,
- },
integration_recommendations: Vec::new(),
agent_manifest_summaries: Vec::new(),
agent_manifest_update_status:
crate::detect::manifest_update::ManifestUpdateStatus::default(),
- integration_install_messages: Vec::new(),
installed_plugins: std::collections::HashMap::new(),
plugin_panes: std::collections::HashMap::new(),
popup_pane: None,
plugin_command_logs: Vec::new(),
next_plugin_command_log_id: 1,
plugin_commands_in_flight: 0,
- global_menu: MenuListState::new(0),
host_terminal_theme: TerminalTheme::default(),
host_cell_size: crate::kitty_graphics::HostCellSize::default(),
- host_mouse_pixels: None,
session_dirty: false,
terminal_runtime_shutdowns: Vec::new(),
}
@@ -2063,52 +1207,12 @@ impl AppState {
self.pending_agent_notifications.is_empty(),
"empty app state must not keep pending agent notifications"
);
- assert!(
- self.copy_mode.is_none(),
- "empty app state must not keep copy mode"
- );
- assert!(
- self.rename_pane_target.is_none(),
- "empty app state must not keep rename pane target"
- );
- assert!(
- self.selection.is_none(),
- "empty app state must not keep text selection"
- );
- assert!(
- self.selection_autoscroll.is_none(),
- "empty app state must not keep selection autoscroll"
- );
if let Some(toast) = &self.toast {
assert!(
toast.target.is_none(),
"empty app state must not keep pane-targeted toast"
);
}
- assert!(
- self.right_click_passthrough.is_none(),
- "empty app state must not keep right-click passthrough gesture"
- );
- assert!(
- self.drag.is_none(),
- "empty app state must not keep drag state"
- );
- assert!(
- self.workspace_presses.is_empty(),
- "empty app state must not keep workspace press state"
- );
- assert!(
- self.tab_presses.is_empty(),
- "empty app state must not keep tab press state"
- );
- assert!(
- self.context_menu.is_none(),
- "empty app state must not keep context menu"
- );
- assert!(
- self.host_mouse_pixels.is_none(),
- "empty app state must not keep host mouse pixel provenance"
- );
return;
}
@@ -2183,25 +1287,6 @@ impl AppState {
workspace_id
);
};
- let assert_workspace_index = |ws_idx: usize, context: &str| {
- assert!(
- ws_idx < self.workspaces.len(),
- "{context} references workspace index {} out of bounds for {} workspaces",
- ws_idx,
- self.workspaces.len()
- );
- };
- let assert_tab_index = |ws_idx: usize, tab_idx: usize, context: &str| {
- assert_workspace_index(ws_idx, context);
- assert!(
- tab_idx < self.workspaces[ws_idx].tabs.len(),
- "{context} references tab index {} out of bounds for workspace {} with {} tabs",
- tab_idx,
- ws_idx,
- self.workspaces[ws_idx].tabs.len()
- );
- };
-
for (&raw, &pane_id) in &self.pane_id_aliases {
assert_live_pane(pane_id, &format!("raw pane alias {raw}"));
}
@@ -2243,96 +1328,6 @@ impl AppState {
for &pane_id in self.plugin_panes.keys() {
assert_live_pane(pane_id, "plugin pane record");
}
- if let Some(copy_mode) = &self.copy_mode {
- assert_live_pane(copy_mode.pane_id, "copy mode");
- }
- if let Some(pane_id) = self.rename_pane_target {
- assert_live_pane(pane_id, "rename pane target");
- }
- if let Some(selection) = &self.selection {
- assert_live_pane(selection.pane_id, "text selection");
- } else {
- assert!(
- self.selection_autoscroll.is_none(),
- "selection autoscroll must not remain without an active text selection"
- );
- }
- if let Some(gesture) = &self.right_click_passthrough {
- assert_live_pane(gesture.pane_info.id, "right-click passthrough gesture");
- }
- if let Some(drag) = &self.drag {
- match &drag.target {
- DragTarget::WorkspaceReorder {
- source_ws_idx,
- drop_target,
- ..
- } => {
- assert_workspace_index(*source_ws_idx, "workspace drag source");
- if let Some(WorkspaceDropTarget::Before(ws_idx)) = drop_target {
- assert_workspace_index(*ws_idx, "workspace drag target");
- }
- }
- DragTarget::TabReorder {
- ws_idx,
- source_tab_idx,
- insert_idx,
- ..
- } => {
- assert_tab_index(*ws_idx, *source_tab_idx, "tab drag source");
- if let Some(insert_idx) = insert_idx {
- assert!(
- *insert_idx <= self.workspaces[*ws_idx].tabs.len(),
- "tab drag insert index {} out of bounds for workspace {} with {} tabs",
- insert_idx,
- ws_idx,
- self.workspaces[*ws_idx].tabs.len()
- );
- }
- }
- DragTarget::PaneScrollbar { pane_id, .. } => {
- assert_live_pane(*pane_id, "pane scrollbar drag")
- }
- _ => {}
- }
- }
- for press in self.workspace_presses.values() {
- assert_workspace_index(press.ws_idx, "workspace press");
- }
- for press in self.tab_presses.values() {
- assert_tab_index(press.ws_idx, press.tab_idx, "tab press");
- }
- if let Some(menu) = &self.context_menu {
- match menu.kind {
- ContextMenuKind::Workspace { ws_idx }
- | ContextMenuKind::GitWorkspace { ws_idx, .. } => {
- assert_workspace_index(ws_idx, "context menu workspace")
- }
- ContextMenuKind::Tab { ws_idx, tab_idx } => {
- assert_tab_index(ws_idx, tab_idx, "context menu tab")
- }
- ContextMenuKind::Pane {
- ws_idx,
- tab_idx,
- pane_id,
- source_pane_id,
- ..
- } => {
- assert_tab_index(ws_idx, tab_idx, "context menu pane tab");
- assert!(
- self.workspaces[ws_idx].tabs[tab_idx]
- .panes
- .contains_key(&pane_id),
- "context menu pane references pane {:?} outside workspace {} tab {}",
- pane_id,
- ws_idx,
- tab_idx
- );
- if let Some(source_pane_id) = source_pane_id {
- assert_live_pane(source_pane_id, "context menu source pane");
- }
- }
- }
- }
}
pub fn insert_test_runtime(
@@ -2400,81 +1395,6 @@ mod tests {
state.assert_invariants_for_test();
}
- fn navigator_row_for_display(is_workspace: bool) -> NavigatorRow {
- NavigatorRow {
- target: NavigatorTarget::Workspace { ws_idx: 0 },
- depth: if is_workspace { 0 } else { 1 },
- label: String::new(),
- meta: String::new(),
- status: crate::detect::AgentState::Idle,
- seen: true,
- is_current: false,
- is_workspace,
- is_tab: false,
- expanded: true,
- search_text: String::new(),
- matched: true,
- }
- }
-
- #[test]
- fn navigator_display_lines_separate_workspace_groups() {
- let rows = vec![
- navigator_row_for_display(true),
- navigator_row_for_display(false),
- navigator_row_for_display(true),
- navigator_row_for_display(false),
- ];
- assert_eq!(
- navigator_display_lines(&rows),
- vec![
- NavigatorDisplayLine::Row(0),
- NavigatorDisplayLine::Row(1),
- NavigatorDisplayLine::Spacer,
- NavigatorDisplayLine::Row(2),
- NavigatorDisplayLine::Row(3),
- ]
- );
- }
-
- #[test]
- fn navigator_display_lines_have_no_leading_spacer() {
- let rows = vec![
- navigator_row_for_display(true),
- navigator_row_for_display(false),
- ];
- assert_eq!(
- navigator_display_lines(&rows),
- vec![NavigatorDisplayLine::Row(0), NavigatorDisplayLine::Row(1)]
- );
- assert!(navigator_display_lines(&[]).is_empty());
- }
-
- #[test]
- fn navigator_display_index_maps_row_to_line() {
- let rows = vec![
- navigator_row_for_display(true),
- navigator_row_for_display(false),
- navigator_row_for_display(true),
- ];
- let lines = navigator_display_lines(&rows);
- assert_eq!(navigator_display_index_of_row(&lines, 2), Some(3));
- assert_eq!(navigator_display_index_of_row(&lines, 9), None);
- }
-
- #[test]
- fn navigator_first_row_skips_spacer_lines() {
- let rows = vec![
- navigator_row_for_display(true),
- navigator_row_for_display(false),
- navigator_row_for_display(true),
- ];
- let lines = navigator_display_lines(&rows);
- // Line 2 is the spacer before the second workspace.
- assert_eq!(navigator_first_row_at_or_after(&lines, 2), Some(2));
- assert_eq!(navigator_first_row_at_or_after(&lines, 4), None);
- }
-
fn rgb_luminance(color: Color) -> f64 {
let Color::Rgb(r, g, b) = color else {
panic!("expected RGB color, got {color:?}");
@@ -2501,7 +1421,7 @@ mod tests {
#[test]
fn built_in_theme_names_resolve() {
- for name in THEME_NAMES {
+ for name in crate::config::THEME_NAMES {
assert!(
Palette::from_name(name).is_some(),
"theme should resolve: {name}"
@@ -2511,7 +1431,7 @@ mod tests {
#[test]
fn built_in_active_rows_remain_visible_with_matching_terminal_backgrounds() {
- for name in THEME_NAMES
+ for name in crate::config::THEME_NAMES
.iter()
.copied()
.filter(|name| *name != "terminal")
@@ -2533,7 +1453,7 @@ mod tests {
#[test]
fn built_in_selection_rows_stay_distinct_from_background_and_active_rows() {
- for name in THEME_NAMES
+ for name in crate::config::THEME_NAMES
.iter()
.copied()
.filter(|name| *name != "terminal")
@@ -2559,7 +1479,7 @@ mod tests {
#[test]
fn built_in_themes_leave_sidebar_background_unset() {
- for name in THEME_NAMES {
+ for name in crate::config::THEME_NAMES {
let palette = Palette::from_name(name).unwrap();
assert_eq!(
palette.sidebar_bg,
@@ -2620,70 +1540,4 @@ mod tests {
KeyModifiers::SHIFT,
));
}
-
- #[test]
- fn linked_worktree_context_menu_keeps_safe_close_and_explicit_remove() {
- let menu = ContextMenuState {
- kind: ContextMenuKind::GitWorkspace {
- ws_idx: 0,
- is_linked_worktree: true,
- has_worktree_children: false,
- collapsed: false,
- },
- x: 0,
- y: 0,
- list: MenuListState::new(0),
- };
-
- assert_eq!(
- menu.items(),
- &["Rename", "Close", "Delete worktree checkout..."]
- );
- }
-
- #[test]
- fn git_workspace_context_menu_keeps_remove_for_managed_worktrees_only() {
- let menu = ContextMenuState {
- kind: ContextMenuKind::GitWorkspace {
- ws_idx: 0,
- is_linked_worktree: false,
- has_worktree_children: false,
- collapsed: false,
- },
- x: 0,
- y: 0,
- list: MenuListState::new(0),
- };
-
- assert_eq!(
- menu.items(),
- &["Rename", "Close", "New worktree", "Open worktree..."]
- );
- }
-
- #[test]
- fn parent_worktree_context_menu_uses_repo_actions() {
- let menu = ContextMenuState {
- kind: ContextMenuKind::GitWorkspace {
- ws_idx: 0,
- is_linked_worktree: false,
- has_worktree_children: true,
- collapsed: false,
- },
- x: 0,
- y: 0,
- list: MenuListState::new(0),
- };
-
- assert_eq!(
- menu.items(),
- &[
- "Rename",
- "Close group",
- "New worktree",
- "Open worktree...",
- "Collapse"
- ]
- );
- }
}