diff options
| -rw-r--r-- | src/app/actions.rs | 47 | ||||
| -rw-r--r-- | src/app/api.rs | 17 | ||||
| -rw-r--r-- | src/app/mod.rs | 5 | ||||
| -rw-r--r-- | src/app/runtime.rs | 8 | ||||
| -rw-r--r-- | src/app/state.rs | 4 | ||||
| -rw-r--r-- | src/client/mod.rs | 7 | ||||
| -rw-r--r-- | src/main.rs | 8 | ||||
| -rw-r--r-- | src/raw_input.rs | 19 | ||||
| -rw-r--r-- | src/server/headless.rs | 209 |
9 files changed, 296 insertions, 28 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs index 210b6ccf..83e9bec6 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -15,8 +15,15 @@ fn is_background_completion_transition(prev_state: AgentState, new_state: AgentS && matches!(prev_state, AgentState::Working | AgentState::Blocked) } -pub fn notification_sound_for_state_change( +pub fn active_tab_suppresses_notifications( is_active_tab: bool, + outer_terminal_focus: Option<bool>, +) -> bool { + is_active_tab && outer_terminal_focus != Some(false) +} + +pub fn notification_sound_for_state_change( + suppress_active_tab_notifications: bool, prev_state: AgentState, new_state: AgentState, ) -> Option<crate::sound::Sound> { @@ -27,7 +34,8 @@ pub fn notification_sound_for_state_change( match new_state { AgentState::Blocked => Some(crate::sound::Sound::Request), AgentState::Idle - if is_background_completion_transition(prev_state, new_state) && !is_active_tab => + if is_background_completion_transition(prev_state, new_state) + && !suppress_active_tab_notifications => { Some(crate::sound::Sound::Done) } @@ -36,11 +44,11 @@ pub fn notification_sound_for_state_change( } pub fn notification_toast_for_state_change( - is_active_tab: bool, + suppress_active_tab_notifications: bool, prev_state: AgentState, new_state: AgentState, ) -> Option<ToastKind> { - if is_active_tab || new_state == prev_state { + if suppress_active_tab_notifications || new_state == prev_state { return None; } @@ -573,6 +581,8 @@ impl AppState { change: &EffectiveStateChange, ) { let is_active_tab = self.pane_is_in_active_tab(ws_idx, pane_id); + let suppress_active_tab_notifications = + active_tab_suppresses_notifications(is_active_tab, self.outer_terminal_focus); let Some(pane) = self.workspaces[ws_idx] .tabs .iter_mut() @@ -582,14 +592,14 @@ impl AppState { }; if is_background_completion_transition(change.previous_state, change.state) - && !is_active_tab + && !suppress_active_tab_notifications { pane.seen = false; } if self.local_sound_playback && self.sound.allows(change.known_agent) { if let Some(sound) = notification_sound_for_state_change( - is_active_tab, + suppress_active_tab_notifications, change.previous_state, change.state, ) { @@ -1066,6 +1076,31 @@ mod tests { } #[test] + fn active_workspace_active_tab_keeps_herdr_toast_suppressed_when_outer_terminal_is_unfocused() { + let mut state = app_with_workspaces(&["active"]); + state.active = Some(0); + state.outer_terminal_focus = Some(false); + state.toast_config.delivery = crate::config::ToastDelivery::Herdr; + let pane_id = *state.workspaces[0].panes.keys().next().unwrap(); + + state.handle_app_event(AppEvent::StateChanged { + pane_id, + agent: Some(Agent::Pi), + state: AgentState::Blocked, + }); + + assert!(state.toast.is_none()); + } + + #[test] + fn active_tab_suppression_preserves_unknown_focus_behavior() { + assert!(active_tab_suppresses_notifications(true, None)); + assert!(active_tab_suppresses_notifications(true, Some(true))); + assert!(!active_tab_suppresses_notifications(true, Some(false))); + assert!(!active_tab_suppresses_notifications(false, None)); + } + + #[test] fn update_ready_sets_manual_update_toast() { let mut state = AppState::test_new(); state.toast_config.delivery = crate::config::ToastDelivery::Herdr; diff --git a/src/app/api.rs b/src/app/api.rs index d7893d6d..5cfa3409 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -70,10 +70,12 @@ impl App { self.restore_overlay_after_exit(overlay); } - if matches!( - self.state.toast_config.delivery, - crate::config::ToastDelivery::Terminal - ) { + if self.local_terminal_notifications + && matches!( + self.state.toast_config.delivery, + crate::config::ToastDelivery::Terminal + ) + { if let Some(version) = update_ready_version { let _ = crate::terminal_notify::show_notification( &format!("v{version} available"), @@ -84,8 +86,13 @@ impl App { let is_active_tab = self .state .pane_is_in_active_tab(update.ws_idx, update.pane_id); + let suppress_active_tab_notifications = + crate::app::actions::active_tab_suppresses_notifications( + is_active_tab, + self.state.outer_terminal_focus, + ); let Some(kind) = crate::app::actions::notification_toast_for_state_change( - is_active_tab, + suppress_active_tab_notifications, update.previous_state, update.state, ) else { diff --git a/src/app/mod.rs b/src/app/mod.rs index 26090ebf..3d588403 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -77,6 +77,7 @@ pub struct App { pub render_notify: Arc<Notify>, pub render_dirty: Arc<AtomicBool>, pub(crate) overlay_panes: HashMap<crate::layout::PaneId, OverlayPaneState>, + pub(crate) local_terminal_notifications: bool, } pub(crate) enum LoopEvent { @@ -315,6 +316,7 @@ impl App { update_dismissed: false, config_diagnostic, toast: None, + outer_terminal_focus: None, prefix_code, prefix_mods, default_sidebar_width: config.ui.sidebar_width, @@ -395,6 +397,7 @@ impl App { render_notify, render_dirty, overlay_panes: HashMap::new(), + local_terminal_notifications: true, } } @@ -761,6 +764,8 @@ impl App { } } } + crate::raw_input::RawInputEvent::OuterFocusGained + | crate::raw_input::RawInputEvent::OuterFocusLost => {} crate::raw_input::RawInputEvent::HostDefaultColor { kind, color } => { if apply_host_terminal_theme { self.update_host_terminal_theme(kind, color); diff --git a/src/app/runtime.rs b/src/app/runtime.rs index 739a3670..094b26f1 100644 --- a/src/app/runtime.rs +++ b/src/app/runtime.rs @@ -89,6 +89,14 @@ impl App { self.handle_mouse(mouse); true } + crate::raw_input::RawInputEvent::OuterFocusGained => { + self.state.outer_terminal_focus = Some(true); + false + } + crate::raw_input::RawInputEvent::OuterFocusLost => { + self.state.outer_terminal_focus = Some(false); + false + } crate::raw_input::RawInputEvent::HostDefaultColor { kind, color } => { self.update_host_terminal_theme(kind, color) } diff --git a/src/app/state.rs b/src/app/state.rs index 971b1476..facb5187 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -631,6 +631,9 @@ pub struct AppState { pub update_dismissed: bool, pub config_diagnostic: Option<String>, pub toast: Option<ToastNotification>, + /// Last reported focus state for the outer terminal hosting herdr. + /// None means unsupported or not yet reported, which preserves active-pane suppression. + pub outer_terminal_focus: Option<bool>, // Config pub prefix_code: KeyCode, pub prefix_mods: KeyModifiers, @@ -765,6 +768,7 @@ impl AppState { update_dismissed: false, config_diagnostic: None, toast: None, + outer_terminal_focus: None, prefix_code: KeyCode::Char('b'), prefix_mods: KeyModifiers::CONTROL, default_sidebar_width: 26, diff --git a/src/client/mod.rs b/src/client/mod.rs index 19d07bfb..f95d0fd4 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -22,8 +22,9 @@ use std::sync::Arc; use std::time::Duration; use crossterm::event::{ - DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, - KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, + DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, + EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, + PushKeyboardEnhancementFlags, }; use crossterm::execute; use tracing::{debug, info, warn}; @@ -136,6 +137,7 @@ fn setup_terminal() -> io::Result<TerminalGuard> { io::stdout(), EnableMouseCapture, EnableBracketedPaste, + EnableFocusChange, PushKeyboardEnhancementFlags( KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_EVENT_TYPES @@ -175,6 +177,7 @@ fn restore_terminal_state(in_tmux: bool) { let _ = execute!( io::stdout(), PopKeyboardEnhancementFlags, + DisableFocusChange, DisableBracketedPaste, DisableMouseCapture ); diff --git a/src/main.rs b/src/main.rs index 1d8439b9..dbc77060 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ use std::io; use crossterm::event::{ - DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, - KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, + DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, + EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, + PushKeyboardEnhancementFlags, }; use crossterm::execute; @@ -342,6 +343,7 @@ fn main() -> io::Result<()> { let _ = execute!( io::stdout(), PopKeyboardEnhancementFlags, + DisableFocusChange, DisableBracketedPaste, DisableMouseCapture ); @@ -368,6 +370,7 @@ fn main() -> io::Result<()> { io::stdout(), EnableMouseCapture, EnableBracketedPaste, + EnableFocusChange, PushKeyboardEnhancementFlags( KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_EVENT_TYPES @@ -406,6 +409,7 @@ fn main() -> io::Result<()> { execute!( io::stdout(), PopKeyboardEnhancementFlags, + DisableFocusChange, DisableBracketedPaste, DisableMouseCapture )?; diff --git a/src/raw_input.rs b/src/raw_input.rs index a98cf25d..c2a412b2 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -125,6 +125,8 @@ pub enum RawInputEvent { Key(TerminalKey), Paste(String), Mouse(MouseEvent), + OuterFocusGained, + OuterFocusLost, HostDefaultColor { kind: DefaultColorKind, color: RgbColor, @@ -297,6 +299,12 @@ fn extract_one_event(buffer: &[u8]) -> Option<(RawInputEvent, usize)> { return Some((RawInputEvent::HostDefaultColor { kind, color }, seq_len)); } + match seq { + "\x1b[I" => return Some((RawInputEvent::OuterFocusGained, seq_len)), + "\x1b[O" => return Some((RawInputEvent::OuterFocusLost, seq_len)), + _ => {} + } + if let Some(mouse) = parse_sgr_mouse(seq) { return Some((RawInputEvent::Mouse(mouse), seq_len)); } @@ -604,6 +612,17 @@ mod tests { } #[test] + fn parses_outer_focus_events() { + let (event, consumed) = extract_one_event(b"\x1b[I").unwrap(); + assert_eq!(consumed, 3); + assert!(matches!(event, RawInputEvent::OuterFocusGained)); + + let (event, consumed) = extract_one_event(b"\x1b[O").unwrap(); + assert_eq!(consumed, 3); + assert!(matches!(event, RawInputEvent::OuterFocusLost)); + } + + #[test] fn parses_xterm_alt_up_arrow() { let (RawInputEvent::Key(key), consumed) = extract_one_event(b"\x1b[1;3A").unwrap() else { panic!("expected key"); diff --git a/src/server/headless.rs b/src/server/headless.rs index cffffe6a..5f9aad2c 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -117,12 +117,15 @@ fn toast_event_text(kind: app::state::ToastKind) -> &'static str { fn toast_message_from_state_change( state: &AppState, pane_id: PaneId, - is_active_tab: bool, + suppress_active_tab_notifications: bool, prev_state: AgentState, new_state: AgentState, ) -> Option<String> { - let kind = - app::actions::notification_toast_for_state_change(is_active_tab, prev_state, new_state)?; + let kind = app::actions::notification_toast_for_state_change( + suppress_active_tab_notifications, + prev_state, + new_state, + )?; state .workspaces @@ -211,6 +214,8 @@ struct ClientConnection { terminal_size: (u16, u16), /// 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, /// Last frame sent to this client. Used to skip identical frame sends. @@ -615,20 +620,35 @@ impl HeadlessServer { fn sync_foreground_client_state(&mut self) { let Some(client_id) = self.foreground_client_id else { self.effective_size = (MIN_COLS, MIN_ROWS); + self.app.state.outer_terminal_focus = None; return; }; let Some(client) = self.clients.get(&client_id) else { self.foreground_client_id = None; self.effective_size = (MIN_COLS, MIN_ROWS); + self.app.state.outer_terminal_focus = None; return; }; self.effective_size = client.terminal_size; + self.app.state.outer_terminal_focus = client.outer_terminal_focus; if !client.host_terminal_theme.is_empty() { self.app.set_host_terminal_theme(client.host_terminal_theme); } } + fn foreground_client_outer_focus(&self) -> Option<bool> { + let client_id = self.foreground_client_id?; + self.clients.get(&client_id)?.outer_terminal_focus + } + + fn active_tab_suppresses_notifications(&self, is_active_tab: bool) -> bool { + crate::app::actions::active_tab_suppresses_notifications( + is_active_tab, + self.foreground_client_outer_focus(), + ) + } + fn promote_client_to_foreground(&mut self, client_id: u64) -> bool { let stamp = self.allocate_activity_stamp(); let Some(client) = self.clients.get_mut(&client_id) else { @@ -692,6 +712,32 @@ impl HeadlessServer { } } + fn update_client_outer_focus_from_events( + &mut self, + 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, + }) + .last(); + + let Some(next_focus) = next_focus else { + return; + }; + let Some(client) = self.clients.get_mut(&client_id) 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!( @@ -699,6 +745,7 @@ impl HeadlessServer { crate::raw_input::RawInputEvent::Key(_) | crate::raw_input::RawInputEvent::Mouse(_) | crate::raw_input::RawInputEvent::Paste(_) + | crate::raw_input::RawInputEvent::OuterFocusGained ) }) } @@ -804,6 +851,7 @@ impl HeadlessServer { // Handle the state change (updates pane state, sets toast on AppState). // Headless mode disables local sound playback separately from the // sound policy so reloads can keep server-side notification policy live. + self.sync_foreground_client_state(); self.app.handle_internal_event(ev); // Forward sound notification to clients when server-side sound policy allows it. @@ -817,9 +865,12 @@ impl HeadlessServer { .is_some_and(|tab_idx| ws.active_tab_index() == tab_idx) }); + let suppress_active_tab_notifications = + self.active_tab_suppresses_notifications(is_active_tab); + if self.app.state.sound.allows(agent_val) { if let Some(sound) = crate::app::actions::notification_sound_for_state_change( - is_active_tab, + suppress_active_tab_notifications, prev_state, state_val, ) { @@ -846,7 +897,7 @@ impl HeadlessServer { toast_message_from_state_change( &self.app.state, pane_id_val, - is_active_tab, + suppress_active_tab_notifications, prev_state, state_val, ) @@ -897,6 +948,7 @@ impl HeadlessServer { }) .unwrap_or(crate::detect::AgentState::Unknown); + self.sync_foreground_client_state(); self.app.handle_internal_event(ev); // Forward sound notification based on hook state transition when @@ -913,9 +965,12 @@ impl HeadlessServer { .is_some_and(|tab_idx| ws.active_tab_index() == tab_idx) }); + let suppress_active_tab_notifications = + self.active_tab_suppresses_notifications(is_active_tab); + if self.app.state.sound.allows(agent_val) { if let Some(sound) = crate::app::actions::notification_sound_for_state_change( - is_active_tab, + suppress_active_tab_notifications, prev_hook_state, hook_state_val, ) { @@ -942,7 +997,7 @@ impl HeadlessServer { toast_message_from_state_change( &self.app.state, pane_id_val, - is_active_tab, + suppress_active_tab_notifications, prev_hook_state, hook_state_val, ) @@ -1123,6 +1178,7 @@ impl HeadlessServer { ClientConnection { terminal_size: (cols, rows), host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, last_activity, last_frame: None, writer: Some(writer), @@ -1143,6 +1199,7 @@ impl HeadlessServer { client.last_frame = None; } let events = crate::raw_input::parse_raw_input_bytes_sync(&data); + self.update_client_outer_focus_from_events(client_id, &events); let interaction = Self::events_include_interaction(&events); let foreground_changed = if interaction { self.promote_client_to_foreground(client_id) @@ -1301,6 +1358,7 @@ impl HeadlessServer { }) .collect(); + self.sync_foreground_client_state(); let response = self.app.handle_api_request(msg.request); let _ = msg.respond_to.send(response); @@ -1370,6 +1428,8 @@ impl HeadlessServer { } let is_active_tab = self.app.state.pane_is_in_active_tab(*ws_idx, *pane_id); + let suppress_active_tab_notifications = + self.active_tab_suppresses_notifications(is_active_tab); // Get the known agent for sound settings. Unknown custom labels // fall back to None so clients use the generic sound behavior. @@ -1390,7 +1450,7 @@ impl HeadlessServer { && should_forward_toast_to_clients(self.app.state.toast_config.delivery) { if let Some(kind) = crate::app::actions::notification_toast_for_state_change( - is_active_tab, + suppress_active_tab_notifications, prev_state, new_state, ) { @@ -1422,7 +1482,7 @@ impl HeadlessServer { // Clients still decide locally whether they can execute the side effect. if self.app.state.sound.allows(agent) { if let Some(sound) = crate::app::actions::notification_sound_for_state_change( - is_active_tab, + suppress_active_tab_notifications, prev_state, new_state, ) { @@ -1965,10 +2025,11 @@ pub fn run_server() -> io::Result<()> { event_hub, ); - // The server runs headless — disable local sound playback. - // Sound notifications are forwarded to connected clients as - // ServerMessage::Notify instead of played locally. + // 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. app.state.local_sound_playback = false; + app.local_terminal_notifications = false; // Create the headless server. let mut server = match HeadlessServer::new(app) { @@ -2013,7 +2074,10 @@ mod tests { fn test_headless_server() -> HeadlessServer { let config = crate::config::Config::default(); let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); - let app = crate::app::App::new(&config, true, None, None, api_rx, api::EventHub::default()); + let mut app = + crate::app::App::new(&config, true, None, None, api_rx, api::EventHub::default()); + app.state.local_sound_playback = false; + app.local_terminal_notifications = false; let dir = std::env::temp_dir().join(format!( "hh-{}-{}", @@ -2255,6 +2319,7 @@ mod tests { b: 0x33, }), }, + outer_terminal_focus: None, last_activity: 1, last_frame: None, writer: None, @@ -2276,6 +2341,7 @@ mod tests { b: 0xff, }), }, + outer_terminal_focus: None, last_activity: 2, last_frame: None, writer: None, @@ -2308,6 +2374,114 @@ mod tests { } #[test] + fn focus_lost_updates_client_without_promoting_foreground() { + let mut server = test_headless_server(); + + server.clients.insert( + 1, + ClientConnection { + terminal_size: (120, 40), + host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, + last_activity: 1, + last_frame: None, + writer: None, + }, + ); + server.clients.insert( + 2, + ClientConnection { + terminal_size: (80, 24), + host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: Some(true), + last_activity: 2, + last_frame: None, + writer: None, + }, + ); + server.foreground_client_id = Some(2); + server.sync_foreground_client_state(); + + let changed = server.handle_server_event(ServerEvent::ClientInput { + client_id: 1, + data: b"\x1b[O".to_vec(), + }); + + assert!(!changed); + assert_eq!(server.foreground_client_id, Some(2)); + assert_eq!(server.clients[&1].outer_terminal_focus, Some(false)); + assert_eq!(server.app.state.outer_terminal_focus, Some(true)); + } + + #[test] + fn focus_gained_promotes_client_to_foreground() { + let mut server = test_headless_server(); + + server.clients.insert( + 1, + ClientConnection { + terminal_size: (120, 40), + host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, + last_activity: 1, + last_frame: None, + writer: None, + }, + ); + server.clients.insert( + 2, + ClientConnection { + terminal_size: (80, 24), + host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: Some(true), + last_activity: 2, + last_frame: None, + writer: None, + }, + ); + server.foreground_client_id = Some(2); + server.sync_foreground_client_state(); + + let changed = server.handle_server_event(ServerEvent::ClientInput { + client_id: 1, + data: b"\x1b[I".to_vec(), + }); + + assert!(changed); + assert_eq!(server.foreground_client_id, Some(1)); + assert_eq!(server.clients[&1].outer_terminal_focus, Some(true)); + assert_eq!(server.app.state.outer_terminal_focus, Some(true)); + } + + #[test] + fn foreground_client_focus_event_updates_app_focus_state() { + let mut server = test_headless_server(); + + server.clients.insert( + 1, + ClientConnection { + terminal_size: (120, 40), + host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: Some(true), + last_activity: 1, + last_frame: None, + writer: None, + }, + ); + server.foreground_client_id = Some(1); + server.sync_foreground_client_state(); + + let changed = server.handle_server_event(ServerEvent::ClientInput { + client_id: 1, + data: b"\x1b[O".to_vec(), + }); + + assert!(!changed); + assert_eq!(server.clients[&1].outer_terminal_focus, Some(false)); + assert_eq!(server.app.state.outer_terminal_focus, Some(false)); + } + + #[test] fn render_and_stream_uses_each_client_terminal_size() { let mut server = test_headless_server(); server.app.state.workspaces = vec![crate::workspace::Workspace::test_new("test")]; @@ -2323,6 +2497,7 @@ mod tests { ClientConnection { terminal_size: (120, 40), host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, last_activity: 1, last_frame: None, writer: Some(desktop_tx), @@ -2333,6 +2508,7 @@ mod tests { ClientConnection { terminal_size: (80, 24), host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, last_activity: 2, last_frame: None, writer: Some(phone_tx), @@ -2366,6 +2542,7 @@ mod tests { ClientConnection { terminal_size: (80, 24), host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, last_activity: 1, last_frame: None, writer: Some(client_tx), @@ -2396,6 +2573,7 @@ mod tests { ClientConnection { terminal_size: (80, 24), host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, last_activity: 1, last_frame: None, writer: Some(client_tx), @@ -2427,6 +2605,7 @@ mod tests { ClientConnection { terminal_size: (120, 40), host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, last_activity: 1, last_frame: None, writer: Some(background_tx), @@ -2437,6 +2616,7 @@ mod tests { ClientConnection { terminal_size: (80, 24), host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, last_activity: 2, last_frame: None, writer: Some(foreground_tx), @@ -2477,6 +2657,7 @@ mod tests { ClientConnection { terminal_size: (120, 40), host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, last_activity: 1, last_frame: None, writer: Some(background_tx), @@ -2487,6 +2668,7 @@ mod tests { ClientConnection { terminal_size: (80, 24), host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, last_activity: 2, last_frame: None, writer: Some(foreground_tx), @@ -2529,6 +2711,7 @@ mod tests { ClientConnection { terminal_size: (80, 24), host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), + outer_terminal_focus: None, last_activity: 1, last_frame: None, writer: Some(client_tx), |
