aboutsummaryrefslogtreecommitdiff
path: root/src/server/headless.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-21 20:19:54 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-21 20:22:57 +0300
commit866aaf740e10fcfce169aaf81380ea0257fbe272 (patch)
treef815392b7c6b24a7c0de14e655bd635656383813 /src/server/headless.rs
parentca579c402d5668560ca13059f82c03178be18b2a (diff)
refactor: separate server client boundaries
Diffstat (limited to 'src/server/headless.rs')
-rw-r--r--src/server/headless.rs573
1 files changed, 72 insertions, 501 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs
index ddfd631c..842ded80 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -25,23 +25,35 @@ use std::time::{Duration, Instant};
use ratatui::layout::Rect;
use tokio::sync::mpsc;
-use tracing::{debug, error, info, warn};
+use tracing::{debug, info, warn};
use base64::Engine;
use bytes::Bytes;
use crate::api;
use crate::app;
-use crate::app::state::AppState;
use crate::config;
-use crate::detect::AgentState;
use crate::events::AppEvent;
-use crate::layout::PaneId;
-use crate::server::client_transport::{self, ClientWriter, ServerEvent};
-use crate::server::protocol::{
- self, FrameData, RenderEncoding, ServerMessage, MAX_FRAME_SIZE, MAX_GRAPHICS_FRAME_SIZE,
+use crate::protocol::{self, FrameData, ServerMessage, MAX_FRAME_SIZE, MAX_GRAPHICS_FRAME_SIZE};
+use crate::server::client_accept::accept_pending_client_connections;
+use crate::server::client_transport::ServerEvent;
+use crate::server::clients::{
+ events_include_interaction, latest_app_client, render_targets, terminal_attach_client_ids,
+ ClientConnection, ClientConnectionMode,
};
-use crate::server::render_stream::ClientRenderState;
+use crate::server::keybindings::{app_keybindings, apply_keybindings};
+use crate::server::notifications::{
+ should_forward_toast_to_clients, toast_message_from_state_change, toast_notify_kind,
+};
+use crate::server::socket_paths::{
+ client_socket_path, prepare_socket_path, restrict_socket_permissions,
+};
+use crate::server::terminal_attach::paste_payload_for_runtime;
+
+#[cfg(test)]
+use crate::protocol::RenderEncoding;
+#[cfg(test)]
+use crate::server::client_transport::ClientWriter;
// ---------------------------------------------------------------------------
// Loop event enum for the headless server event loop
@@ -64,28 +76,6 @@ enum LoopEvent {
const MIN_COLS: u16 = 80;
const MIN_ROWS: u16 = 24;
-fn paste_payload_for_runtime(runtime: &crate::terminal::TerminalRuntime, text: &str) -> String {
- if runtime
- .input_state()
- .map(|state| state.bracketed_paste)
- .unwrap_or(false)
- {
- format!("\x1b[200~{text}\x1b[201~")
- } else {
- text.to_owned()
- }
-}
-
-/// Legacy environment variable for overriding the client socket path.
-///
-/// Contractual override behavior for auto-detect uses `HERDR_SOCKET_PATH`.
-/// This variable is kept as a fallback for callers that explicitly need a
-/// client-only override when `HERDR_SOCKET_PATH` is not set.
-pub const CLIENT_SOCKET_PATH_ENV_VAR: &str = "HERDR_CLIENT_SOCKET_PATH";
-
-/// Socket permission mode (owner read/write only).
-const SOCKET_PERMISSION_MODE: u32 = 0o600;
-
/// Timeout for in-flight API requests during shutdown.
#[allow(dead_code)]
const SHUTDOWN_API_TIMEOUT: Duration = Duration::from_secs(5);
@@ -99,263 +89,6 @@ const SHUTDOWN_API_TIMEOUT: Duration = Duration::from_secs(5);
/// avoid reintroducing the idle CPU spin.
const CLIENT_ACCEPT_POLL_INTERVAL: Duration = Duration::from_millis(250);
-fn should_forward_toast_to_clients(delivery: config::ToastDelivery) -> bool {
- toast_notify_kind(delivery).is_some()
-}
-
-fn toast_notify_kind(delivery: config::ToastDelivery) -> Option<protocol::NotifyKind> {
- match delivery {
- config::ToastDelivery::Terminal => Some(protocol::NotifyKind::Toast),
- config::ToastDelivery::System => Some(protocol::NotifyKind::SystemToast),
- config::ToastDelivery::Off | config::ToastDelivery::Herdr => None,
- }
-}
-
-fn toast_event_text(kind: app::state::ToastKind) -> &'static str {
- match kind {
- app::state::ToastKind::NeedsAttention => "needs attention",
- app::state::ToastKind::Finished => "finished",
- app::state::ToastKind::UpdateInstalled => "updated",
- }
-}
-
-fn toast_message_from_state_change(
- state: &AppState,
- pane_id: PaneId,
- suppress_active_tab_notifications: bool,
- prev_state: AgentState,
- new_state: AgentState,
-) -> Option<String> {
- let kind = app::actions::notification_toast_for_state_change(
- suppress_active_tab_notifications,
- prev_state,
- new_state,
- )?;
-
- state
- .workspaces
- .iter()
- .enumerate()
- .find_map(|(ws_idx, ws)| {
- ws.tabs.iter().find_map(|tab| {
- let pane = tab.panes.get(&pane_id)?;
- let agent_label = state
- .terminals
- .get(&pane.attached_terminal_id)
- .and_then(|terminal| terminal.effective_agent_label())?;
- Some(format!(
- "{} {}: {}",
- agent_label,
- toast_event_text(kind),
- app::actions::notification_context(ws, ws_idx, pane_id)
- ))
- })
- })
-}
-
-// ---------------------------------------------------------------------------
-// Socket path helpers
-// ---------------------------------------------------------------------------
-
-/// Returns the path for the client protocol socket.
-///
-/// Contract-aligned override behavior:
-/// 1. If CLI `--session <name>` is active, use that session's client socket.
-/// 2. If `HERDR_SOCKET_PATH` is set, derive the client socket path from it by
-/// inserting `-client` before `.sock` (e.g. `herdr.sock` -> `herdr-client.sock`).
-/// This keeps JSON API and client socket overrides consistent.
-/// 3. Otherwise, honor `HERDR_CLIENT_SOCKET_PATH` (legacy/testing fallback).
-/// 4. Otherwise, use the active session data directory.
-pub fn client_socket_path() -> PathBuf {
- if crate::session::explicit_session_requested() {
- return crate::session::client_socket_path_for(crate::session::active_name().as_deref());
- }
- client_socket_path_from_overrides(
- std::env::var(api::SOCKET_PATH_ENV_VAR).ok().as_deref(),
- std::env::var(CLIENT_SOCKET_PATH_ENV_VAR).ok().as_deref(),
- )
-}
-
-fn client_socket_path_from_overrides(
- api_socket_override: Option<&str>,
- client_socket_override: Option<&str>,
-) -> PathBuf {
- if let Some(api_socket_override) = api_socket_override {
- return derive_client_socket_from_api_socket(Path::new(api_socket_override));
- }
-
- if let Some(client_socket_override) = client_socket_override {
- return PathBuf::from(client_socket_override);
- }
-
- crate::session::client_socket_path_for(crate::session::active_name().as_deref())
-}
-
-fn derive_client_socket_from_api_socket(api_socket_path: &Path) -> PathBuf {
- let stem = api_socket_path
- .file_stem()
- .and_then(|s| s.to_str())
- .unwrap_or("herdr");
- let parent = api_socket_path.parent().unwrap_or_else(|| Path::new(""));
-
- if api_socket_path
- .extension()
- .and_then(|ext| ext.to_str())
- .is_some_and(|ext| ext == "sock")
- {
- return parent.join(format!("{stem}-client.sock"));
- }
-
- parent.join(format!("{stem}-client.sock"))
-}
-
-fn app_keybindings(app: &app::App) -> crate::config::LiveKeybindConfig {
- crate::config::LiveKeybindConfig {
- prefix: (app.state.prefix_code, app.state.prefix_mods),
- keybinds: app.state.keybinds.clone(),
- }
-}
-
-fn apply_keybindings(app: &mut app::App, keybindings: &crate::config::LiveKeybindConfig) {
- app.state.prefix_code = keybindings.prefix.0;
- app.state.prefix_mods = keybindings.prefix.1;
- app.state.keybinds = keybindings.keybinds.clone();
-}
-
-// ---------------------------------------------------------------------------
-// Connected client state
-// ---------------------------------------------------------------------------
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-enum ClientConnectionMode {
- App,
- TerminalAttach { terminal_id: String },
-}
-
-type RenderTarget = (
- u64,
- (u16, u16),
- crate::kitty_graphics::HostCellSize,
- bool,
- ClientConnectionMode,
-);
-
-/// A connected client tracked by the server.
-struct ClientConnection {
- /// Whether this connection is the full app client or a direct terminal attach.
- mode: ClientConnectionMode,
- /// Client-local app keybindings. None means use the server's keybindings.
- keybindings: Option<Box<crate::config::LiveKeybindConfig>>,
- /// The client's terminal size (after clamping).
- terminal_size: (u16, u16),
- /// Pixel size of one client terminal cell.
- cell_size: crate::kitty_graphics::HostCellSize,
- /// Last known host terminal default colors for this client.
- host_terminal_theme: crate::terminal_theme::TerminalTheme,
- /// Last reported focus state for this client's outer terminal.
- outer_terminal_focus: Option<bool>,
- /// Monotonic activity stamp used to choose the fallback foreground client.
- last_activity: u64,
- /// Render baseline for the negotiated client encoding.
- render_state: ClientRenderState,
- /// Client-local host Kitty graphics cache.
- graphics_cache: crate::kitty_graphics::HostGraphicsCache,
- /// Whether the next graphics frame must clear and rebuild host-side Kitty state.
- graphics_surface_reset_pending: bool,
- /// Whether a render was skipped because the render channel was full.
- render_pending: bool,
- /// Last host mouse capture mode sent to this client.
- host_mouse_capture_active: Option<bool>,
- /// Temporary files staged from this client's local clipboard image pastes.
- staged_clipboard_files: Vec<PathBuf>,
- /// Channels for sending framed ServerMessage data to the client writer thread.
- writer: Option<ClientWriter>,
-}
-
-impl ClientConnection {
- #[cfg(test)]
- 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,
- )
- }
-
- 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,
- }
- }
-
- fn request_full_redraw(&mut self) {
- self.render_state.reset_baseline();
- self.graphics_surface_reset_pending = true;
- }
-
- fn request_semantic_redraw_after_input(&mut self) {
- self.render_state.reset_semantic_input_baseline();
- }
-}
-
-// ---------------------------------------------------------------------------
-// Stale socket cleanup
-// ---------------------------------------------------------------------------
-
-/// Prepares a socket path for binding: creates parent directories,
-/// removes stale socket files (where no server is listening), and
-/// returns an error if a live server is already bound.
-fn prepare_socket_path(path: &Path) -> io::Result<()> {
- crate::ipc::prepare_socket_path(path, |path| {
- format!(
- "herdr server is already running (socket busy at {})",
- path.display()
- )
- })
-}
-
-/// Restricts socket file permissions to owner-only (0o600).
-fn restrict_socket_permissions(path: &Path) -> io::Result<()> {
- crate::ipc::restrict_socket_permissions(path, SOCKET_PERMISSION_MODE)
-}
-
// ---------------------------------------------------------------------------
// Headless server
// ---------------------------------------------------------------------------
@@ -616,9 +349,18 @@ impl HeadlessServer {
let (cols, rows) = self.effective_size;
let area = Rect::new(0, 0, cols, rows);
if self.app.state.kitty_graphics_enabled && client.cell_size.is_known() {
- crate::ui::compute_view_with_cell_size(&mut self.app.state, area, client.cell_size);
+ crate::ui::compute_view_with_cell_size(
+ &mut self.app.state,
+ &self.app.terminal_runtimes,
+ area,
+ client.cell_size,
+ );
} else {
- crate::ui::compute_view(&mut self.app.state, area);
+ crate::ui::compute_view_with_runtime_registry(
+ &mut self.app.state,
+ &self.app.terminal_runtimes,
+ area,
+ );
}
// Shared runtime size changes affect pane wrapping and foreground-driven
@@ -697,12 +439,7 @@ impl HeadlessServer {
}
fn promote_latest_remaining_client(&mut self) -> bool {
- let next_foreground = self
- .clients
- .iter()
- .filter(|(_, client)| matches!(client.mode, ClientConnectionMode::App))
- .max_by_key(|(_, client)| client.last_activity)
- .map(|(&client_id, _)| client_id);
+ let next_foreground = latest_app_client(&self.clients);
let changed = next_foreground != self.foreground_client_id;
self.foreground_client_id = next_foreground;
self.sync_foreground_client_state();
@@ -768,20 +505,12 @@ impl HeadlessServer {
return false;
};
- let mut next_theme = client.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 == client.host_terminal_theme {
+ if !client.update_host_theme_from_events(events) {
return false;
}
- client.host_terminal_theme = next_theme;
if self.foreground_client_id == Some(client_id) {
- self.app.set_host_terminal_theme(next_theme)
+ self.app.set_host_terminal_theme(client.host_terminal_theme)
} else {
false
}
@@ -792,77 +521,25 @@ impl HeadlessServer {
client_id: u64,
events: &[crate::raw_input::RawInputEvent],
) {
- 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();
-
- let Some(next_focus) = next_focus else {
+ let Some(client) = self.clients.get_mut(&client_id) else {
return;
};
- let Some(client) = self.clients.get_mut(&client_id) else {
+ let Some(next_focus) = client.update_outer_focus_from_events(events) else {
return;
};
- client.outer_terminal_focus = Some(next_focus);
if self.foreground_client_id == Some(client_id) {
self.app.state.outer_terminal_focus = Some(next_focus);
}
}
- 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
- )
- })
- }
-
/// Accepts pending client connections from the non-blocking listener.
fn accept_client_connections(&mut self) -> io::Result<()> {
- loop {
- match self.client_listener.accept() {
- Ok((stream, _addr)) => {
- let client_id = self.next_client_id;
- self.next_client_id += 1;
-
- if let Err(err) = stream.set_nonblocking(true) {
- warn!(err = %err, "failed to set client stream nonblocking");
- continue;
- }
-
- // Spawn a thread for the handshake and read loop.
- let should_quit = self.should_quit.clone();
- let server_event_tx = self.server_event_tx.clone();
- std::thread::spawn(move || {
- if let Err(err) = client_transport::handle_client_handshake(
- stream,
- client_id,
- &server_event_tx,
- &should_quit,
- ) {
- debug!(client_id, err = %err, "client handshake failed");
- }
- });
- }
- Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => {
- // No more pending connections.
- break;
- }
- Err(err) => {
- error!(err = %err, "client listener accept failed");
- break;
- }
- }
- }
- Ok(())
+ accept_pending_client_connections(
+ &self.client_listener,
+ &mut self.next_client_id,
+ &self.should_quit,
+ &self.server_event_tx,
+ )
}
/// Drains server events from the dedicated channel.
@@ -890,7 +567,7 @@ impl HeadlessServer {
terminal_id: &str,
) -> Option<&crate::terminal::TerminalRuntime> {
let terminal_id = self.terminal_id_by_string(terminal_id)?;
- self.app.state.terminal_runtimes.get(&terminal_id)
+ self.app.terminal_runtimes.get(&terminal_id)
}
fn write_client_clipboard_image(
@@ -1328,16 +1005,7 @@ impl HeadlessServer {
}
fn shutdown_terminal_attach_clients(&mut self, terminal_id: &str, reason: String) {
- let client_ids: Vec<u64> = self
- .clients
- .iter()
- .filter_map(|(&client_id, client)| match &client.mode {
- ClientConnectionMode::TerminalAttach {
- terminal_id: attached,
- } if attached == terminal_id => Some(client_id),
- _ => None,
- })
- .collect();
+ let client_ids = terminal_attach_client_ids(&self.clients, terminal_id);
for client_id in client_ids {
self.send_to_client(
@@ -1419,7 +1087,7 @@ impl HeadlessServer {
.state
.direct_attach_resize_locks
.insert(real_terminal_id.clone());
- if let Some(runtime) = self.app.state.terminal_runtimes.get(&real_terminal_id) {
+ if let Some(runtime) = self.app.terminal_runtimes.get(&real_terminal_id) {
runtime.resize(rows, cols, cell_size.width_px, cell_size.height_px);
}
true
@@ -1504,7 +1172,7 @@ impl HeadlessServer {
}
}
self.update_client_outer_focus_from_events(client_id, &events);
- let interaction = Self::events_include_interaction(&events);
+ let interaction = events_include_interaction(&events);
let foreground_changed = if interaction {
self.promote_client_to_foreground(client_id)
} else {
@@ -1878,7 +1546,10 @@ impl HeadlessServer {
}
fn stream_host_mouse_capture_mode(&mut self) {
- let enabled = self.app.state.should_capture_host_mouse();
+ let enabled = self
+ .app
+ .state
+ .should_capture_host_mouse_from(&self.app.terminal_runtimes);
let serialized = match Self::frame_server_message(&ServerMessage::MouseCapture { enabled })
{
Ok(framed) => framed,
@@ -1921,33 +1592,18 @@ impl HeadlessServer {
/// Renders the current state to client-sized virtual buffers and streams
/// frames to all connected clients.
fn render_and_stream(&mut self) {
- let foreground_client_id = self.foreground_client_id;
- let mut render_targets: Vec<RenderTarget> = self
- .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();
-
- render_targets
- .sort_by_key(|(client_id, _, _, is_foreground, _)| (*is_foreground, *client_id));
+ let render_targets = render_targets(&self.clients, self.foreground_client_id);
if render_targets.is_empty() {
let (cols, rows) = self.effective_size;
let area = Rect::new(0, 0, cols, rows);
let resize_panes = self.app.state.view.pane_infos.is_empty();
- let _ = crate::server::render_stream::render_virtual(
+ let _ = crate::server::render_stream::render_virtual_with_runtime_registry(
&mut self.app.state,
+ &self.app.terminal_runtimes,
area,
resize_panes,
+ crate::kitty_graphics::HostCellSize::default(),
);
debug!(
cols,
@@ -1964,21 +1620,26 @@ impl HeadlessServer {
ClientConnectionMode::App => {
let (buffer, cursor) =
if self.app.state.kitty_graphics_enabled && cell_size.is_known() {
- crate::server::render_stream::render_virtual_with_cell_size(
+ crate::server::render_stream::render_virtual_with_runtime_registry(
&mut self.app.state,
+ &self.app.terminal_runtimes,
area,
is_foreground,
cell_size,
)
} else {
- crate::server::render_stream::render_virtual(
+ crate::server::render_stream::render_virtual_with_runtime_registry(
&mut self.app.state,
+ &self.app.terminal_runtimes,
area,
is_foreground,
+ crate::kitty_graphics::HostCellSize::default(),
)
};
- let hyperlinks =
- crate::server::render_stream::visible_hyperlinks(&self.app.state);
+ let hyperlinks = crate::server::render_stream::visible_hyperlinks(
+ &self.app.state,
+ &self.app.terminal_runtimes,
+ );
FrameData::from_ratatui_buffer_with_hyperlinks(&buffer, cursor, &hyperlinks)
}
ClientConnectionMode::TerminalAttach { terminal_id } => {
@@ -2014,6 +1675,7 @@ impl HeadlessServer {
.graphics
.extend(crate::kitty_graphics::encode_local_pane_graphics(
&self.app.state,
+ &self.app.terminal_runtimes,
cell_size,
&mut next_graphics_cache,
));
@@ -2415,7 +2077,8 @@ fn init_logging() {
mod tests {
use super::*;
- use crate::server::protocol::CursorState;
+ use crate::app::AppState;
+ use crate::protocol::CursorState;
fn test_headless_server() -> HeadlessServer {
let config = crate::config::Config::default();
@@ -2632,100 +2295,6 @@ new_tab = "prefix+t"
}
#[test]
- fn client_socket_path_derived_from_api_socket_override() {
- let path = client_socket_path_from_overrides(Some("/tmp/test-herdr.sock"), None);
- assert_eq!(path, PathBuf::from("/tmp/test-herdr-client.sock"));
- }
-
- #[test]
- fn client_socket_path_api_override_takes_precedence_over_legacy_client_override() {
- let path = client_socket_path_from_overrides(
- Some("/tmp/test-herdr.sock"),
- Some("/tmp/legacy-client.sock"),
- );
- assert_eq!(path, PathBuf::from("/tmp/test-herdr-client.sock"));
- }
-
- #[test]
- fn client_socket_path_respects_legacy_client_override_without_api_override() {
- let path = client_socket_path_from_overrides(None, Some("/tmp/test-herdr-client.sock"));
- assert_eq!(path, PathBuf::from("/tmp/test-herdr-client.sock"));
- }
-
- #[test]
- fn client_socket_path_defaults_to_config_dir() {
- std::env::remove_var(crate::session::SESSION_ENV_VAR);
- crate::session::clear_explicit_session_for_test();
- let path = client_socket_path_from_overrides(None, None);
- assert_eq!(path, config::config_dir().join("herdr-client.sock"));
- }
-
- #[test]
- fn derive_client_socket_from_api_socket_without_sock_extension() {
- let derived = derive_client_socket_from_api_socket(Path::new("/tmp/custom-api"));
- assert_eq!(derived, PathBuf::from("/tmp/custom-api-client.sock"));
- }
-
- #[test]
- fn prepare_socket_path_removes_stale_socket() {
- let dir = PathBuf::from(format!(
- "/tmp/hs-{}-{}",
- std::process::id(),
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .map(|d| d.as_nanos())
- .unwrap_or(0)
- ));
- let _ = fs::create_dir_all(&dir);
- let socket_path = dir.join("stale.sock");
-
- // Create a socket file that nobody is listening on.
- {
- let _listener = UnixListener::bind(&socket_path).expect("bind stale socket");
- }
- // The listener scope ended, so the socket is now stale.
- let deadline = std::time::Instant::now() + Duration::from_secs(1);
- while std::time::Instant::now() < deadline {
- if std::os::unix::net::UnixStream::connect(&socket_path).is_err() {
- break;
- }
- std::thread::sleep(Duration::from_millis(10));
- }
-
- // prepare_socket_path should remove it without error.
- let result = prepare_socket_path(&socket_path);
- assert!(result.is_ok(), "should remove stale socket: {result:?}");
-
- // Socket file should be gone.
- assert!(!socket_path.exists());
-
- let _ = fs::remove_dir_all(&dir);
- }
-
- #[test]
- fn prepare_socket_path_rejects_live_socket() {
- let dir = PathBuf::from(format!(
- "/tmp/hl-{}-{}",
- std::process::id(),
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .map(|d| d.as_nanos())
- .unwrap_or(0)
- ));
- let _ = fs::create_dir_all(&dir);
- let socket_path = dir.join("live.sock");
-
- // Bind a live listener.
- let _listener = UnixListener::bind(&socket_path).expect("bind");
-
- let result = prepare_socket_path(&socket_path);
- assert!(result.is_err());
- assert_eq!(result.unwrap_err().kind(), io::ErrorKind::AddrInUse);
-
- let _ = fs::remove_dir_all(&dir);
- }
-
- #[test]
fn virtual_render_produces_nonempty_buffer() {
let mut state = AppState::test_new();
let area = Rect::new(0, 0, 80, 24);
@@ -2876,8 +2445,9 @@ new_tab = "prefix+t"
let area = Rect::new(0, 0, 80, 24);
let _ = crate::server::render_stream::render_virtual(&mut state, area, true);
+ let terminal_runtimes = crate::terminal::TerminalRuntimeRegistry::new();
let runtime = state
- .runtime_for_pane(pane_id)
+ .runtime_for_pane(&terminal_runtimes, pane_id)
.expect("pane runtime after initial render");
runtime.scroll_up(6);
assert!(crate::ui::pane_is_scrolled_back(runtime));
@@ -3031,8 +2601,9 @@ new_tab = "prefix+t"
let area = Rect::new(0, 0, 80, 24);
let _ = crate::server::render_stream::render_virtual(&mut state, area, true);
+ let terminal_runtimes = crate::terminal::TerminalRuntimeRegistry::new();
let runtime = state
- .runtime_for_pane(pane_id)
+ .runtime_for_pane(&terminal_runtimes, pane_id)
.expect("pane runtime after initial render");
runtime.scroll_up(6);
assert!(crate::ui::pane_is_scrolled_back(runtime));
@@ -3325,7 +2896,7 @@ new_tab = "prefix+t"
server
.app
.state
- .runtime_for_pane(active_pane)
+ .runtime_for_pane(&server.app.terminal_runtimes, active_pane)
.unwrap()
.current_size(),
expected
@@ -3334,7 +2905,7 @@ new_tab = "prefix+t"
server
.app
.state
- .runtime_for_pane(background_pane)
+ .runtime_for_pane(&server.app.terminal_runtimes, background_pane)
.unwrap()
.current_size(),
expected