diff options
Diffstat (limited to 'src/client/shell.rs')
| -rw-r--r-- | src/client/shell.rs | 287 |
1 files changed, 287 insertions, 0 deletions
diff --git a/src/client/shell.rs b/src/client/shell.rs new file mode 100644 index 00000000..9a877744 --- /dev/null +++ b/src/client/shell.rs @@ -0,0 +1,287 @@ +use std::collections::{HashMap, HashSet, VecDeque}; + +mod actions; +mod agent_sidebar; +mod composition; +mod config; +mod context_menu; +mod copy_mode; +mod global_menu; +mod graphics; +mod input; +mod mobile; +mod mouse; +mod notifications; +mod overlay_input; +mod preferences; +mod render; +mod scroll; +mod settings; +mod state; +mod surface_patch; +mod worktrees; + +pub(crate) use state::*; +#[cfg(test)] +pub(super) use surface_patch::apply_composed_surface_patch; +pub(super) use surface_patch::{ClientComposedSurfacePatch, ClientPaneSurfacePatchOutcome}; + +use crossterm::event::KeyCode; +#[cfg(test)] +use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind}; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::{Modifier, Style}; +use unicode_width::UnicodeWidthStr; + +use crate::app::state::Palette; +use crate::config::{ + Config, LiveKeybindConfig, SidebarCollapsedModeConfig, SpacesSidebarConfig, + TabBarPositionConfig, +}; +use crate::protocol::{ + ClientMessage, ClientMousePosition, ClientPaneInputEvent, ClientShellSnapshot, ClientShellTab, + ClientShellWorkspace, ClientSurfaceSize, FrameData, PaneSurfaceFrame, SemanticNotification, + SemanticNotificationKind, SemanticNotificationSound, +}; +#[cfg(test)] +use crate::raw_input::RawInputEvent; + +fn delete_overlay_word(rename: &mut ClientRenameOverlay) { + if rename.replace_on_type { + rename.input.clear(); + rename.replace_on_type = false; + return; + } + while rename.input.chars().last().is_some_and(char::is_whitespace) { + rename.input.pop(); + } + let Some(word) = rename + .input + .chars() + .last() + .map(|character| character.is_alphanumeric() || character == '_') + else { + return; + }; + while rename.input.chars().last().is_some_and(|character| { + !character.is_whitespace() && (character.is_alphanumeric() || character == '_') == word + }) { + rename.input.pop(); + } +} + +fn target_event_message(target: ClientInputTarget, event: ClientPaneInputEvent) -> ClientMessage { + match target { + ClientInputTarget::Pane(pane_id) => ClientMessage::ClientShellPaneInput { + pane_id, + events: vec![event], + }, + ClientInputTarget::Popup(terminal_id) => ClientMessage::ClientShellPopupInput { + terminal_id, + events: vec![event], + }, + } +} + +fn push_target_event( + target: ClientInputTarget, + event: ClientPaneInputEvent, + outcome: &mut ClientShellInput, +) { + match target { + ClientInputTarget::Pane(pane_id) => { + if let Some(ClientMessage::ClientShellPaneInput { + pane_id: pending_pane, + events, + }) = outcome.requests.last_mut() + { + if *pending_pane == pane_id { + events.push(event); + return; + } + } + outcome.requests.push(target_event_message( + ClientInputTarget::Pane(pane_id), + event, + )); + } + ClientInputTarget::Popup(terminal_id) => { + if let Some(ClientMessage::ClientShellPopupInput { + terminal_id: pending_terminal, + events, + }) = outcome.requests.last_mut() + { + if *pending_terminal == terminal_id { + events.push(event); + return; + } + } + outcome.requests.push(target_event_message( + ClientInputTarget::Popup(terminal_id), + event, + )); + } + } +} + +fn contains(rect: Rect, point: (u16, u16)) -> bool { + rect.width > 0 + && rect.height > 0 + && point.0 >= rect.x + && point.0 < rect.right() + && point.1 >= rect.y + && point.1 < rect.bottom() +} + +fn pane_surface_topology_signature(surface: &PaneSurfaceFrame) -> u64 { + const OFFSET: u64 = 0xcbf2_9ce4_8422_2325; + const PRIME: u64 = 0x0000_0100_0000_01b3; + + fn write(hash: &mut u64, bytes: &[u8]) { + for byte in bytes { + *hash ^= u64::from(*byte); + *hash = hash.wrapping_mul(PRIME); + } + *hash ^= 0xff; + *hash = hash.wrapping_mul(PRIME); + } + + let mut pane_ids = surface + .panes + .iter() + .map(|pane| pane.pane_id.as_bytes()) + .collect::<Vec<_>>(); + pane_ids.sort_unstable(); + let mut hash = OFFSET; + for pane_id in pane_ids { + write(&mut hash, pane_id); + } + let mut splits = surface.splits.iter().collect::<Vec<_>>(); + splits.sort_by(|left, right| left.path.cmp(&right.path)); + for split in splits { + write( + &mut hash, + &[match split.direction { + crate::protocol::PaneSurfaceSplitDirection::Horizontal => 0, + crate::protocol::PaneSurfaceSplitDirection::Vertical => 1, + }], + ); + write( + &mut hash, + &split + .path + .iter() + .map(|right| u8::from(*right)) + .collect::<Vec<_>>(), + ); + } + hash +} + +fn status_icon( + status: crate::api::schema::AgentStatus, + style: crate::config::StatusIndicatorStyle, +) -> &'static str { + use crate::api::schema::AgentStatus; + use crate::config::StatusIndicatorStyle; + match (style, status) { + ( + StatusIndicatorStyle::Dots, + AgentStatus::Working | AgentStatus::Blocked | AgentStatus::Done, + ) => "●", + (StatusIndicatorStyle::Dots, AgentStatus::Idle) => "○", + (StatusIndicatorStyle::Dots, AgentStatus::Unknown) => "·", + (StatusIndicatorStyle::Symbols, AgentStatus::Blocked) => "×", + (StatusIndicatorStyle::Symbols, AgentStatus::Working) => "◐", + (StatusIndicatorStyle::Symbols, AgentStatus::Done) => "✓", + (StatusIndicatorStyle::Symbols, AgentStatus::Idle) => "○", + (StatusIndicatorStyle::Symbols, AgentStatus::Unknown) => "·", + } +} + +fn status_dot(status: crate::api::schema::AgentStatus) -> &'static str { + status_icon(status, crate::config::StatusIndicatorStyle::Dots) +} + +fn status_priority(status: crate::api::schema::AgentStatus) -> u8 { + use crate::api::schema::AgentStatus; + match status { + AgentStatus::Blocked => 4, + AgentStatus::Done => 3, + AgentStatus::Working => 2, + AgentStatus::Idle => 1, + AgentStatus::Unknown => 0, + } +} + +fn status_text(status: crate::api::schema::AgentStatus) -> &'static str { + use crate::api::schema::AgentStatus; + match status { + AgentStatus::Working => "working", + AgentStatus::Blocked => "blocked", + AgentStatus::Done => "done", + AgentStatus::Idle => "idle", + AgentStatus::Unknown => "unknown", + } +} + +fn status_color( + status: crate::api::schema::AgentStatus, + palette: &Palette, +) -> ratatui::style::Color { + use crate::api::schema::AgentStatus; + match status { + AgentStatus::Working => palette.yellow, + AgentStatus::Blocked => palette.red, + AgentStatus::Done => palette.teal, + AgentStatus::Idle => palette.green, + AgentStatus::Unknown => palette.overlay0, + } +} + +fn panel_contrast_fg(palette: &Palette) -> ratatui::style::Color { + match palette.panel_bg { + ratatui::style::Color::Reset => palette.surface_dim, + color => color, + } +} + +fn blit_pane_surface(target: &mut FrameData, source: &FrameData, area: Rect) { + let copy_width = source.width.min(area.width); + let copy_height = source.height.min(area.height); + let hyperlink_base = target.hyperlinks.len() as u32; + target.hyperlinks.extend(source.hyperlinks.iter().cloned()); + + for row in 0..copy_height { + for col in 0..copy_width { + let source_index = row as usize * source.width as usize + col as usize; + let target_x = area.x + col; + let target_y = area.y + row; + let target_index = target_y as usize * target.width as usize + target_x as usize; + let (Some(source_cell), Some(target_cell)) = ( + source.cells.get(source_index), + target.cells.get_mut(target_index), + ) else { + continue; + }; + *target_cell = source_cell.clone(); + target_cell.hyperlink = source_cell.hyperlink.and_then(|index| { + ((index as usize) < source.hyperlinks.len()).then_some(hyperlink_base + index) + }); + } + } + + target.cursor = source.cursor.as_ref().and_then(|cursor| { + (cursor.x < copy_width && cursor.y < copy_height).then(|| crate::protocol::CursorState { + x: area.x + cursor.x, + y: area.y + cursor.y, + visible: cursor.visible, + shape: cursor.shape, + }) + }); + target.graphics.clear(); +} + +#[cfg(test)] +mod tests; |
