aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/dinggerd/src')
-rw-r--r--crates/dinggerd/src/terminals.rs53
1 files changed, 38 insertions, 15 deletions
diff --git a/crates/dinggerd/src/terminals.rs b/crates/dinggerd/src/terminals.rs
index 413ba0d..85ae398 100644
--- a/crates/dinggerd/src/terminals.rs
+++ b/crates/dinggerd/src/terminals.rs
@@ -12,6 +12,11 @@
//! still focused after Ghostty answered, so a fast switch cannot pair the
//! wrong terminal.
//!
+//! Ghostty's answer lags tmux's focus: across a Space switch (a full screen
+//! window) tmux sees the new client focused about 0.3 s before Ghostty's
+//! front window changes, and meanwhile Ghostty names the terminal that had
+//! focus. So Ghostty is asked only once focus has settled (`SETTLE`).
+//!
//! Apple Events go out from children of the tmux server (the focus hook, and
//! `tmux run-shell` for raising), so macOS attributes them to the terminal
//! that started tmux: Ghostty scripting itself needs no permission.
@@ -112,11 +117,33 @@ fn pairings() -> Vec<(String, Pairing)> {
.collect()
}
-fn client_focused(tty: &str) -> bool {
- tmux::run(&["list-clients", "-F", "#{client_tty}\t#{client_flags}"])
- .unwrap_or_default()
- .lines()
- .any(|line| line.split_once('\t').is_some_and(|(t, flags)| t == tty && flags.split(',').any(|f| f == "focused")))
+/// The pairing options of the attached clients, and whether `tty` is focused.
+fn attached(tty: &str) -> (Vec<String>, bool) {
+ let mut names = Vec::new();
+ let mut focused = false;
+ for line in tmux::run(&["list-clients", "-F", "#{client_tty}\t#{client_flags}"]).unwrap_or_default().lines() {
+ if let Some((t, flags)) = line.split_once('\t') {
+ names.push(option_name(t));
+ focused |= t == tty && flags.split(',').any(|f| f == "focused");
+ }
+ }
+ (names, focused)
+}
+
+/// How long after a focus-in Ghostty is asked: its front window lags
+/// tmux's focus by about 0.3 s across a Space switch.
+const SETTLE: std::time::Duration = std::time::Duration::from_millis(1000);
+
+fn ghostty_focused_terminal() -> Result<String, String> {
+ let script = format!(
+ "tell application id \"{GHOSTTY_ID}\" to if frontmost then return id of focused terminal of selected tab of front window"
+ );
+ let out = Command::new(osascript()).args(["-e", &script]).output().map_err(|err| err.to_string())?;
+ let terminal = String::from_utf8_lossy(&out.stdout).trim().to_string();
+ if !valid_id(&terminal) {
+ return Err(format!("Ghostty reported no focused terminal ({terminal:?})"));
+ }
+ Ok(terminal)
}
/// `dingger terminal focused`: the client gained focus; pair it with the
@@ -129,17 +156,12 @@ pub fn learn(client: &str) -> Result<(), String> {
if !is_ghostty(&app) {
return Ok(());
}
- let script = format!(
- "tell application id \"{GHOSTTY_ID}\" to if frontmost then return id of focused terminal of selected tab of front window"
- );
- let out = Command::new(osascript()).args(["-e", &script]).output().map_err(|err| err.to_string())?;
- let terminal = String::from_utf8_lossy(&out.stdout).trim().to_string();
- if !valid_id(&terminal) {
- return Err(format!("Ghostty reported no focused terminal ({terminal:?})"));
- }
+ std::thread::sleep(SETTLE);
+ let terminal = ghostty_focused_terminal()?;
// Focus moved on while Ghostty answered: the answer may be another
// terminal's. The next focus-in pairs it.
- if !client_focused(tty) {
+ let (live, focused) = attached(tty);
+ if !focused {
return Err("focus moved on; not paired".to_string());
}
let ours = option_name(tty);
@@ -149,7 +171,8 @@ pub fn learn(client: &str) -> Result<(), String> {
return Ok(()); // a later focus-in already paired it
}
// A terminal holds one client: an older pairing with it is stale.
- if name != ours && pairing.terminal == terminal && pairing.focused_ms <= started {
+ // So is a client's that is no longer attached.
+ if name != ours && ((pairing.terminal == terminal && pairing.focused_ms <= started) || !live.contains(&name)) {
stale.push(name);
}
}