use std::collections::HashMap; use std::path::PathBuf; use crate::protocol::RenderEncoding; use crate::server::client_transport::ClientWriter; use crate::server::render_stream::ClientRenderState; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum ClientConnectionMode { App, TerminalAttach { terminal_id: String }, } pub(crate) type RenderTarget = ( u64, (u16, u16), crate::kitty_graphics::HostCellSize, bool, ClientConnectionMode, ); /// A connected client tracked by the server. pub(crate) struct ClientConnection { /// Whether this connection is the full app client or a direct terminal attach. pub(crate) mode: ClientConnectionMode, /// Client-local app keybindings. None means use the server's keybindings. pub(crate) keybindings: Option>, /// The client's terminal size after clamping. pub(crate) terminal_size: (u16, u16), /// Pixel size of one client terminal cell. pub(crate) cell_size: crate::kitty_graphics::HostCellSize, /// Last known host terminal default colors for this client. pub(crate) host_terminal_theme: crate::terminal_theme::TerminalTheme, /// Last reported focus state for this client's outer terminal. pub(crate) outer_terminal_focus: Option, /// Monotonic activity stamp used to choose the fallback foreground client. pub(crate) last_activity: u64, /// Render baseline for the negotiated client encoding. pub(crate) render_state: ClientRenderState, /// Client-local host Kitty graphics cache. pub(crate) graphics_cache: crate::kitty_graphics::HostGraphicsCache, /// Whether the next graphics frame must clear and rebuild host-side Kitty state. pub(crate) graphics_surface_reset_pending: bool, /// Whether a render was skipped because the render channel was full. pub(crate) render_pending: bool, /// Last host mouse capture mode sent to this client. pub(crate) host_mouse_capture_active: Option, /// Temporary files staged from this client's local clipboard image pastes. pub(crate) staged_clipboard_files: Vec, /// Channels for sending framed ServerMessage data to the client writer thread. pub(crate) writer: Option, } impl ClientConnection { #[cfg(test)] pub(crate) fn new( terminal_size: (u16, u16), cell_size: crate::kitty_graphics::HostCellSize, host_terminal_theme: crate::terminal_theme::TerminalTheme, outer_terminal_focus: Option, last_activity: u64, render_encoding: RenderEncoding, writer: Option, ) -> Self { Self::new_with_mode( ClientConnectionMode::App, None, terminal_size, cell_size, host_terminal_theme, outer_terminal_focus, last_activity, render_encoding, writer, ) } pub(crate) fn new_with_mode( mode: ClientConnectionMode, keybindings: Option>, terminal_size: (u16, u16), cell_size: crate::kitty_graphics::HostCellSize, host_terminal_theme: crate::terminal_theme::TerminalTheme, outer_terminal_focus: Option, last_activity: u64, render_encoding: RenderEncoding, writer: Option, ) -> Self { Self { mode, keybindings, terminal_size, cell_size, host_terminal_theme, outer_terminal_focus, last_activity, render_state: ClientRenderState::new(render_encoding), graphics_cache: crate::kitty_graphics::HostGraphicsCache::default(), graphics_surface_reset_pending: false, render_pending: false, host_mouse_capture_active: None, staged_clipboard_files: Vec::new(), writer, } } pub(crate) fn request_full_redraw(&mut self) { self.render_state.reset_baseline(); self.graphics_surface_reset_pending = true; } pub(crate) fn request_semantic_redraw_after_input(&mut self) { self.render_state.reset_semantic_input_baseline(); } pub(crate) fn update_host_theme_from_events( &mut self, events: &[crate::raw_input::RawInputEvent], ) -> bool { let mut next_theme = self.host_terminal_theme; for event in events { if let crate::raw_input::RawInputEvent::HostDefaultColor { kind, color } = event { next_theme = next_theme.with_color(*kind, *color); } } if next_theme == self.host_terminal_theme { return false; } self.host_terminal_theme = next_theme; true } pub(crate) fn update_outer_focus_from_events( &mut self, events: &[crate::raw_input::RawInputEvent], ) -> Option { let next_focus = events .iter() .filter_map(|event| match event { crate::raw_input::RawInputEvent::OuterFocusGained => Some(true), crate::raw_input::RawInputEvent::OuterFocusLost => Some(false), _ => None, }) .next_back()?; self.outer_terminal_focus = Some(next_focus); Some(next_focus) } } pub(crate) fn events_include_interaction(events: &[crate::raw_input::RawInputEvent]) -> bool { events.iter().any(|event| { matches!( event, crate::raw_input::RawInputEvent::Key(_) | crate::raw_input::RawInputEvent::Mouse(_) | crate::raw_input::RawInputEvent::Paste(_) | crate::raw_input::RawInputEvent::OuterFocusGained ) }) } pub(crate) fn latest_app_client(clients: &HashMap) -> Option { clients .iter() .filter(|(_, client)| matches!(client.mode, ClientConnectionMode::App)) .max_by_key(|(_, client)| client.last_activity) .map(|(&client_id, _)| client_id) } pub(crate) fn terminal_attach_client_ids( clients: &HashMap, terminal_id: &str, ) -> Vec { clients .iter() .filter_map(|(&client_id, client)| match &client.mode { ClientConnectionMode::TerminalAttach { terminal_id: attached, } if attached == terminal_id => Some(client_id), _ => None, }) .collect() } pub(crate) fn render_targets( clients: &HashMap, foreground_client_id: Option, ) -> Vec { let mut targets: Vec = clients .iter() .filter(|(_, client)| client.writer.is_some()) .map(|(&client_id, client)| { ( client_id, client.terminal_size, client.cell_size, foreground_client_id == Some(client_id), client.mode.clone(), ) }) .collect(); targets.sort_by_key(|(client_id, _, _, is_foreground, _)| (*is_foreground, *client_id)); targets }