feat: forward ghostty pane focus events on focus changes

This commit is contained in:
Can Celik 2026-04-05 02:24:57 +03:00
parent 19239c6c87
commit a08dfaef2f
4 changed files with 99 additions and 0 deletions

View File

@ -3366,6 +3366,7 @@ mod tests {
alternate_screen: true,
application_cursor: false,
bracketed_paste: false,
focus_reporting: false,
mouse_protocol_mode: vt100::MouseProtocolMode::ButtonMotion,
mouse_protocol_encoding: vt100::MouseProtocolEncoding::Sgr,
mouse_alternate_scroll: true,
@ -3380,6 +3381,7 @@ mod tests {
alternate_screen: true,
application_cursor: false,
bracketed_paste: false,
focus_reporting: false,
mouse_protocol_mode: vt100::MouseProtocolMode::None,
mouse_protocol_encoding: vt100::MouseProtocolEncoding::Default,
mouse_alternate_scroll: true,
@ -3394,6 +3396,7 @@ mod tests {
alternate_screen: false,
application_cursor: false,
bracketed_paste: false,
focus_reporting: false,
mouse_protocol_mode: vt100::MouseProtocolMode::None,
mouse_protocol_encoding: vt100::MouseProtocolEncoding::Default,
mouse_alternate_scroll: true,

View File

@ -738,7 +738,13 @@ impl App {
return;
}
#[cfg(feature = "ghostty-vt")]
if let Some((ws_idx, pane_id)) = self.last_focus {
self.send_pane_focus_event(ws_idx, pane_id, crate::ghostty::FocusEvent::Lost);
}
if let Some((ws_idx, pane_id)) = current_focus {
#[cfg(feature = "ghostty-vt")]
self.send_pane_focus_event(ws_idx, pane_id, crate::ghostty::FocusEvent::Gained);
self.emit_event(crate::api::schema::EventEnvelope {
event: crate::api::schema::EventKind::WorkspaceFocused,
data: crate::api::schema::EventData::WorkspaceFocused {
@ -770,6 +776,24 @@ impl App {
self.last_focus = current_focus;
}
#[cfg(feature = "ghostty-vt")]
fn send_pane_focus_event(
&self,
ws_idx: usize,
pane_id: crate::layout::PaneId,
event: crate::ghostty::FocusEvent,
) {
let Some(runtime) = self
.state
.workspaces
.get(ws_idx)
.and_then(|ws| ws.runtime(pane_id))
else {
return;
};
runtime.try_send_focus_event(event);
}
fn find_pane(
&self,
pane_id: crate::layout::PaneId,

View File

@ -121,6 +121,7 @@ pub const MOUSE_BUTTON_WHEEL_RIGHT: ffi::GhosttyMouseButton =
ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_SEVEN;
pub const MODE_APPLICATION_CURSOR_KEYS: u16 = 1;
pub const MODE_FOCUS_EVENT: u16 = 1004;
pub const MODE_BRACKETED_PASTE: u16 = 2004;
pub const MODE_MOUSE_UTF8: u16 = 1005;
pub const MODE_MOUSE_SGR: u16 = 1006;

View File

@ -272,6 +272,7 @@ pub struct InputState {
pub alternate_screen: bool,
pub application_cursor: bool,
pub bracketed_paste: bool,
pub focus_reporting: bool,
pub mouse_protocol_mode: vt100::MouseProtocolMode,
pub mouse_protocol_encoding: vt100::MouseProtocolEncoding,
pub mouse_alternate_scroll: bool,
@ -593,6 +594,7 @@ impl VtPaneTerminal {
alternate_screen: screen.alternate_screen(),
application_cursor: screen.application_cursor(),
bracketed_paste: screen.bracketed_paste(),
focus_reporting: false,
mouse_protocol_mode: screen.mouse_protocol_mode(),
mouse_protocol_encoding: screen.mouse_protocol_encoding(),
mouse_alternate_scroll: self.mouse_alternate_scroll.load(Ordering::Relaxed),
@ -772,6 +774,10 @@ impl GhosttyPaneTerminal {
.terminal
.mode_get(crate::ghostty::MODE_BRACKETED_PASTE)
.ok()?;
let focus_reporting = core
.terminal
.mode_get(crate::ghostty::MODE_FOCUS_EVENT)
.ok()?;
let mouse_sgr = core.terminal.mode_get(crate::ghostty::MODE_MOUSE_SGR).ok()?;
let mouse_utf8 = core.terminal.mode_get(crate::ghostty::MODE_MOUSE_UTF8).ok()?;
let mouse_alternate_scroll = core
@ -800,6 +806,7 @@ impl GhosttyPaneTerminal {
alternate_screen,
application_cursor,
bracketed_paste,
focus_reporting,
mouse_protocol_mode,
mouse_protocol_encoding,
mouse_alternate_scroll,
@ -2008,6 +2015,25 @@ impl PaneRuntime {
self.send_bytes(Bytes::from(payload)).await
}
#[cfg(feature = "ghostty-vt")]
pub fn try_send_focus_event(&self, event: crate::ghostty::FocusEvent) -> bool {
if !self
.input_state()
.map(|state| state.focus_reporting)
.unwrap_or(false)
{
return false;
}
let Ok(bytes) = crate::ghostty::encode_focus(event) else {
return false;
};
if let Err(err) = self.try_send_bytes(Bytes::from(bytes)) {
warn!(err = %err, ?event, "failed to forward pane focus event");
}
true
}
pub fn wheel_routing(&self) -> Option<WheelRouting> {
let input_state = self.input_state()?;
Some(if input_state.mouse_reporting_enabled() {
@ -2183,6 +2209,51 @@ mod tests {
assert_eq!(encoded.as_deref(), Some(&b"\x1b[<36;5;7M"[..]));
}
#[cfg(feature = "ghostty-vt")]
#[tokio::test]
async fn focus_events_are_forwarded_when_enabled() {
let (tx, mut rx) = mpsc::channel(4);
let (resize_tx, _resize_rx) = mpsc::channel(1);
let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
terminal.mode_set(crate::ghostty::MODE_FOCUS_EVENT, true).unwrap();
let runtime = PaneRuntime {
terminal: Arc::new(PaneTerminal::Ghostty(GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap())),
sender: tx,
resize_tx,
current_size: Cell::new((80, 24)),
child_pid: Arc::new(AtomicU32::new(0)),
kitty_keyboard_flags: Arc::new(AtomicU16::new(0)),
detect_reset_notify: Arc::new(Notify::new()),
pending_release: Arc::new(Mutex::new(None)),
detect_handle: tokio::spawn(async {}).abort_handle(),
};
assert!(runtime.try_send_focus_event(crate::ghostty::FocusEvent::Gained));
assert_eq!(rx.recv().await.unwrap(), Bytes::from_static(b"\x1b[I"));
}
#[cfg(feature = "ghostty-vt")]
#[tokio::test]
async fn focus_events_are_suppressed_when_disabled() {
let (tx, mut rx) = mpsc::channel(4);
let (resize_tx, _resize_rx) = mpsc::channel(1);
let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
let runtime = PaneRuntime {
terminal: Arc::new(PaneTerminal::Ghostty(GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap())),
sender: tx,
resize_tx,
current_size: Cell::new((80, 24)),
child_pid: Arc::new(AtomicU32::new(0)),
kitty_keyboard_flags: Arc::new(AtomicU16::new(0)),
detect_reset_notify: Arc::new(Notify::new()),
pending_release: Arc::new(Mutex::new(None)),
detect_handle: tokio::spawn(async {}).abort_handle(),
};
assert!(!runtime.try_send_focus_event(crate::ghostty::FocusEvent::Gained));
assert!(tokio::time::timeout(std::time::Duration::from_millis(10), rx.recv()).await.is_err());
}
#[test]
fn recent_text_reconstructs_scrollback_tail() {
let responses = PtyResponses::new();