From 0b5de324a462a9c3fb96d13263415ae7da5804e8 Mon Sep 17 00:00:00 2001 From: Josh Ellithorpe Date: Thu, 20 Aug 2026 08:52:37 +0000 Subject: [PATCH] gpui_linux: Clear X11 urgency hint when the window becomes active (#61619) # Objective On X11, GPUI raises the ICCCM `WM_HINTS` urgency flag to ask for attention, but it never clears it. The window stays urgent for the rest of its life. `request_attention` was the only thing touching the bit. There was no code anywhere to clear it. Docks and taskbars latch on the rising edge, so they show the attention state once and then sit there with a permanent urgent indicator, no further notifications. ## Solution Clear the flag when the window becomes active. The read/modify/write moves out of `request_attention` into a helper, `set_wm_hints_urgency(xcb, x_window, urgent)`. The raise and clear sites are on different self types (`X11Window` vs `X11WindowStatePtr`), so it takes the connection and window id. The clear goes in `set_wm_properties`, where `state.active` gets set from `_NET_WM_STATE_FOCUSED`. That's the same field `request_attention` already checks in its `if self.is_active() { return; }` guard. Two implementation notes: * The clear is edge triggered (`state.active && !was_active`). `set_wm_properties` recomputes everything on every `_NET_WM_STATE` change, so a level triggered check would do a blocking `WmHints::get` round trip on every maximize, fullscreen, hide and workspace switch while focused. Edge triggering is safe here because `request_attention` early returns when active, so urgency only ever gets raised while inactive, and the false to true transition always follows a raise. * Clearing reads the hints first and skips the write if the bit isn't set. Otherwise focusing a window that never asked for attention would create a `WM_HINTS` property just to say "not urgent". The raise path behaves exactly as before. The skip can't fire when `urgent` is true, so repeat raises still write every time. Focusing clears the flag even if a second thread is still waiting. That's already how it works, since `request_attention` early returns when the window is active. ## Testing Tested on X11, Cinnamon with Muffin. On an unpatched build the window had `_NET_WM_STATE_FOCUSED` and the urgency bit set at the same time. W=$(xdotool search --class "dev.zed.Zed" | head -1) xprop -id $W WM_HINTS xprop -spy -id $W WM_HINTS 1. Fresh window says `WM_HINTS: not found`, and still does after focusing it. 2. With focus on another window, the urgency bit shows up when attention is requested. `request_attention` early returns when active, so the window has to be unfocused for this to fire. 3. Focusing Zed clears the bit. 4. Repeating 2 and 3 re-arms it every cycle, not just once. 5. No `WM_HINTS` writes during normal use (resize, maximize, workspace switch). No automated test. It's an X11 round trip on a real window and there's no X11 test harness in the tree. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments (none added) - [x] The content adheres to Zed's UI standards (no UI change) - [x] Tests cover the new/changed behavior (see Testing) - [x] Performance impact has been considered and is acceptable Note this was manually tested, there wasn't a good way to test it through the current harness. --- Release Notes: - Fixed the X11 urgency hint never being cleared, which left Zed showing a permanent attention indicator in taskbars and docks after the first notification --- crates/gpui_linux/src/linux/x11/window.rs | 61 +++++++++++++++-------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index 4fde66f06c..94037067e7 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/crates/gpui_linux/src/linux/x11/window.rs @@ -389,6 +389,39 @@ where .with_context(failure_context) } +/// Sets or clears the ICCCM WM_HINTS urgency flag, preserving the other hints. +/// +/// Clearing when the flag isn't set is skipped: writing it back would create a +/// WM_HINTS property on windows that never requested attention, and would add an +/// X round trip to every window state change. +fn set_wm_hints_urgency(xcb: &XCBConnection, x_window: xproto::Window, urgent: bool) { + let mut hints = WmHints::new(); + match WmHints::get(xcb, x_window) { + Ok(cookie) => match cookie.reply() { + Ok(Some(existing_hints)) => hints = existing_hints, + Ok(None) => {} + Err(error) => { + log::debug!("failed to read X11 WM_HINTS before setting urgency: {error}") + } + }, + Err(error) => { + log::debug!("failed to request X11 WM_HINTS before setting urgency: {error}") + } + } + + if !urgent && !hints.urgent { + return; + } + + hints.urgent = urgent; + check_reply( + || "X11 ChangeProperty for WM_HINTS urgency failed.", + hints.set(xcb, x_window), + ) + .log_err(); + xcb_flush(xcb); +} + /// Convert X11 connection errors to `anyhow::Error` and panic for unrecoverable errors. pub(crate) fn handle_connection_error(err: ConnectionError) -> anyhow::Error { match err { @@ -1080,6 +1113,7 @@ impl X11WindowStatePtr { .chunks_exact(4) .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); + let was_active = state.active; state.active = false; state.fullscreen = false; state.maximized_vertical = false; @@ -1100,6 +1134,12 @@ impl X11WindowStatePtr { } } + // The urgency hint has no withdrawal signal of its own; ICCCM leaves that to + // the client, and focus is the conventional means for the user to zero it. + if state.active && !was_active { + set_wm_hints_urgency(&self.xcb, self.x_window, false); + } + Ok(()) } @@ -1497,26 +1537,7 @@ impl PlatformWindow for X11Window { return; } - let mut hints = WmHints::new(); - match WmHints::get(&*self.0.xcb, self.0.x_window) { - Ok(cookie) => match cookie.reply() { - Ok(Some(existing_hints)) => hints = existing_hints, - Ok(None) => {} - Err(error) => { - log::debug!("failed to read X11 WM_HINTS before setting urgency: {error}") - } - }, - Err(error) => { - log::debug!("failed to request X11 WM_HINTS before setting urgency: {error}") - } - } - hints.urgent = true; - check_reply( - || "X11 ChangeProperty for WM_HINTS urgency failed.", - hints.set(&*self.0.xcb, self.0.x_window), - ) - .log_err(); - xcb_flush(&self.0.xcb); + set_wm_hints_urgency(&self.0.xcb, self.0.x_window, true); } fn is_active(&self) -> bool {