diff options
| author | akbash <akbash@herdr.dev> | 2026-07-14 01:06:11 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-14 01:06:11 +0300 |
| commit | 2e5da968ea3fb7105c56f7674caf822de41ec176 (patch) | |
| tree | e6d5d7dab9f7262c641a275536b5adf6ada0957d /src/app | |
| parent | 3a8490f6515dfea13292ae28e34f1174d2f68af1 (diff) | |
fix: forward outer focus events to panes (#1388)
* fix: forward outer focus events to panes
refs #1337
* fix: preserve ordered focus forwarding
refs #1337
---------
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/api.rs | 23 | ||||
| -rw-r--r-- | src/app/mod.rs | 120 | ||||
| -rw-r--r-- | src/app/runtime.rs | 2 |
3 files changed, 142 insertions, 3 deletions
diff --git a/src/app/api.rs b/src/app/api.rs index c013d271..d6d657b7 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -765,6 +765,17 @@ impl App { } pub(crate) fn sync_focus_events(&mut self) { + self.sync_focus_events_with_outer_event(None); + } + + pub(super) fn send_outer_focus_event(&mut self, event: crate::ghostty::FocusEvent) { + self.sync_focus_events_with_outer_event(Some(event)); + } + + fn sync_focus_events_with_outer_event( + &mut self, + outer_event: Option<crate::ghostty::FocusEvent>, + ) { let current_focus = self.state.active.and_then(|idx| { self.state .workspaces @@ -772,6 +783,9 @@ impl App { .and_then(|ws| ws.focused_pane_id().map(|pane_id| (idx, pane_id))) }); if current_focus == self.last_focus { + if let (Some((ws_idx, pane_id)), Some(event)) = (current_focus, outer_event) { + self.send_pane_focus_event(ws_idx, pane_id, event); + } return; } @@ -779,7 +793,14 @@ impl App { self.send_pane_focus_event(ws_idx, pane_id, crate::ghostty::FocusEvent::Lost); } if let Some((ws_idx, pane_id)) = current_focus { - self.send_pane_focus_event(ws_idx, pane_id, crate::ghostty::FocusEvent::Gained); + let event = outer_event.unwrap_or_else(|| { + if self.state.outer_terminal_focus == Some(false) { + crate::ghostty::FocusEvent::Lost + } else { + crate::ghostty::FocusEvent::Gained + } + }); + self.send_pane_focus_event(ws_idx, pane_id, event); self.emit_event(crate::api::schema::EventEnvelope { event: crate::api::schema::EventKind::WorkspaceFocused, data: crate::api::schema::EventData::WorkspaceFocused { diff --git a/src/app/mod.rs b/src/app/mod.rs index 16cfeafb..6a7470be 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1617,8 +1617,12 @@ impl App { } } } - crate::raw_input::RawInputEvent::OuterFocusGained - | crate::raw_input::RawInputEvent::OuterFocusLost => {} + crate::raw_input::RawInputEvent::OuterFocusGained => { + self.send_outer_focus_event(crate::ghostty::FocusEvent::Gained); + } + crate::raw_input::RawInputEvent::OuterFocusLost => { + self.send_outer_focus_event(crate::ghostty::FocusEvent::Lost); + } crate::raw_input::RawInputEvent::HostDefaultColor { kind, color } => { if apply_host_terminal_theme { self.update_host_terminal_theme(kind, color); @@ -3263,6 +3267,118 @@ mod tests { } #[tokio::test] + async fn monolithic_outer_focus_events_reach_reporting_pane() { + let mut app = test_app(); + let mut workspace = Workspace::test_new("focus-reporting"); + let pane_id = workspace.tabs[0].root_pane; + let (runtime, mut input_rx) = + crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes( + 80, + 24, + 0, + b"\x1b[?1004h", + 4, + ); + workspace.insert_test_runtime(pane_id, runtime); + app.state.workspaces = vec![workspace]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + + assert!( + app.handle_raw_input_event(crate::raw_input::RawInputEvent::OuterFocusGained) + .await + ); + assert_eq!( + input_rx + .recv() + .await + .expect("forwarded focus gained report"), + bytes::Bytes::from_static(b"\x1b[I") + ); + + assert!( + !app.handle_raw_input_event(crate::raw_input::RawInputEvent::OuterFocusLost) + .await + ); + assert_eq!( + input_rx.recv().await.expect("forwarded focus lost report"), + bytes::Bytes::from_static(b"\x1b[O") + ); + } + + #[tokio::test] + async fn outer_focus_events_reconcile_pending_pane_focus() { + let mut app = test_app(); + let mut workspace = Workspace::test_new("focus-transition"); + let previous_pane = workspace.tabs[0].root_pane; + let next_pane = workspace.test_split(ratatui::layout::Direction::Horizontal); + workspace.tabs[0].layout.focus_pane(previous_pane); + let (runtime, mut input_rx) = + crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes( + 80, + 24, + 0, + b"\x1b[?1004h", + 4, + ); + workspace.insert_test_runtime(next_pane, runtime); + app.state.workspaces = vec![workspace]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + app.last_focus = Some((0, previous_pane)); + + assert!(app.state.focus_pane_in_workspace(0, next_pane)); + assert!( + !app.handle_raw_input_event(crate::raw_input::RawInputEvent::OuterFocusLost) + .await + ); + app.sync_focus_events(); + assert_eq!( + input_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"\x1b[O") + ); + assert!(input_rx.try_recv().is_err()); + + assert!(app.state.focus_pane_in_workspace(0, previous_pane)); + app.sync_focus_events(); + assert_eq!( + input_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"\x1b[O") + ); + + assert!(app.state.focus_pane_in_workspace(0, next_pane)); + app.sync_focus_events(); + assert_eq!( + input_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"\x1b[O") + ); + + assert!( + app.handle_raw_input_event(crate::raw_input::RawInputEvent::OuterFocusGained) + .await + ); + assert_eq!( + input_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"\x1b[I") + ); + + assert!(app.state.focus_pane_in_workspace(0, previous_pane)); + app.sync_focus_events(); + assert_eq!( + input_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"\x1b[O") + ); + assert!(app.state.focus_pane_in_workspace(0, next_pane)); + app.sync_focus_events(); + assert_eq!( + input_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"\x1b[I") + ); + } + + #[tokio::test] async fn repeat_key_events_are_ignored_outside_terminal_mode() { let mut app = test_app(); app.state.mode = Mode::ReleaseNotes; diff --git a/src/app/runtime.rs b/src/app/runtime.rs index 25275cd7..6d63f139 100644 --- a/src/app/runtime.rs +++ b/src/app/runtime.rs @@ -180,6 +180,7 @@ impl App { true } crate::raw_input::RawInputEvent::OuterFocusGained => { + self.send_outer_focus_event(crate::ghostty::FocusEvent::Gained); if self.state.redraw_on_focus_gained { self.request_full_redraw(); } @@ -188,6 +189,7 @@ impl App { true } crate::raw_input::RawInputEvent::OuterFocusLost => { + self.send_outer_focus_event(crate::ghostty::FocusEvent::Lost); self.state.outer_terminal_focus = Some(false); false } |
