diff --git a/src/overlay.rs b/src/overlay.rs index 859a344..d760883 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -17,6 +17,8 @@ //! spawn / kill plan are real and tested here so the live render slots into a //! known shape. +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use crate::hypr::Client; const OVERLAY_W: i32 = 96; @@ -66,12 +68,23 @@ pub fn live_enabled() -> bool { pub struct OverlayHandle { pub slot: u32, pub rect: OverlayRect, + /// External kill signal for the live Wayland thread. The hub flips + /// this to true when the slot goes away so the thread exits cleanly + /// instead of being silently detached (Bug #5). + pub stop: Option>, } impl OverlayHandle { /// Stub handle for non-live paths. Does not connect to Wayland. pub fn stub(slot: u32, rect: OverlayRect) -> Self { - Self { slot, rect } + Self { slot, rect, stop: None } + } + + /// Ask the underlying thread to exit. No-op for stubs. + pub fn kill(&self) { + if let Some(s) = &self.stop { + s.store(true, Ordering::Relaxed); + } } } @@ -101,7 +114,7 @@ pub fn spawn_with_sock( } let sock = ipc_sock.unwrap_or_else(crate::session::default_sock); match crate::wayland_layer::spawn(slot, rect, sock) { - Ok(_) => OverlayHandle::stub(slot, rect), + Ok(handle) => OverlayHandle { slot, rect, stop: Some(handle.stop) }, Err(e) => { tracing::warn!("overlay: live spawn failed for slot {slot}: {e}"); OverlayHandle::stub(slot, rect) @@ -151,7 +164,9 @@ impl OverlayHub { ) -> Vec { let (to_spawn, kill) = self.plan(slots); for k in kill { - self.by_slot.remove(&k); + if let Some(h) = self.by_slot.remove(&k) { + h.kill(); + } } for h in &to_spawn { self.by_slot.insert(h.slot, h.clone()); @@ -168,7 +183,14 @@ impl OverlayHub { } pub fn kill_all(&mut self) { - self.by_slot.clear(); + // Drain via mem::take rather than the BTreeMap::drain iterator to + // keep ownership types local (std::mem::take always works + // regardless of the nightly/stable Rust allocation churn in the + // IntoIterator impls). + let taken = std::mem::take(&mut self.by_slot); + for (_, h) in taken { + h.kill(); + } } } diff --git a/src/wayland_layer.rs b/src/wayland_layer.rs index e5d71fe..3c56345 100644 --- a/src/wayland_layer.rs +++ b/src/wayland_layer.rs @@ -22,6 +22,8 @@ //! commit and listens for clicks. Per T9 the badge is readable (3x5 //! digits, ARGB8888, opaque background, solid foreground). +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use crate::overlay::OverlayRect; use crate::profile::runtime_dir; use std::fs::File; @@ -160,11 +162,18 @@ struct OverlayState { pub struct LiveOverlayHandle { pub slot: u32, pub rect: OverlayRect, + /// External kill signal. The dispatch loop in [`run`] polls this + /// once per roundtrip; flipping to true causes the thread to exit + /// on the next round, so the join in [`shutdown`] does not block + /// forever waiting on the compositor's `Closed` event. + pub stop: Arc, join: Option>, } impl LiveOverlayHandle { + /// Ask the thread to exit, then join it. Idempotent. pub fn shutdown(mut self) { + self.stop.store(true, Ordering::Relaxed); if let Some(j) = self.join.take() { let _ = j.join(); } @@ -177,20 +186,23 @@ impl LiveOverlayHandle { /// surface. Errors during connect are returned; setup errors after /// connect are logged and the thread exits. pub fn spawn(slot: u32, rect: OverlayRect, ipc_sock: PathBuf) -> anyhow::Result { + let stop = Arc::new(AtomicBool::new(false)); + let stop_for_thread = stop.clone(); let join = std::thread::Builder::new() .name(format!("enboxer-overlay-{slot}")) - .spawn(move || match run(slot, rect, ipc_sock) { + .spawn(move || match run(slot, rect, ipc_sock, stop_for_thread) { Ok(()) => tracing::debug!("overlay slot {slot} exited cleanly"), Err(e) => tracing::warn!("overlay slot {slot}: {e}"), })?; Ok(LiveOverlayHandle { slot, rect, + stop, join: Some(join), }) } -fn run(slot: u32, rect: OverlayRect, ipc_sock: PathBuf) -> anyhow::Result<()> { +fn run(slot: u32, rect: OverlayRect, ipc_sock: PathBuf, stop: Arc) -> anyhow::Result<()> { let conn = Connection::connect_to_env()?; let display = conn.display(); let mut event_queue = conn.new_event_queue::(); @@ -267,7 +279,7 @@ fn run(slot: u32, rect: OverlayRect, ipc_sock: PathBuf) -> anyhow::Result<()> { state.buffer = Some(buffer.clone()); surface.attach(Some(&buffer), 0, 0); surface.commit(); - while !state.exited { + while !state.exited && !stop.load(Ordering::Relaxed) { if let Err(e) = event_queue.blocking_dispatch(&mut state) { tracing::warn!("overlay slot {slot}: dispatch: {e}"); break;