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
This commit is contained in:
Josh Ellithorpe authored and GitHub committed 2026-08-20 08:52:37 +00:00
1 parent b25370b285
commit 0b5de324a4
1 file changed
+41 -20
+41 -20
View File
@@ -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 {