summaryrefslogtreecommitdiff
path: root/src/server/headless.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-06 02:27:06 +0300
committerGitHub <noreply@github.com>2026-09-06 02:27:06 +0300
commit9e9bc8a144667a6b7debfd4f0c7a31ff0e11493a (patch)
tree03f1259f91fd9edeacfd4b904f2e84d4acf25232 /src/server/headless.rs
parent35b0dff96579f099e0436290085024424ffb289c (diff)
feat: manage multiple ssh machines from one client (#3670)
Diffstat (limited to 'src/server/headless.rs')
-rw-r--r--src/server/headless.rs217
1 files changed, 91 insertions, 126 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs
index 04a1b17b..abaca0aa 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -73,11 +73,13 @@ use crate::server::terminal_attach::paste_payload_for_runtime;
mod bootstrap;
mod client_views;
+mod endpoint_requests;
mod lifecycle;
mod notifications;
mod pane_graphics;
mod render;
mod retained_surface;
+mod surface_interest;
pub use bootstrap::run_server;
use lifecycle::wait_for_live_handoff_response_write;
@@ -910,6 +912,9 @@ impl HeadlessServer {
let Some(client) = self.clients.get_mut(&client_id) else {
return false;
};
+ if !client.shell_surface_active {
+ return false;
+ }
client.last_activity = stamp;
let changed = self.foreground_client_id != Some(client_id);
@@ -929,13 +934,13 @@ impl HeadlessServer {
fn app_client_count(&self) -> usize {
self.clients
.values()
- .filter(|client| client.is_shell_client() && client.writer.is_some())
+ .filter(|client| client.is_active_shell_client() && client.writer.is_some())
.count()
}
fn client_supports_direct_graphics(&self, client_id: u64) -> bool {
self.clients.get(&client_id).is_some_and(|client| {
- client.is_shell_client()
+ client.is_active_shell_client()
&& client.writer.is_some()
&& client.direct_graphics
&& client.pixel_mouse
@@ -992,12 +997,14 @@ impl HeadlessServer {
let disconnected_focus = self
.clients
.get(&client_id)
- .filter(|client| client.is_shell_client() && client.outer_terminal_focus == Some(true))
+ .filter(|client| {
+ client.is_active_shell_client() && client.outer_terminal_focus == Some(true)
+ })
.and_then(|_| self.shell_focus_target(client_id));
let should_release_focus = disconnected_focus.as_ref().is_some_and(|target| {
!self.clients.iter().any(|(&other_id, client)| {
other_id != client_id
- && client.is_shell_client()
+ && client.is_active_shell_client()
&& client.outer_terminal_focus == Some(true)
&& self.shell_tab_id_for_client(other_id).as_deref()
== Some(target.tab_id.as_str())
@@ -1008,7 +1015,8 @@ impl HeadlessServer {
self.tab_geometry_controllers
.retain(|_, controller_id| *controller_id != client_id);
if let Some(mut removed) = removed {
- self.release_client_shell_inputs(client_id, &mut removed);
+ let held_inputs = removed.drain_shell_held_inputs();
+ self.release_client_shell_inputs(client_id, held_inputs);
crate::server::clipboard_image::remove_files(removed.staged_clipboard_files);
if let ClientConnectionMode::TerminalAttach { terminal_id } = removed.mode {
self.terminal_attach_owners.remove(&terminal_id);
@@ -1033,8 +1041,12 @@ impl HeadlessServer {
}
}
- fn release_client_shell_inputs(&mut self, client_id: u64, client: &mut ClientConnection) {
- for held in client.drain_shell_held_inputs() {
+ fn release_client_shell_inputs(
+ &mut self,
+ client_id: u64,
+ held_inputs: Vec<crate::server::clients::ClientShellHeldInput>,
+ ) {
+ for held in held_inputs {
let result = match held.target {
ClientShellInputTarget::Pane(pane_id) => {
let Some((workspace_index, runtime_pane_id)) = self.app.parse_pane_id(&pane_id)
@@ -1181,16 +1193,18 @@ impl HeadlessServer {
protocol::ClientClipboardImageTarget::Pane(pane_id) => {
!self.handoff_in_progress
&& self.app.state.popup_pane.is_none()
- && self.clients.get(&client_id).is_some_and(|client| {
- matches!(client.mode, ClientConnectionMode::ClientShell)
- })
+ && self
+ .clients
+ .get(&client_id)
+ .is_some_and(ClientConnection::is_active_shell_client)
&& self.app.parse_pane_id(pane_id).is_some()
}
protocol::ClientClipboardImageTarget::Popup(terminal_id) => {
!self.handoff_in_progress
- && self.clients.get(&client_id).is_some_and(|client| {
- matches!(client.mode, ClientConnectionMode::ClientShell)
- })
+ && self
+ .clients
+ .get(&client_id)
+ .is_some_and(ClientConnection::is_active_shell_client)
&& self
.app
.state
@@ -1237,9 +1251,10 @@ impl HeadlessServer {
}
protocol::ClientClipboardImageTarget::Pane(pane_id) => {
if self.handoff_in_progress
- || !self.clients.get(&client_id).is_some_and(|client| {
- matches!(client.mode, ClientConnectionMode::ClientShell)
- })
+ || !self
+ .clients
+ .get(&client_id)
+ .is_some_and(ClientConnection::is_active_shell_client)
{
return false;
}
@@ -1273,9 +1288,10 @@ impl HeadlessServer {
}
protocol::ClientClipboardImageTarget::Popup(terminal_id) => {
if self.handoff_in_progress
- || !self.clients.get(&client_id).is_some_and(|client| {
- matches!(client.mode, ClientConnectionMode::ClientShell)
- })
+ || !self
+ .clients
+ .get(&client_id)
+ .is_some_and(ClientConnection::is_active_shell_client)
{
return false;
}
@@ -1929,6 +1945,7 @@ impl HeadlessServer {
direct_graphics,
endpoint_keybindings,
mouse_capture,
+ surface_active,
writer,
} => {
if self.handoff_in_progress {
@@ -1950,6 +1967,7 @@ impl HeadlessServer {
rows = surface_rows,
cell_width_px,
cell_height_px,
+ surface_active,
render_encoding = ?protocol::RenderEncoding::SemanticFrame,
"client connected"
);
@@ -1972,6 +1990,7 @@ impl HeadlessServer {
connection.direct_graphics = direct_graphics;
connection.shell_uses_endpoint_keybindings = endpoint_keybindings;
connection.shell_mouse_capture = mouse_capture;
+ connection.shell_surface_active = surface_active;
connection.shell_projection_revision = 1;
let config_diagnostic = if endpoint_keybindings {
self.server_config_diagnostic.as_deref()
@@ -2002,7 +2021,9 @@ impl HeadlessServer {
self.popup_owner_tab_id = self.shell_tab_id_for_client(client_id);
}
self.send_to_client(client_id, snapshot_message);
- self.foreground_client_id = Some(client_id);
+ if surface_active {
+ self.foreground_client_id = Some(client_id);
+ }
if first_app_client {
self.app.mark_git_status_refresh_due(Instant::now());
}
@@ -2224,6 +2245,9 @@ impl HeadlessServer {
client.cell_size = observed;
}
client.pixel_mouse = pixel_mouse && observed.is_known();
+ if !client.shell_surface_active {
+ return false;
+ }
client.request_repaint();
self.promote_client_to_foreground(client_id);
self.resize_shell_tab_if_controller(client_id, true);
@@ -2239,7 +2263,7 @@ impl HeadlessServer {
if !client.update_host_theme(&update) {
return false;
}
- if self.foreground_client_id != Some(client_id) {
+ if !client.shell_surface_active || self.foreground_client_id != Some(client_id) {
return false;
}
let mut changed = self.app.set_host_terminal_appearance_state(
@@ -2256,15 +2280,14 @@ impl HeadlessServer {
let Some(client) = self.clients.get(&client_id) else {
return false;
};
- if !matches!(client.mode, ClientConnectionMode::ClientShell)
- || client.outer_terminal_focus == Some(focused)
+ if !client.is_active_shell_client() || client.outer_terminal_focus == Some(focused)
{
return false;
}
let tab_id = self.shell_tab_id_for_client(client_id);
let another_focused_viewer = self.clients.iter().any(|(&other_id, client)| {
other_id != client_id
- && client.is_shell_client()
+ && client.is_active_shell_client()
&& client.outer_terminal_focus == Some(true)
&& self.shell_tab_id_for_client(other_id) == tab_id
});
@@ -2308,15 +2331,38 @@ impl HeadlessServer {
client.host_mouse_capture_active = None;
true
}
+ ServerEvent::ClientShellPresentationSync { client_id, token } => {
+ let Some(client) = self.clients.get_mut(&client_id) else {
+ return false;
+ };
+ if !client.is_active_shell_client() {
+ return false;
+ }
+ client.host_mouse_capture_active = None;
+ client.host_sgr_pixels_active = None;
+ client.host_keyboard_report_all_active = None;
+ self.sent_window_title = None;
+ self.stream_host_mouse_capture_mode();
+ self.stream_direct_terminal_keyboard_mode();
+ self.sync_window_title();
+ self.send_to_client(
+ client_id,
+ ServerMessage::EndpointControl {
+ kind: crate::protocol::endpoint::PRESENTATION_EFFECTS_READY_KIND.into(),
+ data: token,
+ },
+ )
+ }
ServerEvent::ClientShellPaneInput {
client_id,
pane_id,
events,
} => {
if self.handoff_in_progress
- || !self.clients.get(&client_id).is_some_and(|client| {
- matches!(client.mode, ClientConnectionMode::ClientShell)
- })
+ || !self
+ .clients
+ .get(&client_id)
+ .is_some_and(ClientConnection::is_active_shell_client)
{
return false;
}
@@ -2400,9 +2446,10 @@ impl HeadlessServer {
events,
} => {
if self.handoff_in_progress
- || !self.clients.get(&client_id).is_some_and(|client| {
- matches!(client.mode, ClientConnectionMode::ClientShell)
- })
+ || !self
+ .clients
+ .get(&client_id)
+ .is_some_and(ClientConnection::is_active_shell_client)
{
return false;
}
@@ -2494,96 +2541,8 @@ impl HeadlessServer {
ServerEvent::ClientShellEndpointRequest {
client_id,
boot_id,
- mut request,
- } => {
- let Some(client) = self.clients.get(&client_id) else {
- return false;
- };
- if !matches!(client.mode, ClientConnectionMode::ClientShell) {
- self.remove_client_and_resize_if_needed(client_id);
- return true;
- }
- let request_id = request.id.clone();
- if !crate::server::client_commands::supports_client_shell_method(&request.method) {
- let message = crate::server::client_commands::error_message(
- boot_id,
- request_id,
- "unsupported_endpoint_command",
- "this method is not available through the client shell command lane",
- );
- self.send_to_client(client_id, message);
- return false;
- }
- if boot_id != self.client_shell_boot_id {
- let message = crate::server::client_commands::error_message(
- boot_id,
- request_id,
- "stale_boot",
- "endpoint command targeted an earlier server boot",
- );
- self.send_to_client(client_id, message);
- return false;
- }
- if client.shell_endpoint_command_in_flight {
- self.send_to_client(
- client_id,
- ServerMessage::ServerShutdown {
- reason: Some("client sent concurrent endpoint commands".into()),
- },
- );
- self.remove_client_and_resize_if_needed(client_id);
- return true;
- }
-
- let api_request_id = format!(
- "endpoint:{}:{client_id}:{request_id}",
- self.client_shell_boot_id
- );
- request.id = api_request_id.clone();
- let (respond_to, response_rx) = std::sync::mpsc::channel();
- if let Err(err) = crate::server::client_commands::spawn_response_waiter(
- client_id,
- boot_id.clone(),
- request_id.clone(),
- response_rx,
- self.server_event_tx.clone(),
- ) {
- let message = crate::server::client_commands::error_message(
- boot_id,
- request_id,
- "server_unavailable",
- format!("failed to start endpoint response bridge: {err}"),
- );
- self.send_to_client(client_id, message);
- return false;
- }
- if let Some(client) = self.clients.get_mut(&client_id) {
- client.shell_endpoint_command_in_flight = true;
- let deferred_worktree = matches!(
- &request.method,
- api::schema::Method::WorktreeCreate(_)
- | api::schema::Method::WorktreeRemove(_)
- );
- let deferred_navigation = matches!(
- &request.method,
- api::schema::Method::WorktreeCreate(params) if params.focus
- );
- client.shell_deferred_navigation_request_id =
- deferred_worktree.then(|| api_request_id.clone());
- client.shell_deferred_navigation_response = deferred_navigation.then(Vec::new);
- }
- let foreground_changed = self.promote_client_to_foreground(client_id);
- foreground_changed
- | self.handle_client_shell_api_request(
- client_id,
- api::ApiRequestMessage {
- request: *request,
- respond_to,
- response_write_complete: None,
- stream_active: None,
- },
- )
- }
+ request,
+ } => self.handle_client_shell_endpoint_request(client_id, boot_id, request),
ServerEvent::ClientShellEndpointResponseChunkReady {
client_id,
boot_id,
@@ -2591,14 +2550,16 @@ impl HeadlessServer {
final_chunk,
data,
} => {
- let valid = self.clients.get(&client_id).is_some_and(|client| {
- matches!(client.mode, ClientConnectionMode::ClientShell)
+ let command_surface_revision = self.clients.get(&client_id).and_then(|client| {
+ (matches!(client.mode, ClientConnectionMode::ClientShell)
&& client.shell_endpoint_command_in_flight
- && boot_id == self.client_shell_boot_id
+ && boot_id == self.client_shell_boot_id)
+ .then_some(client.shell_endpoint_command_surface_revision)
+ .flatten()
});
- if !valid {
+ let Some(command_surface_revision) = command_surface_revision else {
return false;
- }
+ };
let completed_deferred_response =
self.clients.get_mut(&client_id).and_then(|client| {
let response = client.shell_deferred_navigation_response.as_mut()?;
@@ -2610,6 +2571,7 @@ impl HeadlessServer {
if final_chunk {
if let Some(client) = self.clients.get_mut(&client_id) {
client.shell_endpoint_command_in_flight = false;
+ client.shell_endpoint_command_surface_revision = None;
client.shell_deferred_navigation_request_id = None;
}
}
@@ -2618,7 +2580,10 @@ impl HeadlessServer {
.and_then(Self::deferred_endpoint_navigation_tab_id);
let focus_before = self.shell_focus_target(client_id);
let focused_tabs_before = self.focused_shell_tabs();
- let navigation_changed = deferred_tab_id
+ let navigation_changed = self.clients.get(&client_id).is_some_and(|client| {
+ client.is_active_shell_client()
+ && client.shell_projection_revision == command_surface_revision
+ }) && deferred_tab_id
.as_deref()
.is_some_and(|tab_id| self.focus_shell_client_on_tab(client_id, tab_id));
let geometry_changed =