aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/clients.rs3
-rw-r--r--src/server/headless.rs133
2 files changed, 134 insertions, 2 deletions
diff --git a/src/server/clients.rs b/src/server/clients.rs
index 243a131d..b1a56f0e 100644
--- a/src/server/clients.rs
+++ b/src/server/clients.rs
@@ -64,6 +64,8 @@ pub(crate) struct ClientConnection {
pane_graphics_render_pending: bool,
/// Last host mouse capture mode sent to this client.
pub(crate) host_mouse_capture_active: Option<bool>,
+ /// Last Kitty report-all mode sent to this client's host terminal.
+ pub(crate) host_keyboard_report_all_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.
@@ -127,6 +129,7 @@ impl ClientConnection {
render_pending: false,
pane_graphics_render_pending: false,
host_mouse_capture_active: None,
+ host_keyboard_report_all_active: None,
staged_clipboard_files: Vec::new(),
writer,
}
diff --git a/src/server/headless.rs b/src/server/headless.rs
index a1c0e257..6817e2ad 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -613,6 +613,7 @@ impl HeadlessServer {
self.drain_client_config_reload_request();
self.stream_host_mouse_capture_mode();
+ self.stream_host_keyboard_enhancement_flags();
self.app.sync_headless_animation_timer(now);
@@ -1397,6 +1398,7 @@ impl HeadlessServer {
fn remove_client(&mut self, client_id: u64) -> bool {
let was_foreground = self.foreground_client_id == Some(client_id);
+ self.app.clear_input_source(client_id);
self.send_client_graphics_cleanup(client_id);
let removed = self.clients.remove(&client_id);
if let Some(removed) = removed {
@@ -2595,6 +2597,12 @@ impl HeadlessServer {
}
if source_is_full_app {
self.update_client_outer_focus_from_events(client_id, &events);
+ if events
+ .iter()
+ .any(|event| matches!(event, crate::raw_input::RawInputEvent::OuterFocusLost))
+ {
+ self.app.clear_input_source(client_id);
+ }
}
let events = events_for_app_routing(events, source_was_foreground, source_is_full_app);
let interaction = events_include_interaction(&events);
@@ -2607,8 +2615,11 @@ impl HeadlessServer {
self.resize_shared_runtime_to_effective_size_before_input();
}
let theme_changed = self.update_client_host_theme_from_events(client_id, &events);
- self.app
- .route_client_events(events, self.foreground_client_id == Some(client_id));
+ self.app.route_client_events_from(
+ client_id,
+ events,
+ self.foreground_client_id == Some(client_id),
+ );
if self.app.take_config_reloaded_from_disk() {
self.reload_server_config(false);
} else {
@@ -3352,6 +3363,44 @@ impl HeadlessServer {
}
}
+ fn stream_host_keyboard_enhancement_flags(&mut self) {
+ let report_all_keys = self.app.host_keyboard_report_all_requested();
+ let serialized = match Self::frame_server_message(&ServerMessage::KittyKeyboardReportAll {
+ enabled: report_all_keys,
+ }) {
+ Ok(framed) => framed,
+ Err(err) => {
+ warn!(err = %err, "failed to serialize keyboard enhancement flags for clients");
+ return;
+ }
+ };
+
+ let mut broken_clients = Vec::new();
+ for (&client_id, client) in &mut self.clients {
+ if !client.is_full_app_client()
+ || client.host_keyboard_report_all_active == Some(report_all_keys)
+ {
+ continue;
+ }
+ let Some(writer) = &client.writer else {
+ continue;
+ };
+ if writer.control.send(serialized.clone()).is_err() {
+ debug!(
+ client_id,
+ "client writer channel closed during keyboard enhancement update"
+ );
+ broken_clients.push(client_id);
+ continue;
+ }
+ client.host_keyboard_report_all_active = Some(report_all_keys);
+ }
+
+ for client_id in broken_clients {
+ self.remove_client_and_resize_if_needed(client_id);
+ }
+ }
+
fn render_retained_pty_update_and_stream(&mut self) -> bool {
crate::render_prof::event("retained.attempt");
let retained_started = crate::render_prof::timer();
@@ -6820,6 +6869,43 @@ next_tab = ""
}
#[tokio::test]
+ async fn background_client_focus_loss_releases_its_owned_keys() {
+ let mut server = test_headless_server();
+ let mut input_rx = install_focused_test_runtime(&mut server, b"\x1b[>15u");
+ server.clients.insert(1, test_app_client(Some(true), 1));
+ server.clients.insert(2, test_app_client(Some(true), 2));
+ server.foreground_client_id = Some(1);
+ server.sync_foreground_client_state();
+
+ assert!(server.handle_server_event(ServerEvent::ClientInputEvents {
+ client_id: 1,
+ events: vec![crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Char('j'),
+ modifiers: 0,
+ kind: crate::protocol::ClientKeyKind::Press,
+ }],
+ }));
+ server.foreground_client_id = Some(2);
+ server.sync_foreground_client_state();
+
+ assert!(!server.handle_server_event(ServerEvent::ClientInputEvents {
+ client_id: 1,
+ events: vec![crate::protocol::ClientInputEvent::FocusLost],
+ }));
+ assert_eq!(
+ input_rx.try_recv().expect("forwarded press"),
+ Bytes::from_static(b"\x1b[106;1:1u")
+ );
+ assert_eq!(
+ input_rx
+ .try_recv()
+ .expect("synthetic release from background client"),
+ Bytes::from_static(b"\x1b[106;1:3u")
+ );
+ assert!(server.app.pressed_terminal_keys.is_empty());
+ }
+
+ #[tokio::test]
async fn structured_outer_focus_events_reach_reporting_pane() {
let mut server = test_headless_server();
let mut input_rx = install_focused_test_runtime(&mut server, b"\x1b[?1004h");
@@ -7933,6 +8019,49 @@ next_tab = ""
}
#[tokio::test]
+ async fn focused_report_all_pane_updates_headless_client_keyboard_flags() {
+ let mut server = test_headless_server();
+ let (client_tx, client_control_rx, _client_rx) = test_client_writer();
+ server.clients.insert(
+ 1,
+ ClientConnection::new(
+ (80, 24),
+ crate::kitty_graphics::HostCellSize::default(),
+ crate::terminal_theme::TerminalTheme::default(),
+ None,
+ 1,
+ RenderEncoding::SemanticFrame,
+ Some(client_tx),
+ ),
+ );
+ let popup_runtime =
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(40, 12, b"\x1b[>15u");
+ server.app.install_test_popup_runtime(popup_runtime);
+
+ server.stream_host_keyboard_enhancement_flags();
+
+ assert!(matches!(
+ read_server_message(
+ client_control_rx
+ .recv_timeout(Duration::from_millis(100))
+ .expect("keyboard enhancement message")
+ ),
+ ServerMessage::KittyKeyboardReportAll { enabled: true }
+ ));
+
+ assert!(server.app.close_popup_pane());
+ server.stream_host_keyboard_enhancement_flags();
+ assert!(matches!(
+ read_server_message(
+ client_control_rx
+ .recv_timeout(Duration::from_millis(100))
+ .expect("IME-compatible keyboard enhancement message")
+ ),
+ ServerMessage::KittyKeyboardReportAll { enabled: false }
+ ));
+ }
+
+ #[tokio::test]
async fn virtual_render_uses_popup_cursor() {
let (mut server, _client_rx, _) = retained_test_server(b"\x1b[2;2H");
let popup_runtime =