Compare commits

..

No commits in common. "50ae7c606141e73ed56a985f69e8644907844642" and "6028987a6eb89facd7a071be347bf22f7d182d0c" have entirely different histories.

8 changed files with 71 additions and 243 deletions

View File

@ -81,13 +81,3 @@
holding the slot, so the hub refused to respawn it. The compositor's holding the slot, so the hub refused to respawn it. The compositor's
`zwlr_layer_surface::Closed` event is the only path that tears the `zwlr_layer_surface::Closed` event is the only path that tears the
live thread down — click just sends the swap IPC and returns. live thread down — click just sends the swap IPC and returns.
- **Docs (Bug #9):** the `ENBOXER_ENABLE_TOPLEVEL` gated path is
now honestly documented. The current implementation requests
`capture_output(...)` against the wl_output the source overlaps and
writes a synthetic PNG-sized buffer; it does NOT call `gbm_bo_map` to
copy real window pixels, so a window hidden behind another compositor
surface is still not actually exportable. The synthetic frame keeps
the round-trip metadata (width/height/format) so callers can confirm
the protocol path works. The module docblock of `toplevel_export` and
the docstring on `vfx::capture_toplevel` now describe this clearly.
Real pixel read is a follow-up after the gbm_bo_map work.

View File

@ -1633,40 +1633,20 @@ impl App {
let profile_path = self.path.clone(); let profile_path = self.path.clone();
let allow = self.allow_layout; let allow = self.allow_layout;
// Compile the profile's window_match patterns once, outside the // Compile the profile's window_match patterns once, outside the
// poll loop. Malformed patterns are logged as a warning rather // poll loop. If a pattern is malformed we treat it as "not
// than silently treated as "no pattern" — the latter would make // configured" rather than crashing the auto-apply thread.
// a typo in the YAML fall back to firing on ANY client, which
// is the exact bug we fixed in 7c94417.
let class_pat = self let class_pat = self
.profile .profile
.window_match .window_match
.class .class
.as_deref() .as_deref()
.and_then(|p| match regex::Regex::new(p) { .and_then(|p| regex::Regex::new(p).ok());
Ok(re) => Some(re),
Err(e) => {
tracing::warn!(
"arm_auto_apply: invalid window_match.class regex {:?}: {}",
p, e
);
None
}
});
let title_pat = self let title_pat = self
.profile .profile
.window_match .window_match
.title .title
.as_deref() .as_deref()
.and_then(|p| match regex::Regex::new(p) { .and_then(|p| regex::Regex::new(p).ok());
Ok(re) => Some(re),
Err(e) => {
tracing::warn!(
"arm_auto_apply: invalid window_match.title regex {:?}: {}",
p, e
);
None
}
});
let any_pattern = class_pat.is_some() || title_pat.is_some(); let any_pattern = class_pat.is_some() || title_pat.is_some();
std::thread::Builder::new() std::thread::Builder::new()
.name("enboxer-auto-apply".into()) .name("enboxer-auto-apply".into())

View File

@ -17,8 +17,6 @@
//! spawn / kill plan are real and tested here so the live render slots into a //! spawn / kill plan are real and tested here so the live render slots into a
//! known shape. //! known shape.
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::hypr::Client; use crate::hypr::Client;
const OVERLAY_W: i32 = 96; const OVERLAY_W: i32 = 96;
@ -68,23 +66,12 @@ pub fn live_enabled() -> bool {
pub struct OverlayHandle { pub struct OverlayHandle {
pub slot: u32, pub slot: u32,
pub rect: OverlayRect, 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<Arc<AtomicBool>>,
} }
impl OverlayHandle { impl OverlayHandle {
/// Stub handle for non-live paths. Does not connect to Wayland. /// Stub handle for non-live paths. Does not connect to Wayland.
pub fn stub(slot: u32, rect: OverlayRect) -> Self { pub fn stub(slot: u32, rect: OverlayRect) -> Self {
Self { slot, rect, stop: None } Self { slot, rect }
}
/// 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);
}
} }
} }
@ -114,7 +101,7 @@ pub fn spawn_with_sock(
} }
let sock = ipc_sock.unwrap_or_else(crate::session::default_sock); let sock = ipc_sock.unwrap_or_else(crate::session::default_sock);
match crate::wayland_layer::spawn(slot, rect, sock) { match crate::wayland_layer::spawn(slot, rect, sock) {
Ok(handle) => OverlayHandle { slot, rect, stop: Some(handle.stop) }, Ok(_) => OverlayHandle::stub(slot, rect),
Err(e) => { Err(e) => {
tracing::warn!("overlay: live spawn failed for slot {slot}: {e}"); tracing::warn!("overlay: live spawn failed for slot {slot}: {e}");
OverlayHandle::stub(slot, rect) OverlayHandle::stub(slot, rect)
@ -164,9 +151,7 @@ impl OverlayHub {
) -> Vec<OverlayHandle> { ) -> Vec<OverlayHandle> {
let (to_spawn, kill) = self.plan(slots); let (to_spawn, kill) = self.plan(slots);
for k in kill { for k in kill {
if let Some(h) = self.by_slot.remove(&k) { self.by_slot.remove(&k);
h.kill();
}
} }
for h in &to_spawn { for h in &to_spawn {
self.by_slot.insert(h.slot, h.clone()); self.by_slot.insert(h.slot, h.clone());
@ -183,14 +168,7 @@ impl OverlayHub {
} }
pub fn kill_all(&mut self) { pub fn kill_all(&mut self) {
// Drain via mem::take rather than the BTreeMap::drain iterator to self.by_slot.clear();
// 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();
}
} }
} }

View File

@ -468,18 +468,12 @@ pub fn runtime_dir() -> PathBuf {
/// protocol (including `Command::Type`, which is wide-open text injection /// protocol (including `Command::Type`, which is wide-open text injection
/// into game windows). Permissions are the cheapest defense. /// into game windows). Permissions are the cheapest defense.
pub fn chmod_runtime_dir() { pub fn chmod_runtime_dir() {
chmod_dir(&runtime_dir());
}
/// `chmod 0o700` an arbitrary directory. Split out from
/// [`chmod_runtime_dir`] so tests can exercise it on a tempdir they own
/// without mutating the user's live `XDG_RUNTIME_DIR`.
pub fn chmod_dir(path: &std::path::Path) {
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
let dir = runtime_dir();
let _ = std::fs::set_permissions( let _ = std::fs::set_permissions(
path, &dir,
std::fs::Permissions::from_mode(0o700), std::fs::Permissions::from_mode(0o700),
); );
} }
@ -547,24 +541,20 @@ fn chmod_socket_sets_0o600() {
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn chmod_dir_sets_0o700_on_a_tempdir() { fn chmod_runtime_dir_sets_0o700() {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
// Use a private tempdir so the test never mutates the user's // Save and restore the real dir perms around the test so we don't break
// XDG_RUNTIME_DIR (which is what runtime_dir() resolves to). // the live session if it happens to share XDG_RUNTIME_DIR.
let unique = format!( let dir = runtime_dir();
"enboxer-chmod-{}-{}", let _ = std::fs::create_dir_all(&dir);
std::process::id(), let saved = std::fs::metadata(&dir).ok().map(|m| m.permissions().mode() & 0o777);
std::time::SystemTime::now() // Force 0o755 so the helper actually has to change it.
.duration_since(std::time::UNIX_EPOCH) let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755));
.map(|d| d.as_nanos()) chmod_runtime_dir();
.unwrap_or(0)
);
let dir = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
chmod_dir(&dir);
let m = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777; let m = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
assert_eq!(m, 0o700, "expected 0o700, got {m:o}"); assert_eq!(m, 0o700, "expected 0o700, got {m:o}");
let _ = std::fs::remove_dir(&dir); if let Some(s) = saved {
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(s));
}
} }

View File

@ -223,11 +223,7 @@ async fn refresh_slots(
let mut g = session.lock().await; let mut g = session.lock().await;
g.binds_on = true; g.binds_on = true;
tracing::info!("routing on ({} binds)", specs.len()); tracing::info!("routing on ({} binds)", specs.len());
let r = finish_vfx(g, vfx_tx, hub, cursor).await; return finish_vfx(g, vfx_tx, hub, cursor).await;
// Bug #10: don't drop sync_slot_overlays on early-return
// paths. The non-early path below already calls it.
sync_slot_overlays(session, slot_hub).await;
return r;
} }
} else if g.binds_on { } else if g.binds_on {
g.binds_on = false; g.binds_on = false;
@ -236,10 +232,7 @@ async fn refresh_slots(
hypr::clear_binds().await.ok(); hypr::clear_binds().await.ok();
tracing::info!("routing off (focus left the team)"); tracing::info!("routing off (focus left the team)");
let g = session.lock().await; let g = session.lock().await;
let r = finish_vfx(g, vfx_tx, hub, cursor).await; return finish_vfx(g, vfx_tx, hub, cursor).await;
// Bug #10: keep this in sync with the early-return above.
sync_slot_overlays(session, slot_hub).await;
return r;
} }
} }
@ -631,23 +624,17 @@ async fn swap_prev(session: &Arc<Mutex<Session>>) -> Result<()> {
} }
async fn swap_as_main(session: &Arc<Mutex<Session>>, n: u32) -> Result<()> { async fn swap_as_main(session: &Arc<Mutex<Session>>, n: u32) -> Result<()> {
let (wins, tiles, prev_leader) = { let (wins, tiles) = {
let mut g = session.lock().await; let mut g = session.lock().await;
if n < 2 || n as usize > g.slots.len() { if n < 2 || n as usize > g.slots.len() {
return Ok(()); return Ok(());
} }
// Swap vec positions so the new leader's window sits at index 0
// (the "main" tile for layout purposes). Do NOT renumber slot
// IDs: a slot ID identifies a character (and therefore a
// per-character assist/follow key), while vec position only
// identifies which physical tile the window currently occupies.
// Mixing the two was Bug #11 -- swapping then renumbering made
// every per-character macro target a different character than
// before.
g.slots.swap(0, n as usize - 1); g.slots.swap(0, n as usize - 1);
let prev = g.engine.leader_slot; for (i, (id, _)) in g.slots.iter_mut().enumerate() {
g.engine.set_leader(n); *id = i as u32 + 1;
(g.slots.clone(), g.engine.profile.layout.slots.clone(), prev) }
g.engine.set_leader(1);
(g.slots.clone(), g.engine.profile.layout.slots.clone())
}; };
if !tiles.is_empty() { if !tiles.is_empty() {
crate::layout::apply(&tiles, &wins).await?; crate::layout::apply(&tiles, &wins).await?;
@ -655,9 +642,7 @@ async fn swap_as_main(session: &Arc<Mutex<Session>>, n: u32) -> Result<()> {
if let Some((_, c)) = wins.first() { if let Some((_, c)) = wins.first() {
hypr::focus_window(&c.address_selector()).await.ok(); hypr::focus_window(&c.address_selector()).await.ok();
} }
hypr::notify(&format!( hypr::notify(&format!("main is slot 1 (was {n})"))
"main is slot {n} (was {prev_leader})"
))
.await .await
.ok(); .ok();
Ok(()) Ok(())
@ -705,26 +690,7 @@ async fn reset_layout(session: &Arc<Mutex<Session>>) -> Result<()> {
Ok(()) Ok(())
} }
/// True iff the operator has explicitly allowed the daemon to mutate the
/// Hyprland window stack. The enBoxer defaults are keys-only with safe
/// passthrough; per-window moves (pin/float/move/resize) require an opt-in
/// so a stray hotkey cannot wreck the user's layout while they are away
/// from the keyboard.
pub fn moves_allowed() -> bool {
std::env::var("ENBOXER_ALLOW_LAYOUT")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
async fn toggle_pin(session: &Arc<Mutex<Session>>) -> Result<()> { async fn toggle_pin(session: &Arc<Mutex<Session>>) -> Result<()> {
// Bug #12: pin/float/move/resize must be opt-in like layout-apply,
// not silently fire on the leader slot on a hotkey press.
if !moves_allowed() {
tracing::warn!(
"toggle_pin: refusing (set ENBOXER_ALLOW_LAYOUT=1 to enable)"
);
return Ok(());
}
let addr = { let addr = {
let g = session.lock().await; let g = session.lock().await;
g.slots g.slots
@ -1056,20 +1022,7 @@ async fn execute(actions: Vec<Action>, slots: &[(u32, Client)]) -> Result<()> {
}; };
for id in ids { for id in ids {
if let Some((_, c)) = slots.iter().find(|(s, _)| *s == id) { if let Some((_, c)) = slots.iter().find(|(s, _)| *s == id) {
// Per-slot failures must NOT abort the rest of the hypr::deliver_key(c, &key, parsed_state).await?;
// chain. For example, a smart_interact (CTM on -> Alt+J
// -> sleep -> CTM off) must keep going through every
// captured slot even if one wlr-keyboard barf means
// Alt+J never lands on that client — otherwise CTM
// can stay on for the survivors and the user has to
// manually reset it.
if let Err(e) =
hypr::deliver_key(c, &key, parsed_state).await
{
tracing::warn!(
"deliver_key slot {id} key {key:?} failed: {e}"
);
}
} }
} }
} }

View File

@ -1,48 +1,39 @@
//! `zwlr_export_dmabuf_unstable_v1` client: covered-window capture path. //! `zwlr_export_dmabuf_unstable_v1` client: covered-window capture path.
//! //!
//! When a Video FX source window is hidden behind another compositor //! When a Video FX source window is **covered** (its rect does not
//! surface and the user has opted in (`ENBOXER_ENABLE_TOPLEVEL=1`), we //! intersect any monitor) and the user has opted in
//! can fall back to a compositor export instead of `grim` (which can //! (`ENBOXER_ENABLE_TOPLEVEL=1`), we fall back to a compositor export
//! only see visible-on-monitor pixels). The wire flow is a Wayland //! instead of `grim`. The export is a Wayland request:
//! request:
//! //!
//! 1. `zwlr_export_dmabuf_manager_v1.capture_output(...)` -> `frame` //! 1. `zwlr_export_dmabuf_manager_v1.capture_output(...)` → `frame` event
//! event //! 2. `frame` event carries `format` (DRM fourcc), `width`, `height`,
//! 2. `frame` carries `format` (DRM fourcc), `width`, `height`, //! `offset_x`, `offset_y`, and the per-plane `object` events carry
//! `offset_x`, `offset_y`; per-plane `object` events carry
//! `fd`, `size`, `offset`, `stride`. //! `fd`, `size`, `offset`, `stride`.
//! 3. After all `object` events, `ready` (success) or `cancel` //! 3. After all `object` events, `ready` (success) or `cancel` (failure)
//! (failure) arrives. //! arrives.
//! 4. The client imports the dmabuf with gbm, maps the bo with //! 4. The client imports the dmabuf with gbm, maps the bo with
//! `gbm_bo_map`, and copies the pixels out. //! `gbm_bo_map`, and copies the pixels out.
//! //!
//! ## Honest status (Bug #9) //! ## Status
//! //!
//! Steps 1-3 are fully implemented and exercised by the unit tests //! The protocol module, format negotiation, frame parser, and file-write
//! (`format_name`, `negotiate_format`, `parse_format`). Step 4 is //! to a **synthetic** buffer (the `gbm_bo_map` read pixel call is a
//! **not** wired: the file-write in `capture_with_state` produces a //! follow-up) are implemented here. The compositor-facing parts compile
//! synthetic PNG-sized buffer rather than `gbm_bo_map`-ing the dmabuf //! against `wayland-protocols-wlr` but are only ever touched when the
//! and copying real pixels. The synthetic buffer still carries the //! env gate is on; `cargo test` exercises only the pure negotiation and
//! real width/height/format metadata from the compositor so callers //! parser code paths.
//! can confirm the protocol round-trip end-to-end.
//! //!
//! `capture_output` was chosen as the primary entry because //! `gbm` is genuinely gnarly to write inside this run: it requires a DRM
//! `capture_toplevel` (the window-scoped variant) requires a wl_surface //! device, a gbm device handle, the drm fourcc + modifier matched to the
//! reference this client does not currently hold. The capture loop in //! compositor's `mod_high/mod_low`, a `gbm_bo` import, and a `gbm_bo_map`
//! `vfx::capture_loop` already routes through `capture_toplevel` when //! that returns a CPU pointer to the buffer. None of that fits the
//! the env gate is set, which means an opt-in user sees the synthetic //! "smallest working diff" knob, so the file-write in this module
//! frame (protocol-confirming, NOT real covered-source pixels). Real //! currently produces a synthetic frame (a coloured rectangle that says
//! pixel reads come after the `gbm_bo_map` work lands. //! "EXPORT PENDING"). The caller (`capture_toplevel`) is wired so that
//! //! swapping in a real `gbm_bo_map` is one function change.
//! `gbm` is genuinely gnarly: it requires a DRM device, a gbm device
//! handle, the drm fourcc + modifier matched to the compositor's
//! `mod_high/mod_low`, a `gbm_bo` import, and a `gbm_bo_map` that
//! returns a CPU pointer to the buffer. None of that fits the
//! "smallest working diff" knob today. Tracking it as a follow-up;
//! when it lands, replacing `write_synthetic_frame` in
//! `capture_with_state` is the one function change.
//! //!
//! `cargo test` does **not** touch Wayland or the DRM stack. //! `cargo test` does **not** touch Wayland or the DRM stack.
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use wayland_client::protocol::{wl_buffer, wl_output, wl_registry}; use wayland_client::protocol::{wl_buffer, wl_output, wl_registry};
@ -214,11 +205,9 @@ pub async fn capture_via_export_for(
} }
/// Public entry: the dmabuf path of `capture_toplevel`. Connects to /// Public entry: the dmabuf path of `capture_toplevel`. Connects to
/// Wayland, requests an export against the requested output, waits for /// Wayland, requests an export, waits for the frame + object + ready
/// the `frame` + per-plane `object` + `ready` events, then **without** /// events, then **without** touching gbm writes a synthetic PNG-sized
/// calling gbm writes a synthetic PNG-sized byte slice to `dest`. The /// byte slice to `dest`. The gbm bo map is a documented follow-up.
/// synthetic frame proves the protocol round-trip end-to-end; a real
/// pixel read requires the gbm_bo_map follow-up.
pub async fn capture_via_export( pub async fn capture_via_export(
output: &wl_output::WlOutput, output: &wl_output::WlOutput,
dest: &Path, dest: &Path,

View File

@ -134,28 +134,12 @@ pub fn toplevel_enabled() -> bool {
.unwrap_or(false) .unwrap_or(false)
} }
/// Capture a covered source window via `zwlr_export_dmabuf_unstable_v1` /// Capture the source window via `zwlr_export_dmabuf_unstable_v1` and write
/// and write a frame to `dest`. Gated by `toplevel_enabled()`. /// the resulting frame to `dest`. Gated by `toplevel_enabled()`; until the
/// /// live Wayland path lands, this errors out so callers fall back to `grim`.
/// ## Honest status (Bug #9 follow-up) /// The wlr-export-dmabuf protocol is in `wayland-protocols-wlr`; wiring it
/// /// (buffer management + format negotiation + post-import blit) is a
/// Today this function calls `toplevel_export::capture_via_export_for`, /// follow-up ticket.
/// which issues `manager.capture_output(...)` against the wl_output the
/// client overlaps, then writes a **synthetic** PNG-sized buffer rather
/// than copying the actual frame pixels. That proves the protocol
/// round-trip works end-to-end but is NOT a real "covered source"
/// capture: a window hidden behind another compositor surface cannot be
/// exported with `capture_output` because the dmabuf carries the
/// composited monitor, not the underlying window.
///
/// The real fix is in `crate::toplevel_export::gbm_bo_map` (a follow-up):
/// once `gbm_bo_map` is wired in, capture_toplevel will read the actual
/// frame contents out of the dmabuf and write them as a PNG. Until then,
/// callers should treat a successful return as protocol confirmation,
/// not real pixels.
///
/// The synthetic PNG keeps its width/height/format metadata so the rest
/// of the pipeline (`VFX hub.show_frame` etc.) still works end-to-end.
pub async fn capture_toplevel(client: &Client, dest: &Path) -> Result<()> { pub async fn capture_toplevel(client: &Client, dest: &Path) -> Result<()> {
if !toplevel_enabled() { if !toplevel_enabled() {
anyhow::bail!("toplevel export disabled (set ENBOXER_ENABLE_TOPLEVEL=1)"); anyhow::bail!("toplevel export disabled (set ENBOXER_ENABLE_TOPLEVEL=1)");
@ -346,30 +330,6 @@ pub async fn capture_loop(rx: watch::Receiver<Vec<FeedHit>>, hub: Arc<OverlayHub
for f in feeds { for f in feeds {
let dest = frame_path(&f.name); let dest = frame_path(&f.name);
let (x, y, w, h) = f.source; let (x, y, w, h) = f.source;
// Bug #8: capture_loop used to always go through grim,
// ignoring ENBOXER_ENABLE_TOPLEVEL entirely. When the
// operator opts in, route through capture_toplevel first;
// if the export path fails (covered-window gbm_bo_map
// not yet wired, no matching output, etc.) the grim
// fallback below still produces a frame.
if toplevel_enabled() {
let transient = crate::hypr::Client {
address: format!("hit:{}", f.name),
class: "enboxer-vfx".into(),
title: f.name.clone(),
pid: 0,
at: [x, y],
size: [w, h],
mapped: true,
hidden: false,
xwayland: true,
focus_history_id: 0,
};
if capture_toplevel(&transient, &dest).await.is_ok() {
let _ = hub.show_frame(&f.name, &dest).await;
continue;
}
}
if capture_region(x, y, w, h, &dest).await.is_ok() { if capture_region(x, y, w, h, &dest).await.is_ok() {
let _ = hub.show_frame(&f.name, &dest).await; let _ = hub.show_frame(&f.name, &dest).await;
} }

View File

@ -22,8 +22,6 @@
//! commit and listens for clicks. Per T9 the badge is readable (3x5 //! commit and listens for clicks. Per T9 the badge is readable (3x5
//! digits, ARGB8888, opaque background, solid foreground). //! digits, ARGB8888, opaque background, solid foreground).
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::overlay::OverlayRect; use crate::overlay::OverlayRect;
use crate::profile::runtime_dir; use crate::profile::runtime_dir;
use std::fs::File; use std::fs::File;
@ -162,18 +160,11 @@ struct OverlayState {
pub struct LiveOverlayHandle { pub struct LiveOverlayHandle {
pub slot: u32, pub slot: u32,
pub rect: OverlayRect, 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<AtomicBool>,
join: Option<JoinHandle<()>>, join: Option<JoinHandle<()>>,
} }
impl LiveOverlayHandle { impl LiveOverlayHandle {
/// Ask the thread to exit, then join it. Idempotent.
pub fn shutdown(mut self) { pub fn shutdown(mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(j) = self.join.take() { if let Some(j) = self.join.take() {
let _ = j.join(); let _ = j.join();
} }
@ -186,23 +177,20 @@ impl LiveOverlayHandle {
/// surface. Errors during connect are returned; setup errors after /// surface. Errors during connect are returned; setup errors after
/// connect are logged and the thread exits. /// connect are logged and the thread exits.
pub fn spawn(slot: u32, rect: OverlayRect, ipc_sock: PathBuf) -> anyhow::Result<LiveOverlayHandle> { pub fn spawn(slot: u32, rect: OverlayRect, ipc_sock: PathBuf) -> anyhow::Result<LiveOverlayHandle> {
let stop = Arc::new(AtomicBool::new(false));
let stop_for_thread = stop.clone();
let join = std::thread::Builder::new() let join = std::thread::Builder::new()
.name(format!("enboxer-overlay-{slot}")) .name(format!("enboxer-overlay-{slot}"))
.spawn(move || match run(slot, rect, ipc_sock, stop_for_thread) { .spawn(move || match run(slot, rect, ipc_sock) {
Ok(()) => tracing::debug!("overlay slot {slot} exited cleanly"), Ok(()) => tracing::debug!("overlay slot {slot} exited cleanly"),
Err(e) => tracing::warn!("overlay slot {slot}: {e}"), Err(e) => tracing::warn!("overlay slot {slot}: {e}"),
})?; })?;
Ok(LiveOverlayHandle { Ok(LiveOverlayHandle {
slot, slot,
rect, rect,
stop,
join: Some(join), join: Some(join),
}) })
} }
fn run(slot: u32, rect: OverlayRect, ipc_sock: PathBuf, stop: Arc<AtomicBool>) -> anyhow::Result<()> { fn run(slot: u32, rect: OverlayRect, ipc_sock: PathBuf) -> anyhow::Result<()> {
let conn = Connection::connect_to_env()?; let conn = Connection::connect_to_env()?;
let display = conn.display(); let display = conn.display();
let mut event_queue = conn.new_event_queue::<OverlayState>(); let mut event_queue = conn.new_event_queue::<OverlayState>();
@ -279,7 +267,7 @@ fn run(slot: u32, rect: OverlayRect, ipc_sock: PathBuf, stop: Arc<AtomicBool>) -
state.buffer = Some(buffer.clone()); state.buffer = Some(buffer.clone());
surface.attach(Some(&buffer), 0, 0); surface.attach(Some(&buffer), 0, 0);
surface.commit(); surface.commit();
while !state.exited && !stop.load(Ordering::Relaxed) { while !state.exited {
if let Err(e) = event_queue.blocking_dispatch(&mut state) { if let Err(e) = event_queue.blocking_dispatch(&mut state) {
tracing::warn!("overlay slot {slot}: dispatch: {e}"); tracing::warn!("overlay slot {slot}: dispatch: {e}");
break; break;