Compare commits
No commits in common. "50ae7c606141e73ed56a985f69e8644907844642" and "6028987a6eb89facd7a071be347bf22f7d182d0c" have entirely different histories.
50ae7c6061
...
6028987a6e
10
CHANGELOG.md
10
CHANGELOG.md
@ -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.
|
|
||||||
|
|||||||
28
src/gui.rs
28
src/gui.rs
@ -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())
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,11 +642,9 @@ 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
|
||||||
))
|
.ok();
|
||||||
.await
|
|
||||||
.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}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
52
src/vfx.rs
52
src/vfx.rs
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user