Compare commits
8 Commits
6028987a6e
...
50ae7c6061
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50ae7c6061 | ||
|
|
b9d5f144bd | ||
|
|
5da77b28b3 | ||
|
|
a36443bd86 | ||
|
|
3dbc1b11df | ||
|
|
84941247c4 | ||
|
|
91cfee9609 | ||
|
|
0ad3e3335e |
10
CHANGELOG.md
10
CHANGELOG.md
@ -81,3 +81,13 @@
|
||||
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
|
||||
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.
|
||||
|
||||
28
src/gui.rs
28
src/gui.rs
@ -1633,20 +1633,40 @@ impl App {
|
||||
let profile_path = self.path.clone();
|
||||
let allow = self.allow_layout;
|
||||
// Compile the profile's window_match patterns once, outside the
|
||||
// poll loop. If a pattern is malformed we treat it as "not
|
||||
// configured" rather than crashing the auto-apply thread.
|
||||
// poll loop. Malformed patterns are logged as a warning rather
|
||||
// than silently treated as "no pattern" — the latter would make
|
||||
// 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
|
||||
.profile
|
||||
.window_match
|
||||
.class
|
||||
.as_deref()
|
||||
.and_then(|p| regex::Regex::new(p).ok());
|
||||
.and_then(|p| match regex::Regex::new(p) {
|
||||
Ok(re) => Some(re),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"arm_auto_apply: invalid window_match.class regex {:?}: {}",
|
||||
p, e
|
||||
);
|
||||
None
|
||||
}
|
||||
});
|
||||
let title_pat = self
|
||||
.profile
|
||||
.window_match
|
||||
.title
|
||||
.as_deref()
|
||||
.and_then(|p| regex::Regex::new(p).ok());
|
||||
.and_then(|p| match regex::Regex::new(p) {
|
||||
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();
|
||||
std::thread::Builder::new()
|
||||
.name("enboxer-auto-apply".into())
|
||||
|
||||
@ -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<Arc<AtomicBool>>,
|
||||
}
|
||||
|
||||
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<OverlayHandle> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -468,12 +468,18 @@ pub fn runtime_dir() -> PathBuf {
|
||||
/// protocol (including `Command::Type`, which is wide-open text injection
|
||||
/// into game windows). Permissions are the cheapest defense.
|
||||
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)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = runtime_dir();
|
||||
let _ = std::fs::set_permissions(
|
||||
&dir,
|
||||
path,
|
||||
std::fs::Permissions::from_mode(0o700),
|
||||
);
|
||||
}
|
||||
@ -541,20 +547,24 @@ fn chmod_socket_sets_0o600() {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn chmod_runtime_dir_sets_0o700() {
|
||||
fn chmod_dir_sets_0o700_on_a_tempdir() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
// Save and restore the real dir perms around the test so we don't break
|
||||
// the live session if it happens to share XDG_RUNTIME_DIR.
|
||||
let dir = runtime_dir();
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let saved = std::fs::metadata(&dir).ok().map(|m| m.permissions().mode() & 0o777);
|
||||
// Force 0o755 so the helper actually has to change it.
|
||||
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755));
|
||||
chmod_runtime_dir();
|
||||
// Use a private tempdir so the test never mutates the user's
|
||||
// XDG_RUNTIME_DIR (which is what runtime_dir() resolves to).
|
||||
let unique = format!(
|
||||
"enboxer-chmod-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.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;
|
||||
assert_eq!(m, 0o700, "expected 0o700, got {m:o}");
|
||||
if let Some(s) = saved {
|
||||
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(s));
|
||||
}
|
||||
let _ = std::fs::remove_dir(&dir);
|
||||
}
|
||||
|
||||
|
||||
@ -223,7 +223,11 @@ async fn refresh_slots(
|
||||
let mut g = session.lock().await;
|
||||
g.binds_on = true;
|
||||
tracing::info!("routing on ({} binds)", specs.len());
|
||||
return finish_vfx(g, vfx_tx, hub, cursor).await;
|
||||
let r = 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 {
|
||||
g.binds_on = false;
|
||||
@ -232,7 +236,10 @@ async fn refresh_slots(
|
||||
hypr::clear_binds().await.ok();
|
||||
tracing::info!("routing off (focus left the team)");
|
||||
let g = session.lock().await;
|
||||
return finish_vfx(g, vfx_tx, hub, cursor).await;
|
||||
let r = 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -624,17 +631,23 @@ async fn swap_prev(session: &Arc<Mutex<Session>>) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn swap_as_main(session: &Arc<Mutex<Session>>, n: u32) -> Result<()> {
|
||||
let (wins, tiles) = {
|
||||
let (wins, tiles, prev_leader) = {
|
||||
let mut g = session.lock().await;
|
||||
if n < 2 || n as usize > g.slots.len() {
|
||||
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);
|
||||
for (i, (id, _)) in g.slots.iter_mut().enumerate() {
|
||||
*id = i as u32 + 1;
|
||||
}
|
||||
g.engine.set_leader(1);
|
||||
(g.slots.clone(), g.engine.profile.layout.slots.clone())
|
||||
let prev = g.engine.leader_slot;
|
||||
g.engine.set_leader(n);
|
||||
(g.slots.clone(), g.engine.profile.layout.slots.clone(), prev)
|
||||
};
|
||||
if !tiles.is_empty() {
|
||||
crate::layout::apply(&tiles, &wins).await?;
|
||||
@ -642,9 +655,11 @@ async fn swap_as_main(session: &Arc<Mutex<Session>>, n: u32) -> Result<()> {
|
||||
if let Some((_, c)) = wins.first() {
|
||||
hypr::focus_window(&c.address_selector()).await.ok();
|
||||
}
|
||||
hypr::notify(&format!("main is slot 1 (was {n})"))
|
||||
.await
|
||||
.ok();
|
||||
hypr::notify(&format!(
|
||||
"main is slot {n} (was {prev_leader})"
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -690,7 +705,26 @@ async fn reset_layout(session: &Arc<Mutex<Session>>) -> Result<()> {
|
||||
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<()> {
|
||||
// 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 g = session.lock().await;
|
||||
g.slots
|
||||
@ -1022,7 +1056,20 @@ async fn execute(actions: Vec<Action>, slots: &[(u32, Client)]) -> Result<()> {
|
||||
};
|
||||
for id in ids {
|
||||
if let Some((_, c)) = slots.iter().find(|(s, _)| *s == id) {
|
||||
hypr::deliver_key(c, &key, parsed_state).await?;
|
||||
// Per-slot failures must NOT abort the rest of the
|
||||
// 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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,39 +1,48 @@
|
||||
//! `zwlr_export_dmabuf_unstable_v1` client: covered-window capture path.
|
||||
//!
|
||||
//! When a Video FX source window is **covered** (its rect does not
|
||||
//! intersect any monitor) and the user has opted in
|
||||
//! (`ENBOXER_ENABLE_TOPLEVEL=1`), we fall back to a compositor export
|
||||
//! instead of `grim`. The export is a Wayland request:
|
||||
//! When a Video FX source window is hidden behind another compositor
|
||||
//! surface and the user has opted in (`ENBOXER_ENABLE_TOPLEVEL=1`), we
|
||||
//! can fall back to a compositor export instead of `grim` (which can
|
||||
//! only see visible-on-monitor pixels). The wire flow is a Wayland
|
||||
//! request:
|
||||
//!
|
||||
//! 1. `zwlr_export_dmabuf_manager_v1.capture_output(...)` → `frame` event
|
||||
//! 2. `frame` event carries `format` (DRM fourcc), `width`, `height`,
|
||||
//! `offset_x`, `offset_y`, and the per-plane `object` events carry
|
||||
//! 1. `zwlr_export_dmabuf_manager_v1.capture_output(...)` -> `frame`
|
||||
//! event
|
||||
//! 2. `frame` carries `format` (DRM fourcc), `width`, `height`,
|
||||
//! `offset_x`, `offset_y`; per-plane `object` events carry
|
||||
//! `fd`, `size`, `offset`, `stride`.
|
||||
//! 3. After all `object` events, `ready` (success) or `cancel` (failure)
|
||||
//! arrives.
|
||||
//! 3. After all `object` events, `ready` (success) or `cancel`
|
||||
//! (failure) arrives.
|
||||
//! 4. The client imports the dmabuf with gbm, maps the bo with
|
||||
//! `gbm_bo_map`, and copies the pixels out.
|
||||
//!
|
||||
//! ## Status
|
||||
//! ## Honest status (Bug #9)
|
||||
//!
|
||||
//! The protocol module, format negotiation, frame parser, and file-write
|
||||
//! to a **synthetic** buffer (the `gbm_bo_map` read pixel call is a
|
||||
//! follow-up) are implemented here. The compositor-facing parts compile
|
||||
//! against `wayland-protocols-wlr` but are only ever touched when the
|
||||
//! env gate is on; `cargo test` exercises only the pure negotiation and
|
||||
//! parser code paths.
|
||||
//! Steps 1-3 are fully implemented and exercised by the unit tests
|
||||
//! (`format_name`, `negotiate_format`, `parse_format`). Step 4 is
|
||||
//! **not** wired: the file-write in `capture_with_state` produces a
|
||||
//! synthetic PNG-sized buffer rather than `gbm_bo_map`-ing the dmabuf
|
||||
//! and copying real pixels. The synthetic buffer still carries the
|
||||
//! real width/height/format metadata from the compositor so callers
|
||||
//! can confirm the protocol round-trip end-to-end.
|
||||
//!
|
||||
//! `gbm` is genuinely gnarly to write inside this run: 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, so the file-write in this module
|
||||
//! currently produces a synthetic frame (a coloured rectangle that says
|
||||
//! "EXPORT PENDING"). The caller (`capture_toplevel`) is wired so that
|
||||
//! swapping in a real `gbm_bo_map` is one function change.
|
||||
//! `capture_output` was chosen as the primary entry because
|
||||
//! `capture_toplevel` (the window-scoped variant) requires a wl_surface
|
||||
//! reference this client does not currently hold. The capture loop in
|
||||
//! `vfx::capture_loop` already routes through `capture_toplevel` when
|
||||
//! the env gate is set, which means an opt-in user sees the synthetic
|
||||
//! frame (protocol-confirming, NOT real covered-source pixels). Real
|
||||
//! pixel reads come after the `gbm_bo_map` work lands.
|
||||
//!
|
||||
//! `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.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use wayland_client::protocol::{wl_buffer, wl_output, wl_registry};
|
||||
@ -205,9 +214,11 @@ pub async fn capture_via_export_for(
|
||||
}
|
||||
|
||||
/// Public entry: the dmabuf path of `capture_toplevel`. Connects to
|
||||
/// Wayland, requests an export, waits for the frame + object + ready
|
||||
/// events, then **without** touching gbm writes a synthetic PNG-sized
|
||||
/// byte slice to `dest`. The gbm bo map is a documented follow-up.
|
||||
/// Wayland, requests an export against the requested output, waits for
|
||||
/// the `frame` + per-plane `object` + `ready` events, then **without**
|
||||
/// calling gbm writes a synthetic PNG-sized byte slice to `dest`. The
|
||||
/// 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(
|
||||
output: &wl_output::WlOutput,
|
||||
dest: &Path,
|
||||
|
||||
52
src/vfx.rs
52
src/vfx.rs
@ -134,12 +134,28 @@ pub fn toplevel_enabled() -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Capture the source window via `zwlr_export_dmabuf_unstable_v1` and write
|
||||
/// the resulting frame to `dest`. Gated by `toplevel_enabled()`; until the
|
||||
/// live Wayland path lands, this errors out so callers fall back to `grim`.
|
||||
/// The wlr-export-dmabuf protocol is in `wayland-protocols-wlr`; wiring it
|
||||
/// (buffer management + format negotiation + post-import blit) is a
|
||||
/// follow-up ticket.
|
||||
/// Capture a covered source window via `zwlr_export_dmabuf_unstable_v1`
|
||||
/// and write a frame to `dest`. Gated by `toplevel_enabled()`.
|
||||
///
|
||||
/// ## Honest status (Bug #9 follow-up)
|
||||
///
|
||||
/// Today this function calls `toplevel_export::capture_via_export_for`,
|
||||
/// 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<()> {
|
||||
if !toplevel_enabled() {
|
||||
anyhow::bail!("toplevel export disabled (set ENBOXER_ENABLE_TOPLEVEL=1)");
|
||||
@ -330,6 +346,30 @@ pub async fn capture_loop(rx: watch::Receiver<Vec<FeedHit>>, hub: Arc<OverlayHub
|
||||
for f in feeds {
|
||||
let dest = frame_path(&f.name);
|
||||
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() {
|
||||
let _ = hub.show_frame(&f.name, &dest).await;
|
||||
}
|
||||
|
||||
@ -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<AtomicBool>,
|
||||
join: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
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<LiveOverlayHandle> {
|
||||
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<AtomicBool>) -> anyhow::Result<()> {
|
||||
let conn = Connection::connect_to_env()?;
|
||||
let display = conn.display();
|
||||
let mut event_queue = conn.new_event_queue::<OverlayState>();
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user