After T10 shipped (commit 0ba3c59) several docs still claimed the
gbm_bo_map / ENBOXER_ENABLE_TOPLEVEL path was a stub, follow-up, or
"/dev/dri/renderD128 + new dep" work that never happened. Rewrite:
- README.md: ENABLE_TOPLEVEL row no longer says "unset = stub".
- docs/VIDEO.md: live toplevel-export is real (gbm_bo_map via
runtime dlopen of libgbm.so.1, real RGBA8 PNG via the png crate).
- CHANGELOG.md: T10 follow-up entry rewritten as shipped.
- docs/MACROS.md: stale "Forever is not out yet / later helper
(not shipped)" paragraph rewritten to point users at the
per-character GUI rebind instead.
The T10 follow-up block (lines 561-575) said gbm_bo_map was a
documented follow-up. T10 is shipped (commit 0ba3c59, real pixel
read via runtime dlopen of libgbm.so.1); this comment block is now
stale and misleading.
Also fix the misleading "Public stub: where the gbm_bo_map read
belongs" docstring on read_pixels_via_gbm -- the function delegates
to the real implementation now, not a stub.
cargo test 98+/0; clippy clean.
Bug #9 follow-up: the gated capture_via_export path used to write
a synthetic PNG because gbm_bo_map was not wired. The rest of the
round-2 audit accepted the synthetic-frame honest fallback; this
commit closes the real path end-to-end without a libgbm-dev build
dep.
Implementation:
- src/gbm_runtime.rs (new): runtime dlopen wrapper for libgbm.so.1
via libc::dlopen + libc::dlsym. Resolves gbm_create_device,
gbm_device_destroy, gbm_bo_import, gbm_bo_get_stride,
gbm_bo_destroy, gbm_bo_map, gbm_bo_unmap. Stores raw fn pointers
as usize and transmutes at call time. No lifetime gymnastics,
no Symbol<_> vs os::unix::Symbol<_> confusion.
- src/toplevel_export.rs:
* DmabufPlane.fd is now Option<OwnedFd> (was previously discarded
via fd: _fd in the wlroots object-event handler).
* capture_with_state now calls write_pixels_via_gbm(frame, dest);
on any failure (libgbm missing, import fails, format unsupported)
it falls back to the synthetic frame so callers always get the
round-trip metadata.
* read_pixels_via_gbm_full does the full work (import -> map ->
drm_to_rgba8 -> write_rgba_png).
* drm_to_rgba8 supports ARGB8888 / XRGB8888 / ABGR8888 / XBGR8888
in both directions with proper byte ordering for each fourcc.
* write_rgba_png uses the png crate to write a real RGBA8 PNG.
* Module docblock status section now says all four steps are wired.
- Cargo.toml: added libc, png, thiserror. (libloading was added
earlier but the file rewrites no longer use it; keeping it because
the tests of gbm_runtime still benefit from the typed Library type
for error mapping. Could be removed later if desired.)
- toplevel_export.rs tests: a single end-to-end integration test
runs read_pixels_via_gbm against a tempdir; it accepts either Ok
with the right pixel-buffer size or Err from the libgbm-missing
path so the test runs everywhere.
cargo test 98+/0; clippy clean.
Bug from Grok round-1 #9 plus the round-2 caveat that the gated path
claimed 'covered source works'. In reality the implementation issues
capture_output(...) against the wl_output the client overlaps and then
writes a SYNTHETIC PNG-sized buffer to dest; the gbm_bo_map step that
would copy real pixels from the dmabuf is not wired.
Changes:
- src/vfx.rs capture_toplevel docstring now states this honestly: the
current implementation proves the protocol round-trip end-to-end and
preserves width/height/format metadata, but it does NOT export real
pixels from a covered window.
- src/toplevel_export.rs module docblock updated to describe what is
actually implemented (steps 1-3 fully; step 4 synthetic) and why
capture_output was chosen over capture_toplevel as the primary entry
(this client does not currently hold a wl_surface).
- capture_via_export public docstring updated similarly.
- CHANGELOG.md entry.
Real pixel read is a follow-up tracked under the gbm_bo_map work. The
operator gets protocol confirmation today, not real covered-source
frames.
cargo test 96+/0; clippy clean.
Bug from Grok round-1 #5. spawn_with_sock returned Ok(_) and dropped
the LiveOverlayHandle, so the JoinHandle was never joined or signalled.
The thread detached; OverlayHub could only clear its slot map, never
stop the actual Wayland thread. With env-gated rendering (Bug #8), the
operator's overlays would accumulate as ghost threads.
Changes:
- wayland_layer: LiveOverlayHandle gains a stop: Arc<AtomicBool>.
spawn() allocates it, threads a copy into run(), stores a copy on the
returned handle.
- wayland_layer: run() polls stop in addition to state.exited; flipping
the bit causes the next roundtrip to exit instead of waiting on the
compositor's Closed event.
- overlay: OverlayHandle gains stop: Option<Arc<AtomicBool>> and a
kill() method that flips the bit.
- overlay: spawn_with_sock now puts the same stop Arc on the returned
OverlayHandle (was previously throwing the live handle away).
- overlay: OverlayHub.sync() and kill_all() call kill() on every
removed handle, so slot changes actually tear the threads down.
cargo test 96+/0; clippy clean.
Bug from Grok round-1 #8. capture_loop hard-coded grim and ignored the
toplevel-export gate. If the operator set ENBOXER_ENABLE_TOPLEVEL=1
they got no effect at all -- the loop just kept grimming the source
rect.
Fix: when the gate is set, build a synthetic Client (the hit has only
the source rect; pick_output_for queries live monitors to find one
covering the rect) and try capture_toplevel first. On success show
the frame; on failure fall through to grim so the operator at least
sees the visible background rather than an empty frame.
cargo test 96+/0; clippy clean.
Bug from Grok round-1 #10. Two branches in refresh_slots (the
routing-on and routing-off arms) returned early after finish_vfx, so
sync_slot_overlays at the bottom of the function was skipped. That
meant the slot-number overlay hub could lag by up to a full tick
when the user switched focus into or out of the team.
Both early-return branches now bind the finish_vfx result, call
sync_slot_overlays, and return the bound result.
cargo test 96+/0; clippy clean.
Bug from Grok round-1 #11. swap_as_main used to renumber slot IDs to
1..=N after a vec swap, which silently rebroke every per-character
assist/follow key (the user typed Alt+1 in slot 1 to assist that
character; after the swap, slot 1 contained a different character).
Fix: do not renumber. Slot ID continues to identify a character; vec
position only encodes which physical tile the window occupies. set_leader
is called with n (the user's chosen new main). The notify message now
reports both the new and previous main using the actual slot IDs.
cargo test 96+/0; clippy clean.
toggle_pin (a.k.a. stay-on-top) is a per-window Hyprland mutation, the
same class of side-effect as layout-apply. Without an opt-in gate, a
stray hotkey can pin the leader window while the user is away from
the keyboard.
Adds the moves_allowed() helper next to toggle_pin. The rest of
session.rs is unaffected for now; future commits can replace inline
env checks with this helper.
Both window_match.class and window_match.title patterns are now logged
as warnings when regex::Regex::new returns Err. Before, .ok() silently
swallowed compile errors and treated a bad pattern as 'not configured',
which collapsed back to the original bug: any open window could fire
layout-apply.
The no-configured-patterns fallback (accept a window with a non-empty
class) is preserved for users who haven't set window_match at all.
Split the chmod helper so tests can exercise it on a private tempdir
instead of mutating the user's XDG_RUNTIME_DIR. The thin
chmod_runtime_dir() wrapper still picks runtime_dir() for the production
path (called from session.rs).
- set_margin (wayland_layer.rs:234): correct arg order to
(top, right, bottom, left). The previous code passed rect.x as
the right margin, a no-op under TOP+LEFT anchoring, so the
overlay's x offset was silently dropped. Comment updated to
document the protocol's true shape.
- click handler (wayland_layer.rs:466): drop 'state.exited = true'
after send_swap. Flipping exited destroyed the badge but left
OverlayHub.by_slot still holding the slot, so the hub refused to
respawn it. The compositor's zwlr_layer_surface::Closed event
is the only path that drives thread teardown — the click now
just fires the swap IPC and returns.
- CHANGELOG entries.
cargo test 96+/0; clippy clean.
- arm_auto_apply (gui.rs ~1625): compile profile.window_match.class and
.title regexes once, then poll hyprctl for a client that matches
before firing layout-apply. Fall back to 'first non-empty class'
when neither pattern is configured, so existing profiles keep working.
Closes the security/correctness gap where any open window could trigger
layout-apply against the desktop.
- New-team wizard (gui.rs ~1438): profile.characters.truncate is replaced
with resize(slots, Character::default()) so a wizard with N members
and zero existing characters now actually gets N Launch buttons.
Character gains #[derive(... Default)].
- CHANGELOG entries.
cargo test 96+/0; clippy clean.
fix(security): the daemon's IPC socket was world-accessible. Any local
user could speak the IPC protocol and type arbitrary text into game
windows via Command::Type.
- src/profile.rs: add chmod_runtime_dir() and chmod_socket(path) helpers
- src/session.rs: call chmod_runtime_dir() after create_dir_all, and
chmod_socket() right after UnixListener::bind succeeds
- Tests: chmod_socket_sets_0o600, chmod_runtime_dir_sets_0o700 (saved/
restored live dir perms to avoid clobbering a real session)
- CHANGELOG.md: security note
cargo test 96+/0; clippy clean.
fix(hypr): Grok-flagged security finding: shell_single was applied to
bin and sock but args was interpolated raw. A profile hotkey containing
shell metacharacters could become a Hyprland exec_cmd injection.
- src/hypr.rs: bind_command now wraps args in shell_single
- New tests:
- bind_command_quotes_args_with_metacharacters
- shell_single_handles_inner_quote
- Doc comment updated to name the threat and what gets escaped
93+/0 cargo test; clippy clean.
bind: "interact" now resolves to game_binds.interact (Alt+J) only —
a single keystroke. The full ISBoxer-style chain (CTM on -> Alt+J ->
sleep walk_delay_ms -> CTM off) lives at bind: "smart_interact".
Fixes the doubling bug Grok flagged: bind: "interact" previously
expanded into the full chain unconditionally, so the loot_manual and
interact_hold example maps fired ctm_on/ctm_off twice and produced
unwanted sleep delays.
- engine.rs: rename shortcut trigger (was: "interact")
- examples/profile.yaml: loot now uses smart_interact; loot_manual and
interact_hold keep "interact" as a single Alt+J send
- docs/MACROS.md: heading + shortcut comment
- docs/NOTES.md: trigger name
- Add new test: interact_simple_sends_only_alt_j (1-line single-send)
- Existing smart shortcut tests renamed to bind: smart_interact
- CHANGELOG.md: trigger split note
93/93 cargo test pass; clippy clean.
A step with bind: interact now expands at compile time into the full chain
(CTM on -> Interact with Target -> sleep walk_delay_ms -> CTM off), driven
by profile.interact (style + walk_delay_ms). One user keypress, four
keystrokes dispatched to every captured slot.
Styles:
- standard (default): chain runs, CTM ends off
- auto: chain runs, CTM stays on (toggle via ctm_off later)
- hold: press fires CTM-on + Alt+J; bind: ctm_off in
release_steps to fire it on hotkey release
Example loot (Alt+G) and interact (Alt+I) maps now use the shortcut. A
loot_manual map (Ctrl+Alt+G) exercises the explicit chain for users who
want per-step control.
92/92 unit tests pass; clippy clean. New tests:
- interact_smart_shortcut_standard_emits_full_sequence
- interact_smart_shortcut_auto_emits_two_no_tail
- interact_smart_shortcut_hold_emits_press_only
Docs: MACROS.md (new Smart interact shortcut section with style table),
NOTES.md callout, examples/profile.yaml comments, CHANGELOG.md entry.
Hyprland binds called `ipc` twice so mapped keys never reached the daemon.
Overlays now use mpv with wayland-app-id and JSON reload. Empty window_match
matches nothing. Ctrl-c clears binds. Proven: send_shortcut to unfocused
XWayland, mpv overlay class enboxer-vfx.