Files
herdr/src/client/shell/state.rs
T

1234 lines
40 KiB
Rust

use super::*;
pub(super) const MIN_TAB_WIDTH: u16 = 8;
pub(super) const NEW_TAB_WIDTH: u16 = 3;
pub(super) const WORKSPACE_HEADER_ROWS: u16 = 2;
pub(crate) struct ClientShellConfig {
pub(super) sidebar_width: u16,
pub(super) sidebar_min_width: u16,
pub(super) sidebar_max_width: u16,
pub(super) sidebar_start_collapsed: bool,
pub(super) sidebar_collapsed_mode: SidebarCollapsedModeConfig,
pub(super) mobile_width_threshold: u16,
pub(super) tab_bar_position: TabBarPositionConfig,
pub(super) hide_tab_bar_when_single_tab: bool,
pub(super) spaces: SpacesSidebarConfig,
pub(super) agents: crate::config::AgentsSidebarConfig,
pub(super) agent_panel_sort: crate::config::AgentPanelSortConfig,
pub(super) status_indicators: crate::config::StatusIndicatorStyle,
pub(super) sound_enabled: bool,
pub(super) toast_delivery: crate::config::ToastDelivery,
pub(super) toast_delay_seconds: u64,
pub(super) toast_position: crate::config::ToastHerdrPosition,
pub(super) copy_on_select: bool,
pub(super) clipboard_toast_enabled: bool,
pub(super) clipboard_toast_position: crate::config::ToastClipboardPosition,
pub(super) theme_name: String,
pub(super) theme_runtime: crate::app::state::ThemeRuntimeConfig,
pub(super) palette: Palette,
pub(super) keybinds: LiveKeybindConfig,
pub(super) prompt_new_tab_name: bool,
pub(super) prompt_new_workspace_name: bool,
pub(super) confirm_close: bool,
pub(super) mouse_capture: bool,
pub(super) mouse_scroll_lines: usize,
pub(super) right_click_passthrough_modifiers: Option<crossterm::event::KeyModifiers>,
pub(super) worktree_directory: std::path::PathBuf,
pub(super) preferences_path: Option<std::path::PathBuf>,
pub(super) preferences: preferences::ClientChromePreferences,
pub(super) startup_config_diagnostic: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct ClientShellLayout {
pub sidebar: Rect,
pub tab_bar: Rect,
pub mobile_header: Rect,
pub pane_surface: Rect,
}
#[derive(Default)]
pub(super) struct ShellHitMap {
pub(super) workspaces: Vec<WorkspaceHit>,
pub(super) workspace_body: Rect,
pub(super) workspace_scrollbar: Rect,
pub(super) workspace_scroll_metrics: Option<crate::pane::ScrollMetrics>,
pub(super) workspace_max_scroll: usize,
pub(super) tabs: Vec<(Rect, String)>,
pub(super) panes: Vec<PaneHit>,
pub(super) popup: Option<PaneHit>,
pub(super) pane_splits: Vec<PaneSplitHit>,
pub(super) agents: Vec<(Rect, String)>,
pub(super) agent_body: Rect,
pub(super) agent_scrollbar: Rect,
pub(super) agent_scroll_metrics: Option<crate::pane::ScrollMetrics>,
pub(super) agent_max_scroll: usize,
pub(super) agent_sort_toggle: Rect,
pub(super) sidebar_divider: Rect,
pub(super) sidebar_section_divider: Rect,
pub(super) sidebar_toggle: Rect,
pub(super) new_workspace: Rect,
pub(super) new_tab: Rect,
pub(super) tab_scroll_left: Rect,
pub(super) tab_scroll_right: Rect,
pub(super) global_launcher: Rect,
pub(super) notification_toast: Rect,
pub(super) global_menu_rows: Vec<(Rect, usize)>,
pub(super) context_menu_rows: Vec<(Rect, usize)>,
pub(super) overlay_primary: Rect,
pub(super) overlay_clear: Rect,
pub(super) overlay_cancel: Rect,
pub(super) navigator_popup: Rect,
pub(super) navigator_search: Rect,
pub(super) navigator_rows: Vec<(Rect, usize)>,
pub(super) worktree_search: Rect,
pub(super) worktree_rows: Vec<(Rect, usize)>,
pub(super) help_popup: Rect,
pub(super) help_scrollbar: Rect,
pub(super) help_scroll_metrics: Option<crate::pane::ScrollMetrics>,
pub(super) help_max_scroll: usize,
pub(super) settings_popup: Rect,
pub(super) settings_tabs: Vec<(Rect, ClientSettingsSection)>,
pub(super) settings_choices: Vec<(Rect, usize)>,
}
#[derive(Clone)]
pub(super) struct PaneHit {
pub(super) rect: Rect,
pub(super) inner_rect: Rect,
pub(super) scrollbar_rect: Option<Rect>,
pub(super) scroll: Option<crate::pane::ScrollMetrics>,
pub(super) pane_id: String,
pub(super) popup: bool,
pub(super) mouse_reporting: bool,
pub(super) sgr_pixel_mouse: bool,
pub(super) pixel_width: u32,
pub(super) pixel_height: u32,
}
#[derive(Clone)]
pub(super) struct PaneSplitHit {
pub(super) direction: crate::protocol::PaneSurfaceSplitDirection,
pub(super) pos: u16,
pub(super) area: Rect,
pub(super) hit_rect: Rect,
pub(super) path: Vec<bool>,
pub(super) topology_signature: u64,
}
pub(super) struct ClientPaneMouseGesture {
pub(super) hit: PaneHit,
pub(super) button: crossterm::event::MouseButton,
pub(super) stripped_modifiers: crossterm::event::KeyModifiers,
}
pub(super) struct ClientWorkspacePress {
pub(super) workspace_id: String,
pub(super) start_column: u16,
pub(super) start_row: u16,
}
pub(super) struct ClientTabPress {
pub(super) tab_id: String,
pub(super) workspace_id: String,
pub(super) start_column: u16,
pub(super) start_row: u16,
}
pub(super) enum ClientChromeDrag {
SidebarWidth,
SidebarSection,
WorkspaceScrollbar {
grab_row_offset: u16,
},
AgentScrollbar {
grab_row_offset: u16,
},
HelpScrollbar {
grab_row_offset: u16,
},
Tab {
tab_id: String,
workspace_id: String,
insert_index: Option<usize>,
},
Workspace {
source_workspace_id: String,
target: Option<(Option<String>, u16)>,
},
PaneSplit {
hit: PaneSplitHit,
tab_id: String,
grab_offset: i32,
last_sent_ratio: Option<f32>,
last_sent_at: Option<std::time::Instant>,
},
PaneScrollbar {
hit: PaneHit,
grab_row_offset: u16,
last_sent_offset: Option<usize>,
last_sent_at: Option<std::time::Instant>,
},
}
pub(super) struct WorkspaceHit {
pub(super) rect: Rect,
pub(super) workspace_id: String,
pub(super) indented: bool,
pub(super) group_toggle: Option<(Rect, String)>,
}
#[derive(Debug)]
pub(crate) enum ClientShellAction {
Endpoint {
boot_id: String,
request: Box<crate::api::schema::Request>,
},
ClipboardWrite(Vec<u8>),
Keybind(crate::input::KeybindAction),
}
#[derive(Default)]
pub(crate) struct ClientShellInput {
pub detach: bool,
pub repaint: bool,
pub resize: bool,
pub query_host_appearance: bool,
pub requests: Vec<ClientMessage>,
pub actions: Vec<ClientShellAction>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ClientShellMode {
Terminal,
Prefix,
Navigate,
Resize,
Copy,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ClientShellOverlayKind {
Rename,
ConfirmClose,
Help,
Navigator,
WorktreeCreate,
WorktreeOpen,
WorktreeRemove,
ContextMenu,
GlobalMenu,
Settings,
}
#[derive(Debug)]
pub(super) enum ClientRenameTarget {
NewWorkspace {
cwd: Option<String>,
suggested_name: String,
},
Workspace {
workspace_id: String,
},
NewTab {
workspace_id: String,
default_name: String,
},
Tab {
tab_id: String,
auto_name: bool,
original_name: String,
},
Pane {
pane_id: String,
},
}
#[derive(Debug)]
pub(super) struct ClientRenameOverlay {
pub(super) title: &'static str,
pub(super) input: String,
pub(super) replace_on_type: bool,
pub(super) target: ClientRenameTarget,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ClientNavigatorFilter {
Blocked,
Working,
Idle,
Done,
}
#[derive(Clone, Debug)]
pub(super) enum ClientNavigatorTarget {
Workspace(String),
Tab(String),
Pane(String),
}
#[derive(Clone, Debug)]
pub(super) struct ClientNavigatorRow {
pub(super) depth: u8,
pub(super) label: String,
pub(super) meta: String,
pub(super) status: crate::api::schema::AgentStatus,
pub(super) current: bool,
pub(super) target: ClientNavigatorTarget,
}
#[derive(Debug)]
pub(super) struct ClientNavigatorOverlay {
pub(super) query: String,
pub(super) search_focused: bool,
pub(super) selected: usize,
pub(super) scroll: usize,
pub(super) filter: Option<ClientNavigatorFilter>,
pub(super) expanded_workspaces: HashSet<String>,
}
#[derive(Debug)]
pub(super) struct ClientHelpOverlay {
pub(super) query: String,
pub(super) search_focused: bool,
pub(super) scroll: usize,
}
#[derive(Debug)]
pub(super) struct ClientGlobalMenuOverlay {
pub(super) highlighted: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ClientSettingsSection {
Theme,
Indicators,
Sound,
Toast,
Integrations,
}
impl ClientSettingsSection {
pub(super) const ALL: &[Self] = &[
Self::Theme,
Self::Indicators,
Self::Sound,
Self::Toast,
Self::Integrations,
];
pub(super) fn label(self) -> &'static str {
match self {
Self::Theme => "theme",
Self::Indicators => "indicators",
Self::Sound => "sound",
Self::Toast => "toasts",
Self::Integrations => "integrations",
}
}
}
#[derive(Debug)]
pub(super) struct ClientSettingsOverlay {
pub(super) section: ClientSettingsSection,
pub(super) selected: usize,
pub(super) original_theme_name: String,
pub(super) original_palette: Palette,
pub(super) integrations: Vec<crate::api::schema::IntegrationInfo>,
pub(super) integration_messages: Vec<String>,
pub(super) loading_integrations: bool,
pub(super) installing_integrations: bool,
}
#[derive(Debug)]
pub(super) struct ClientWorktreeCreateOverlay {
pub(super) source_workspace_id: String,
pub(super) repo_name: String,
pub(super) branch: String,
pub(super) checkout_path: String,
pub(super) replace_on_type: bool,
pub(super) error: Option<String>,
pub(super) creating: bool,
}
#[derive(Debug, Clone)]
pub(super) struct ClientWorktreeOpenEntry {
pub(super) path: String,
pub(super) branch: Option<String>,
pub(super) is_linked_worktree: bool,
pub(super) is_detached: bool,
pub(super) open_workspace_id: Option<String>,
pub(super) label: String,
}
impl ClientWorktreeOpenEntry {
pub(super) fn status_label(&self) -> &'static str {
if self.open_workspace_id.is_some() {
"open"
} else if self.branch.is_some() {
""
} else if self.is_detached && self.is_linked_worktree {
"detached"
} else {
"root"
}
}
pub(super) fn matches_query(&self, query: &str) -> bool {
let query = query.trim().to_lowercase();
query.is_empty()
|| format!(
"{} {} {} {}",
self.label,
self.branch.as_deref().unwrap_or_default(),
self.path,
self.status_label()
)
.to_lowercase()
.contains(&query)
}
}
#[derive(Debug)]
pub(super) struct ClientWorktreeOpenOverlay {
pub(super) source_workspace_id: String,
pub(super) entries: Vec<ClientWorktreeOpenEntry>,
pub(super) selected: usize,
pub(super) query: String,
pub(super) search_focused: bool,
pub(super) error: Option<String>,
pub(super) opening: bool,
}
impl ClientWorktreeOpenOverlay {
pub(super) fn filtered_indices(&self) -> Vec<usize> {
self.entries
.iter()
.enumerate()
.filter_map(|(index, entry)| entry.matches_query(&self.query).then_some(index))
.collect()
}
pub(super) fn selected_entry_index(&self) -> Option<usize> {
let filtered = self.filtered_indices();
filtered
.contains(&self.selected)
.then_some(self.selected)
.or_else(|| filtered.first().copied())
}
}
#[derive(Debug)]
pub(super) struct ClientWorktreeRemoveOverlay {
pub(super) workspace_id: String,
pub(super) path: String,
pub(super) error: Option<String>,
pub(super) removing: bool,
pub(super) force_confirmation: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ClientContextMenuAction {
Rename,
Close,
NewWorktree,
OpenWorktree,
RemoveWorktree,
ToggleGroup,
NewTab,
RenamePane,
ClearPaneName,
SwapWithFocusedPane,
SplitRight,
SplitDown,
Zoom,
ToggleRightClickPassthrough,
ClosePane,
}
#[derive(Debug)]
pub(super) enum ClientContextMenuTarget {
Workspace {
workspace_id: String,
is_git: bool,
is_linked_worktree: bool,
has_worktree_children: bool,
collapsed: bool,
},
Tab {
tab_id: String,
workspace_id: String,
},
Pane {
pane_id: String,
workspace_id: String,
source_pane_id: Option<String>,
has_manual_label: bool,
right_click_passthrough: bool,
},
}
#[derive(Debug)]
pub(super) struct ClientContextMenuOverlay {
pub(super) target: ClientContextMenuTarget,
pub(super) x: u16,
pub(super) y: u16,
pub(super) highlighted: usize,
}
pub(super) struct ClientContextMenuItem {
pub(super) label: &'static str,
pub(super) action: ClientContextMenuAction,
}
#[derive(Debug)]
pub(super) struct ClientConfirmCloseOverlay {
pub(super) workspace_id: String,
pub(super) title: String,
pub(super) detail: String,
}
#[derive(Debug)]
pub(super) enum ClientShellOverlay {
Rename(ClientRenameOverlay),
ConfirmClose(ClientConfirmCloseOverlay),
Help(ClientHelpOverlay),
Navigator(ClientNavigatorOverlay),
WorktreeCreate(ClientWorktreeCreateOverlay),
WorktreeOpen(ClientWorktreeOpenOverlay),
WorktreeRemove(ClientWorktreeRemoveOverlay),
ContextMenu(ClientContextMenuOverlay),
GlobalMenu(ClientGlobalMenuOverlay),
Settings(ClientSettingsOverlay),
}
impl ClientShellOverlay {
pub(super) fn kind(&self) -> ClientShellOverlayKind {
match self {
Self::Rename(_) => ClientShellOverlayKind::Rename,
Self::ConfirmClose(_) => ClientShellOverlayKind::ConfirmClose,
Self::Help(_) => ClientShellOverlayKind::Help,
Self::Navigator(_) => ClientShellOverlayKind::Navigator,
Self::WorktreeCreate(_) => ClientShellOverlayKind::WorktreeCreate,
Self::WorktreeOpen(_) => ClientShellOverlayKind::WorktreeOpen,
Self::WorktreeRemove(_) => ClientShellOverlayKind::WorktreeRemove,
Self::ContextMenu(_) => ClientShellOverlayKind::ContextMenu,
Self::GlobalMenu(_) => ClientShellOverlayKind::GlobalMenu,
Self::Settings(_) => ClientShellOverlayKind::Settings,
}
}
}
#[derive(Debug)]
pub(super) enum PendingEndpointKind {
Generic,
PopupCommand,
ReloadConfig,
IntegrationList,
IntegrationInstall,
PrepareWorktreeCreate {
workspace_id: String,
},
PrepareWorktreeOpen {
workspace_id: String,
},
PrepareWorktreeRemove {
workspace_id: String,
},
WorktreeCreate,
WorktreeOpen,
WorktreeRemove {
forced: bool,
},
SelectionCopy,
PaneScroll {
pane_id: String,
serial: u64,
},
WordSelection {
pane_id: String,
absolute_row: u32,
col: u16,
generation: u64,
},
CopyMotion {
pane_id: String,
origin: crate::api::schema::PaneTextPoint,
session_generation: u64,
},
CopySearch {
pane_id: String,
origin: crate::api::schema::PaneTextPoint,
query: String,
direction: crate::api::schema::PaneCopySearchDirection,
repeat: bool,
generation: u64,
session_generation: u64,
},
}
pub(super) struct PendingEndpointRequest {
pub(super) boot_id: String,
pub(super) confirmation_workspace_id: Option<String>,
pub(super) kind: PendingEndpointKind,
}
pub(crate) struct ClientShellEndpointError {
pub code: Option<String>,
pub message: String,
}
pub(crate) enum ClientShellNotificationEffect {
Sound {
sound: crate::sound::Sound,
agent: Option<String>,
},
Terminal {
title: String,
body: Option<String>,
},
System {
title: String,
body: Option<String>,
},
}
pub(super) struct ClientPendingNotification {
pub(super) event: SemanticNotification,
pub(super) deadline: std::time::Instant,
pub(super) validate_state: bool,
}
pub(super) struct ClientVisibleNotification {
pub(super) event: SemanticNotification,
pub(super) deadline: std::time::Instant,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum ClientInputTarget {
Pane(String),
Popup(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct ClientInputContext {
pub(super) mode: ClientShellMode,
pub(super) overlay: Option<ClientShellOverlayKind>,
pub(super) popup_terminal_id: Option<String>,
pub(super) popup_pending: bool,
pub(super) retained_selection: bool,
}
type ClientInputLeases = crate::input::InputLeaseTable<u8, ClientInputContext, ClientInputTarget>;
#[derive(Clone, Debug)]
pub(super) struct ClientPaneClick {
pub(super) pane_id: String,
pub(super) viewport_row: u16,
pub(super) col: u16,
pub(super) at: std::time::Instant,
}
impl ClientPaneClick {
pub(super) fn is_double_click_for(&self, next: &Self) -> bool {
self.pane_id == next.pane_id
&& next.at.duration_since(self.at) <= std::time::Duration::from_millis(350)
&& self.viewport_row.abs_diff(next.viewport_row) <= 1
&& self.col.abs_diff(next.col) <= 1
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ClientSelectionAutoscrollDirection {
Up,
Down,
}
#[derive(Clone, Debug)]
pub(super) struct ClientSelectionAutoscroll {
pub(super) pane_id: String,
pub(super) direction: ClientSelectionAutoscrollDirection,
pub(super) last_mouse_column: u16,
pub(super) last_mouse_row: u16,
pub(super) inner_rect: Rect,
pub(super) offset_from_bottom: usize,
pub(super) max_offset_from_bottom: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ClientCopySelection {
Character {
anchor: crate::api::schema::PaneTextPoint,
},
Linewise {
anchor_row: u32,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct ClientCopySearchPrompt {
pub(super) direction: crate::api::schema::PaneCopySearchDirection,
pub(super) query: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum ClientCopyOperation {
Motion(crate::api::schema::PaneCopyMotion),
Search {
query: String,
direction: crate::api::schema::PaneCopySearchDirection,
repeat: bool,
},
}
pub(super) struct ClientCopySearchResult {
pub(super) content_revision: u64,
pub(super) matches: Vec<crate::api::schema::PaneTextRange>,
pub(super) total: u64,
pub(super) current: Option<usize>,
pub(super) current_global: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct ClientCopyModeState {
pub(super) pane_id: String,
pub(super) content_revision: u64,
pub(super) geometry: (u16, u16),
pub(super) cursor: crate::api::schema::PaneTextPoint,
pub(super) offset_from_bottom: usize,
pub(super) max_offset_from_bottom: usize,
pub(super) entry_offset_from_bottom: usize,
pub(super) selection: Option<ClientCopySelection>,
pub(super) search_prompt: Option<ClientCopySearchPrompt>,
pub(super) search_query: String,
pub(super) search_direction: Option<crate::api::schema::PaneCopySearchDirection>,
pub(super) search_matches: Vec<crate::api::schema::PaneTextRange>,
pub(super) search_total: u64,
pub(super) search_current: Option<usize>,
pub(super) search_current_global: Option<u64>,
pub(super) search_generation: u64,
pub(super) copy_after_search: bool,
}
pub(crate) struct ClientShellState {
pub(super) config: ClientShellConfig,
pub(super) snapshot: Option<Box<ClientShellSnapshot>>,
pub(super) pane_surface: Option<PaneSurfaceFrame>,
pub(super) popup_terminal_id: Option<String>,
pub(super) sidebar_collapsed: bool,
pub(super) sidebar_collapsed_manual: bool,
pub(super) sidebar_width: u16,
pub(super) sidebar_width_manual: bool,
pub(super) sidebar_section_split: f32,
pub(super) sidebar_section_split_manual: bool,
pub(super) agent_panel_sort_manual: bool,
pub(super) last_sidebar_divider_click: Option<std::time::Instant>,
pub(super) chrome_drag: Option<ClientChromeDrag>,
pub(super) workspace_press: Option<ClientWorkspacePress>,
pub(super) tab_press: Option<ClientTabPress>,
pub(super) collapsed_groups: HashSet<String>,
pub(super) workspace_scroll: usize,
pub(super) agent_scroll: usize,
pub(super) tab_scroll: usize,
pub(super) reveal_focused_tab: bool,
pub(super) last_tab_bar_width: Option<u16>,
pub(super) hits: ShellHitMap,
pub(super) mode: ClientShellMode,
pub(super) navigate_workspace_id: Option<String>,
pub(super) overlay: Option<ClientShellOverlay>,
pub(super) previous_pane_id: Option<String>,
pub(super) pane_mouse_gesture: Option<ClientPaneMouseGesture>,
pub(super) selection: Option<crate::selection::Selection<String>>,
pub(super) last_pane_click: Option<ClientPaneClick>,
pub(super) selection_autoscroll: Option<ClientSelectionAutoscroll>,
pub(super) selection_autoscroll_deadline: Option<std::time::Instant>,
pub(super) selection_highlight_clear_deadline: Option<std::time::Instant>,
pub(super) pending_word_selection: Option<u64>,
pub(super) word_selection_generation: u64,
pub(super) copy_mode: Option<ClientCopyModeState>,
pub(super) copy_session_generation: u64,
pub(super) copy_operation_in_flight: bool,
pub(super) copy_operation_queue: VecDeque<ClientCopyOperation>,
pub(super) copy_input_queue: VecDeque<crate::input::TerminalKey>,
pub(super) next_scroll_serial: u64,
pub(super) pane_scroll_in_flight: HashMap<String, u64>,
pub(super) pane_scroll_queued: HashMap<String, usize>,
pub(super) pane_scroll_targets: HashMap<String, usize>,
pub(super) copy_feedback: Option<crate::app::state::CopyFeedback>,
pub(super) copy_feedback_deadline: Option<std::time::Instant>,
pub(super) host_mouse_pixels: Option<crate::input::mouse::HostPixels>,
pub(super) input_leases: ClientInputLeases,
pub(super) popup_pending: bool,
pub(super) popup_pending_deadline: Option<std::time::Instant>,
pub(super) next_request_id: u64,
pub(super) pending_requests: HashMap<String, PendingEndpointRequest>,
pub(super) pending_integration_installs: usize,
pub(super) pending_notifications: Vec<ClientPendingNotification>,
pub(super) visible_notification: Option<ClientVisibleNotification>,
pub(super) outer_focused: Option<bool>,
pub(super) local_config_diagnostic: Option<String>,
pub(super) config_diagnostic: Option<String>,
pub(super) endpoint_error: Option<String>,
}
#[derive(Clone, Copy)]
pub(super) struct WorkspaceEntry {
pub(super) index: usize,
pub(super) indented: bool,
pub(super) last_child: bool,
}
impl ClientShellState {
pub(crate) fn new(mut config: ClientShellConfig) -> Self {
let preferences = config.preferences;
let local_config_diagnostic = config.startup_config_diagnostic.take();
let sidebar_collapsed = preferences
.sidebar_collapsed
.unwrap_or(config.sidebar_start_collapsed);
let (min_width, max_width) = crate::config::validated_sidebar_bounds(
config.sidebar_min_width,
config.sidebar_max_width,
)
.unwrap_or((18, 36));
let sidebar_width = preferences
.sidebar_width
.unwrap_or(config.sidebar_width)
.clamp(min_width, max_width);
let sidebar_section_split = preferences
.sidebar_section_split
.filter(|split| split.is_finite())
.map(|split| split.clamp(0.1, 0.9))
.unwrap_or(0.5);
if let Some(sort) = preferences.agent_panel_sort {
config.agent_panel_sort = sort;
}
Self {
config,
snapshot: None,
pane_surface: None,
popup_terminal_id: None,
sidebar_collapsed,
sidebar_collapsed_manual: preferences.sidebar_collapsed.is_some(),
sidebar_width,
sidebar_width_manual: preferences.sidebar_width.is_some(),
sidebar_section_split,
sidebar_section_split_manual: preferences.sidebar_section_split.is_some(),
agent_panel_sort_manual: preferences.agent_panel_sort.is_some(),
last_sidebar_divider_click: None,
chrome_drag: None,
workspace_press: None,
tab_press: None,
collapsed_groups: HashSet::new(),
workspace_scroll: 0,
agent_scroll: 0,
tab_scroll: 0,
reveal_focused_tab: true,
last_tab_bar_width: None,
hits: ShellHitMap::default(),
mode: ClientShellMode::Terminal,
navigate_workspace_id: None,
overlay: None,
previous_pane_id: None,
pane_mouse_gesture: None,
selection: None,
last_pane_click: None,
selection_autoscroll: None,
selection_autoscroll_deadline: None,
selection_highlight_clear_deadline: None,
pending_word_selection: None,
word_selection_generation: 0,
copy_mode: None,
copy_session_generation: 0,
copy_operation_in_flight: false,
copy_operation_queue: VecDeque::new(),
copy_input_queue: VecDeque::new(),
next_scroll_serial: 0,
pane_scroll_in_flight: HashMap::new(),
pane_scroll_queued: HashMap::new(),
pane_scroll_targets: HashMap::new(),
copy_feedback: None,
copy_feedback_deadline: None,
host_mouse_pixels: None,
input_leases: ClientInputLeases::default(),
popup_pending: false,
popup_pending_deadline: None,
next_request_id: 1,
pending_requests: HashMap::new(),
pending_integration_installs: 0,
pending_notifications: Vec::new(),
visible_notification: None,
outer_focused: None,
config_diagnostic: local_config_diagnostic.clone(),
local_config_diagnostic,
endpoint_error: None,
}
}
fn focused_tab_count(&self) -> usize {
let Some(snapshot) = self.snapshot.as_deref() else {
return 0;
};
snapshot
.tabs
.iter()
.filter(|tab| {
Some(tab.workspace_id.as_str()) == snapshot.focused_workspace_id.as_deref()
})
.count()
}
pub(super) fn layout(&self, cols: u16, rows: u16) -> ClientShellLayout {
self.config.layout(
cols,
rows,
self.sidebar_collapsed,
self.focused_tab_count(),
self.sidebar_width,
)
}
pub(crate) fn surface_size(&self, cols: u16, rows: u16) -> ClientSurfaceSize {
let surface = self.layout(cols, rows).pane_surface;
ClientSurfaceSize {
cols: surface.width.max(1),
rows: surface.height.max(1),
}
}
pub(crate) fn set_snapshot(&mut self, snapshot: Box<ClientShellSnapshot>) {
self.config_diagnostic = super::config::merged_config_diagnostic(
self.local_config_diagnostic.as_deref(),
snapshot.config_diagnostic.as_deref(),
);
if self
.pane_surface
.as_ref()
.is_none_or(|surface| surface.projection_revision != snapshot.revision)
{
self.hits = ShellHitMap::default();
}
if self
.snapshot
.as_ref()
.is_some_and(|current| current.boot_id != snapshot.boot_id)
{
self.pane_surface = None;
self.popup_terminal_id = None;
self.chrome_drag = None;
self.workspace_press = None;
self.tab_press = None;
self.collapsed_groups.clear();
self.workspace_scroll = 0;
self.agent_scroll = 0;
self.tab_scroll = 0;
self.reveal_focused_tab = true;
self.last_tab_bar_width = None;
self.pending_requests.clear();
self.pane_scroll_in_flight.clear();
self.pane_scroll_queued.clear();
self.pane_scroll_targets.clear();
self.popup_pending = false;
self.popup_pending_deadline = None;
self.pending_integration_installs = 0;
self.pending_notifications.clear();
self.visible_notification = None;
self.endpoint_error = None;
self.navigate_workspace_id = None;
self.overlay = None;
self.previous_pane_id = None;
self.pane_mouse_gesture = None;
self.selection = None;
self.last_pane_click = None;
self.selection_autoscroll = None;
self.selection_autoscroll_deadline = None;
self.selection_highlight_clear_deadline = None;
self.pending_word_selection = None;
self.copy_mode = None;
self.reset_copy_pipeline();
self.copy_feedback = None;
self.copy_feedback_deadline = None;
self.host_mouse_pixels = None;
} else if let Some(previous) = self
.snapshot
.as_deref()
.and_then(|current| current.focused_pane_id.as_ref())
.filter(|previous| Some(previous.as_str()) != snapshot.focused_pane_id.as_deref())
{
self.previous_pane_id = Some(previous.clone());
}
let tab_layout_changed = self.snapshot.as_deref().is_none_or(|current| {
current.tabs.len() != snapshot.tabs.len()
|| current
.tabs
.iter()
.zip(&snapshot.tabs)
.any(|(left, right)| {
left.tab_id != right.tab_id
|| left.workspace_id != right.workspace_id
|| left.label != right.label
|| left.zoomed != right.zoomed
})
|| render::tab_bar_status_width(current) != render::tab_bar_status_width(&snapshot)
});
if tab_layout_changed
|| self
.snapshot
.as_deref()
.and_then(|current| current.focused_tab_id.as_deref())
!= snapshot.focused_tab_id.as_deref()
{
self.reveal_focused_tab = true;
}
if self.selection.as_ref().is_some_and(|selection| {
snapshot.focused_pane_id.as_deref() != Some(selection.pane_id.as_str())
|| !snapshot
.panes
.iter()
.any(|pane| pane.pane_id == selection.pane_id)
}) {
self.selection = None;
self.selection_autoscroll = None;
self.selection_autoscroll_deadline = None;
self.selection_highlight_clear_deadline = None;
self.pending_word_selection = None;
self.last_pane_click = None;
}
if let Some(copy_pane_id) = self
.copy_mode
.as_ref()
.map(|copy_mode| copy_mode.pane_id.clone())
{
let pane_exists = snapshot
.panes
.iter()
.any(|pane| pane.pane_id == copy_pane_id);
let pane_focused = snapshot.focused_pane_id.as_deref() == Some(copy_pane_id.as_str());
if !pane_exists {
self.copy_mode = None;
self.reset_copy_pipeline();
if self
.selection
.as_ref()
.is_some_and(|selection| selection.pane_id == copy_pane_id)
{
self.selection = None;
self.stop_selection_autoscroll();
self.selection_highlight_clear_deadline = None;
}
if self.mode == ClientShellMode::Copy {
self.mode = ClientShellMode::Terminal;
}
} else if pane_focused {
if self.mode == ClientShellMode::Terminal {
self.mode = ClientShellMode::Copy;
}
if self.selection.is_none() {
self.sync_copy_selection();
}
} else {
if self
.selection
.as_ref()
.is_some_and(|selection| selection.pane_id == copy_pane_id)
{
self.selection = None;
self.stop_selection_autoscroll();
self.selection_highlight_clear_deadline = None;
}
if self.mode == ClientShellMode::Copy {
self.mode = ClientShellMode::Terminal;
}
}
}
if self.mode == ClientShellMode::Navigate
&& self.navigate_workspace_id.as_ref().is_none_or(|selected| {
!snapshot
.workspaces
.iter()
.any(|workspace| &workspace.workspace_id == selected)
})
{
self.navigate_workspace_id = snapshot.focused_workspace_id.clone();
}
let pane_exists =
|pane_id: &String| snapshot.panes.iter().any(|pane| &pane.pane_id == pane_id);
self.pane_scroll_in_flight
.retain(|pane_id, _| pane_exists(pane_id));
self.pane_scroll_queued
.retain(|pane_id, _| pane_exists(pane_id));
self.pane_scroll_targets
.retain(|pane_id, _| pane_exists(pane_id));
self.snapshot = Some(snapshot);
}
pub(crate) fn set_pane_surface(&mut self, surface: PaneSurfaceFrame) {
if self
.snapshot
.as_ref()
.is_none_or(|snapshot| snapshot.revision != surface.projection_revision)
{
self.hits = ShellHitMap::default();
}
let previous_popup = self.popup_terminal_id.clone();
let next_popup = surface
.popup
.as_deref()
.map(|popup| popup.terminal_id.clone());
if previous_popup != next_popup {
if let Some(terminal_id) = previous_popup.as_ref() {
self.input_leases
.remove_target(&ClientInputTarget::Popup(terminal_id.clone()));
}
self.mode = ClientShellMode::Terminal;
self.navigate_workspace_id = None;
self.overlay = None;
self.selection = None;
self.last_pane_click = None;
self.selection_autoscroll = None;
self.selection_autoscroll_deadline = None;
self.selection_highlight_clear_deadline = None;
self.pending_word_selection = None;
self.copy_mode = None;
self.reset_copy_pipeline();
self.chrome_drag = None;
self.workspace_press = None;
self.tab_press = None;
if self.pane_mouse_gesture.as_ref().is_some_and(|gesture| {
gesture.hit.popup && previous_popup.as_deref() == Some(gesture.hit.pane_id.as_str())
}) {
self.pane_mouse_gesture = None;
}
self.hits.popup = None;
self.endpoint_error = None;
}
if next_popup.is_some() {
self.popup_pending = false;
self.popup_pending_deadline = None;
}
let selection_content_changed = self.selection.as_ref().is_some_and(|selection| {
let previous_revision = self.pane_surface.as_ref().and_then(|previous| {
previous
.panes
.iter()
.find(|pane| pane.pane_id == selection.pane_id)
.map(|pane| pane.content_revision)
});
let next_revision = surface
.panes
.iter()
.find(|pane| pane.pane_id == selection.pane_id)
.map(|pane| pane.content_revision);
previous_revision.is_some()
&& next_revision.is_some()
&& previous_revision != next_revision
});
if selection_content_changed {
self.selection = None;
self.stop_selection_autoscroll();
self.selection_highlight_clear_deadline = None;
}
for pane in &surface.panes {
let Some(target) = self.pane_scroll_targets.get(&pane.pane_id).copied() else {
continue;
};
let Some(scroll) = pane.scroll else {
continue;
};
let target =
target.min(usize::try_from(scroll.max_offset_from_bottom).unwrap_or(usize::MAX));
if usize::try_from(scroll.offset_from_bottom).unwrap_or(usize::MAX) == target {
self.pane_scroll_targets.remove(&pane.pane_id);
}
}
let mut invalidated_copy_pane = None;
if let Some(copy_mode) = self.copy_mode.as_mut() {
if let Some(pane) = surface
.panes
.iter()
.find(|pane| pane.pane_id == copy_mode.pane_id)
{
let geometry = (pane.inner_rect.width, pane.inner_rect.height);
if copy_mode.content_revision != pane.content_revision
|| copy_mode.geometry != geometry
{
copy_mode.content_revision = pane.content_revision;
copy_mode.geometry = geometry;
copy_mode.selection = None;
copy_mode.search_matches.clear();
copy_mode.search_total = 0;
copy_mode.search_current = None;
copy_mode.search_current_global = None;
copy_mode.search_generation = copy_mode.search_generation.saturating_add(1);
copy_mode.copy_after_search = false;
invalidated_copy_pane = Some(copy_mode.pane_id.clone());
}
if let Some(scroll) = pane.scroll {
let actual_offset =
usize::try_from(scroll.offset_from_bottom).unwrap_or(usize::MAX);
if !self.pane_scroll_targets.contains_key(&pane.pane_id) {
copy_mode.offset_from_bottom = actual_offset;
}
copy_mode.max_offset_from_bottom =
usize::try_from(scroll.max_offset_from_bottom).unwrap_or(usize::MAX);
}
}
}
if invalidated_copy_pane.as_ref().is_some_and(|pane_id| {
self.selection
.as_ref()
.is_some_and(|selection| &selection.pane_id == pane_id)
}) {
self.selection = None;
self.stop_selection_autoscroll();
self.selection_highlight_clear_deadline = None;
}
self.popup_terminal_id = next_popup;
self.pane_surface = Some(surface);
}
pub(crate) fn tick_popup_pending(&mut self, now: std::time::Instant) {
if self
.popup_pending_deadline
.is_some_and(|deadline| now >= deadline)
{
self.popup_pending = false;
self.popup_pending_deadline = None;
}
}
pub(crate) fn tick_copy_feedback(&mut self, now: std::time::Instant) -> bool {
let mut repaint = false;
if self
.copy_feedback_deadline
.is_some_and(|deadline| now >= deadline)
{
self.copy_feedback = None;
self.copy_feedback_deadline = None;
repaint = true;
}
if self
.selection_highlight_clear_deadline
.is_some_and(|deadline| now >= deadline)
{
self.selection = None;
self.selection_highlight_clear_deadline = None;
repaint = true;
}
repaint
}
pub(crate) fn timer_delay(&self, now: std::time::Instant) -> std::time::Duration {
let default = std::time::Duration::from_millis(100);
self.selection_autoscroll_deadline
.map(|deadline| deadline.saturating_duration_since(now).min(default))
.unwrap_or(default)
}
pub(crate) fn invalidate_pane_surface(&mut self) {
self.pane_surface = None;
self.hits = ShellHitMap::default();
self.host_mouse_pixels = None;
}
}