aboutsummaryrefslogtreecommitdiff
path: root/src/server/clients.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/clients.rs')
-rw-r--r--src/server/clients.rs208
1 files changed, 208 insertions, 0 deletions
diff --git a/src/server/clients.rs b/src/server/clients.rs
new file mode 100644
index 00000000..4ee1ff09
--- /dev/null
+++ b/src/server/clients.rs
@@ -0,0 +1,208 @@
+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<Box<crate::config::LiveKeybindConfig>>,
+ /// 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<bool>,
+ /// 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<bool>,
+ /// Temporary files staged from this client's local clipboard image pastes.
+ pub(crate) staged_clipboard_files: Vec<PathBuf>,
+ /// Channels for sending framed ServerMessage data to the client writer thread.
+ pub(crate) writer: Option<ClientWriter>,
+}
+
+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<bool>,
+ last_activity: u64,
+ render_encoding: RenderEncoding,
+ writer: Option<ClientWriter>,
+ ) -> 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<Box<crate::config::LiveKeybindConfig>>,
+ terminal_size: (u16, u16),
+ cell_size: crate::kitty_graphics::HostCellSize,
+ host_terminal_theme: crate::terminal_theme::TerminalTheme,
+ outer_terminal_focus: Option<bool>,
+ last_activity: u64,
+ render_encoding: RenderEncoding,
+ writer: Option<ClientWriter>,
+ ) -> 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<bool> {
+ 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<u64, ClientConnection>) -> Option<u64> {
+ 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<u64, ClientConnection>,
+ terminal_id: &str,
+) -> Vec<u64> {
+ 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<u64, ClientConnection>,
+ foreground_client_id: Option<u64>,
+) -> Vec<RenderTarget> {
+ let mut targets: Vec<RenderTarget> = 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
+}