diff options
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/clients.rs | 3 | ||||
| -rw-r--r-- | src/server/headless.rs | 133 |
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 = |
