Bound every wait before the overlay is interactive

Running wl-pick twice left the second instance hung with no window at all
and no way to end it but kill.

The hang was in the capture phase, not the overlay. sway answers a capture
request for a toplevel that another client is already capturing with
silence: no frame, no failed, no stopped. captures_settled() waits for every
tile to reach one of those three, so it waited forever, holding its buffers,
having never created a layer surface. Any concurrent capture client will do
this, not just a second wl-pick.

So the phases before the overlay is interactive now have a deadline. pump_for
polls the connection with one, and a tile that never arrives is drawn as a
bare label, exactly as an outright capture failure already was.
report_unsettled names those tiles on stderr, which is the diagnostic whose
absence made this hard to find.

The first instance had a second problem: sway hands the keyboard to the new
overlay and sends the old one wl_keyboard.leave, but the dispatcher only
handled Key, so the loser sat on screen holding a grab it no longer had,
deaf to every key. That is the process that stays around. Losing the grab
for good now ends the run, so a second wl-pick started from the same
keybinding replaces the first rather than stranding it.

For good, because sway also sends leave followed immediately by enter on the
same surface -- microseconds apart -- as a focus refresh when the pointer
crosses the overlay. Treating a bare leave as terminal made a lone instance
quit itself after less than a second. Focus is tracked in the dispatcher and
the main loop only gives up once it has failed to come back.

pump had no callers left after that: every wait is now either budgeted or
focus-aware.
This commit is contained in:
Milad Alizadeh
2026-09-07 11:47:41 +01:00
parent 34548fe4f6
commit c607fc9a1c
6 changed files with 142 additions and 17 deletions
+25
View File
@@ -109,6 +109,8 @@ pub struct App {
pub(crate) output: String,
pub(crate) ending: Ending,
/// Whether we hold the keyboard. Without it the overlay cannot be operated.
pub(crate) focused: bool,
pub(crate) picked: Option<Target>,
pub(crate) stats: Stats,
}
@@ -134,6 +136,8 @@ pub enum Ending {
Picked,
Cancelled,
Closed,
/// The keyboard went to another surface, so we can no longer be operated.
Unfocused,
}
impl Ending {
@@ -143,6 +147,7 @@ impl Ending {
Ending::Picked => "picked",
Ending::Cancelled => "cancelled",
Ending::Closed => "the compositor closed the overlay",
Ending::Unfocused => "lost the keyboard to another surface",
}
}
}
@@ -196,6 +201,7 @@ impl App {
configured: false,
output,
ending: Ending::Running,
focused: false,
picked: None,
stats: Stats::default(),
};
@@ -272,6 +278,25 @@ impl App {
pub fn captures_settled(&self) -> bool {
self.tiles.iter().all(|t| t.settled)
}
/// Say which tiles the compositor went quiet on, and name the likeliest
/// reason: sway answers a capture request on a toplevel that another client
/// is already capturing with silence rather than with `failed`.
pub fn report_unsettled(&self) {
let stuck: Vec<&str> = self
.tiles
.iter()
.filter(|t| !t.settled)
.map(|t| t.target.title.as_str())
.collect();
eprintln!(
"wl-pick: no frame for {} of {} tiles ({}); \
another capture client may hold these sources",
stuck.len(),
self.tiles.len(),
stuck.join(", ")
);
}
}
// --- enumeration ----------------------------------------------------------