aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-07-14 01:06:11 +0300
committerGitHub <noreply@github.com>2026-07-14 01:06:11 +0300
commit2e5da968ea3fb7105c56f7674caf822de41ec176 (patch)
treee6d5d7dab9f7262c641a275536b5adf6ada0957d /src/app
parent3a8490f6515dfea13292ae28e34f1174d2f68af1 (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.rs23
-rw-r--r--src/app/mod.rs120
-rw-r--r--src/app/runtime.rs2
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
}