diff options
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/headless.rs | 120 |
1 files changed, 118 insertions, 2 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs index 761fcfc9..e84dfc59 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -1842,6 +1842,14 @@ impl HeadlessServer { } true } + AppEvent::PrefixInputSource { active } => { + // Input-source switching is a client-local host side effect; forward it to the + // foreground client (which owns the real TIS switch + run-loop pump), like clipboard. + self.send_to_foreground_client(ServerMessage::PrefixInputSource { + active: *active, + }); + true + } AppEvent::StateChanged { pane_id, agent, .. } => { // Capture toast before handling. let toast_before = self.app.state.toast.clone(); @@ -3953,8 +3961,11 @@ pub fn run_server() -> io::Result<()> { // The server runs headless — disable local notification side effects. // Sound and terminal notifications are forwarded to connected clients // as ServerMessage::Notify instead of emitted by the server process. + // The prefix input-source switch is likewise forwarded to the foreground + // client (ServerMessage::PrefixInputSource), never applied in-process. app.state.local_sound_playback = false; app.local_terminal_notifications = false; + app.local_input_source_switch = false; // Create the headless server. let mut server = match HeadlessServer::new( @@ -4054,6 +4065,7 @@ fn run_handoff_import_server(socket_path: &Path, token: &str) -> io::Result<()> )?; app.state.local_sound_playback = false; app.local_terminal_notifications = false; + app.local_input_source_switch = false; crate::server::handoff::report_restored(&mut received.stream)?; if std::env::var("HERDR_TEST_HANDOFF_IMPORT_FAIL").as_deref() == Ok("after_restored") { return Err(io::Error::other( @@ -4151,6 +4163,7 @@ mod tests { let mut app = crate::app::App::new(&config, true, None, api_rx, event_hub); app.state.local_sound_playback = false; app.local_terminal_notifications = false; + app.local_input_source_switch = false; let dir = std::env::temp_dir().join(format!( "hh-{}-{}", @@ -7745,6 +7758,107 @@ next_tab = "" } #[test] + fn prefix_input_source_targets_foreground_client_only() { + let mut server = test_headless_server(); + let (background_tx, background_control_rx, _background_rx) = test_client_writer(); + let (foreground_tx, foreground_control_rx, _foreground_rx) = test_client_writer(); + + server.clients.insert( + 1, + ClientConnection::new( + (120, 40), + crate::kitty_graphics::HostCellSize::default(), + crate::terminal_theme::TerminalTheme::default(), + None, + 1, + RenderEncoding::SemanticFrame, + Some(background_tx), + ), + ); + server.clients.insert( + 2, + ClientConnection::new( + (80, 24), + crate::kitty_graphics::HostCellSize::default(), + crate::terminal_theme::TerminalTheme::default(), + None, + 2, + RenderEncoding::SemanticFrame, + Some(foreground_tx), + ), + ); + server.foreground_client_id = Some(2); + server.sync_foreground_client_state(); + // Drain any setup messages (e.g. mouse-capture sync) before exercising the event. + while foreground_control_rx + .recv_timeout(Duration::from_millis(20)) + .is_ok() + {} + + let changed = server + .handle_internal_event_with_forwarding(AppEvent::PrefixInputSource { active: true }); + + assert!(changed); + match read_server_message( + foreground_control_rx + .recv_timeout(Duration::from_millis(100)) + .expect("foreground prefix input-source message"), + ) { + ServerMessage::PrefixInputSource { active } => assert!(active), + other => panic!("expected prefix input-source message, got {other:?}"), + } + assert!( + background_control_rx + .recv_timeout(Duration::from_millis(50)) + .is_err(), + "background client should not receive prefix input-source changes" + ); + } + + #[test] + fn headless_app_keeps_prefix_input_source_switch_off_process() { + // An App-internal drain (e.g. the exhaustive drain at the top of + // handle_api_request) can consume a queued PrefixInputSource intent + // before the forwarding drain sees it. The headless App must treat the + // event as inert instead of switching the host input source from the + // server process. + struct CountingPrefixInputSource(std::rc::Rc<std::cell::Cell<usize>>); + impl crate::platform::PrefixInputSource for CountingPrefixInputSource { + fn switch_to_ascii(&mut self) { + self.0.set(self.0.get() + 1); + } + fn restore(&mut self) { + self.0.set(self.0.get() + 1); + } + } + + let mut server = test_headless_server(); + let calls = std::rc::Rc::new(std::cell::Cell::new(0)); + server + .app + .set_prefix_input_source(Box::new(CountingPrefixInputSource(calls.clone()))); + + server + .app + .handle_internal_event(AppEvent::PrefixInputSource { active: true }); + server + .app + .handle_internal_event(AppEvent::PrefixInputSource { active: false }); + assert_eq!( + calls.get(), + 0, + "headless server must not apply the host input-source switch" + ); + + // Sanity: the same event does apply once the flag is on (monolithic semantics). + server.app.local_input_source_switch = true; + server + .app + .handle_internal_event(AppEvent::PrefixInputSource { active: true }); + assert_eq!(calls.get(), 1); + } + + #[test] fn client_local_notifications_target_foreground_client_only() { let mut server = test_headless_server(); let (background_tx, background_control_rx, _background_rx) = test_client_writer(); @@ -8470,7 +8584,8 @@ next_tab = "" } /// Verify that no direct calls to `self.app.handle_internal_event` - /// exist outside of `handle_internal_event_with_forwarding` in this + /// (or its `handle_internal_event_with_prefix_sync` wrapper) exist + /// outside of `handle_internal_event_with_forwarding` in this /// module. This ensures the forwarding bypass cannot be reintroduced. /// /// The search pattern looks for `handle_internal_event` calls that @@ -8508,7 +8623,8 @@ next_tab = "" _ => {} } } - } else if line.contains("self.app.handle_internal_event(") + } else if (line.contains("self.app.handle_internal_event(") + || line.contains("self.app.handle_internal_event_with_prefix_sync(")) && !line.trim().starts_with("///") && !line.contains("contains(") { |
