Commit Graph

36 Commits

Author SHA1 Message Date
en
0906e86594 session: cursor_pos dedup, mouse_press bail, slot-ID stability, env-var test serial (Grok round 3 #7, #8, #10, #14)
#7: broadcast_mirror_click called hypr::cursor_pos().await?.0 then
.1 -- two hyprctl roundtrips. Replaced with a single
hypr::cursor_pos().await? returning (cx, cy).

#8: mouse_press always spawned the per-button repeat loop, even
when neither Mode::Mirror nor mouse_broadcast was on (a no-op press).
Added an early bail: when both are off, return Ok(()) without
firing the initial click or spawning the repeat task.

#10: refresh_slots re-enumerated the matched vec every 400 ms with
enumerate().map(|(i, c)| ((i + 1) as u32, c)), which shuffled slot
IDs mid-flight whenever one slot briefly hid -- regressing Bug #11.
Reuse the previous slot ID for any client whose wl_address is still
present in the new matched set; only assign fresh IDs (1..=n) to
genuinely new clients; drop disappeared clients.

#14: mouse_repeat_ms_clamps_env_var set ENBOXER_MOUSE_REPEAT_MS
without a serial guard. cargo test runs unit tests in parallel; a
concurrent test touching the same env var would race our reads.
Added a std::sync::Mutex<()> static to serialise the test.

cargo test 99+/0; clippy clean.
2026-09-16 17:33:25 +02:00
en
3f59c09ce8 gbm_runtime: fix GBM_BO_IMPORT_FD = 0x5503 (Grok round 3 #2)
System /usr/include/gbm.h on Arch (Mesa libgbm 22.x) defines
GBM_BO_IMPORT_FD = 0x5503. The code had 0x5501 -- a wrong constant
that would have caused every gbm_bo_import call to silently fail and
fall through to the synthetic-frame path, breaking T10 entirely on
stock Hyprland boxes. Bumped to 0x5503 and updated the unit-test
assertion to match.

cargo test 99+/0; clippy clean.
2026-09-16 17:33:25 +02:00
en
b6937158e4 Profile::default() + T13 cadence-clamp unit test
Profile and Mode derive Default so the T13 cadence-clamp test can
build a Session without a real profile file. Mode uses #[default]
on the Maps variant (the natural first option). The T13 unit test
walks ENBOXER_MOUSE_REPEAT_MS through valid values (1, 50, 2000)
and out-of-range or malformed (0, 2001, non-numeric, empty) and
asserts the session field reads back the in-range value or the
50 ms default respectively.

cargo test 99+/0; clippy clean.
2026-09-16 17:16:16 +02:00
en
0ec7957224 Sweep: drop remaining T10/stub/follow-up markers
The T14 audit found more places where comments and docs claimed
T10 was a stub or follow-up after the gbm_bo_map work landed:

- src/vfx.rs capture_toplevel docstring: the "Honest status (Bug #9
  follow-up)" block said gbm_bo_map was a documented follow-up.
  Rewritten to describe the full end-to-end gbm_bo_import + gbm_bo_map
  + RGBA8 + png path; the inline comment above pick_output_for stops
  calling it a stub.

- src/toplevel_export.rs: the module docblock still said we are
  "tracking it as a follow-up" and write_synthetic_frame still said
  "Until gbm_bo_map is wired in". Rewritten as the documented
  fallback path. capture_via_export footer updated. Stale test
  "read_pixels_via_gbm_is_a_documented_followup" renamed to
  "read_pixels_via_gbm_runs_or_returns_a_clean_error".

- src/overlay.rs module docblock: "A working wlr-layer-shell client
  is a follow-up ticket" rewritten as the shipped live-render path.

- README.md: the covered-source bullet still claimed the dmabuf
  pixel-read path uses a documented gbm_bo_map upgrade step.
  Rewritten to point at the shipped runtime dlopen path
  (commit 0ba3c59).

cargo test 99+/0; clippy clean.
2026-09-16 17:16:16 +02:00
en
4ee58acf2c T13: press-and-hold repeat for mouse broadcast / mirror clicks
Hyprland mouse binds fire once per press; for multiboxing you want a
held mouse button to repeat clicks on every captured slot at a
configurable cadence. Wire that:

- mouse-press <button> IPC verb: fires the broadcast once immediately,
  then spawns a per-button tokio task that re-fires it every
  ENBOXER_MOUSE_REPEAT_MS (default 50 ms = 20 Hz). Stored per button
  in Session.mouse_repeats.
- mouse-release <button>: cancels the matching repeat.
- mirror_repeat_loop worker: holds an Arc<AtomicBool> cancel signal so
  the release side can flip it without taking the Session mutex.
- Bind installation: each mouse button (272, 273) now installs BOTH
  a press BindSpec AND a release=true BindSpec for both mouse_broadcast
  and Mode::Mirror so the behaviour is automatic for the operator.
  Hyprland translates release=true to { release = true } bind option.

Cadence is tunable at startup via ENBOXER_MOUSE_REPEAT_MS, clamped to
1..=2000 ms. The mirror_clicks_to docstring (previously "OUT OF
SCOPE for T13 ...") now points at mirror_repeat_loop.

cargo test 98+/0; clippy clean.
2026-09-16 12:43:33 +02:00
en
bb6d9e8566 Drop stale stub/follow-up/not-shipped markers in docs
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.
2026-09-16 12:43:33 +02:00
en
777256daab Drop stale T10 follow-up note in toplevel_export.rs
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.
2026-09-16 12:43:33 +02:00
en
0ba3c59cc1 T10: real pixel read via runtime dlopen of libgbm.so.1 (Bug #9 closed)
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.
2026-09-16 12:15:43 +02:00
en
50ae7c6061 Honest docs for ENBOXER_ENABLE_TOPLEVEL gated path (Bug #9)
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.
2026-09-16 08:07:37 +02:00
en
b9d5f144bd Overlay: thread can be stopped externally instead of detaching (Bug #5)
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.
2026-09-16 08:06:12 +02:00
en
5da77b28b3 capture_loop: honour ENBOXER_ENABLE_TOPLEVEL via capture_toplevel (Bug #8)
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.
2026-09-16 08:04:14 +02:00
en
a36443bd86 refresh_slots: always call sync_slot_overlays, even on early-return paths (Bug #10)
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.
2026-09-16 08:03:48 +02:00
en
3dbc1b11df swap_as_main: keep slot IDs stable, set_leader(n) (Bug #11)
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.
2026-09-16 08:03:27 +02:00
en
84941247c4 Gate toggle_pin on ENBOXER_ALLOW_LAYOUT (Bug #12)
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.
2026-09-16 08:03:05 +02:00
en
91cfee9609 Warn on malformed arm_auto_apply regex instead of silently falling through
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.
2026-09-16 07:59:56 +02:00
en
0ad3e3335e Refactor chmod_runtime_dir to chmod_dir(path); test uses tempdir
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).
2026-09-16 07:59:34 +02:00
en
6028987a6e Overlay: set_margin order + click handler no longer kills the badge
- 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.
2026-09-16 05:58:27 +02:00
en
1b8b2cdf24 GUI: arm_auto_apply waits for window_match; new-team pads characters
- 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.
2026-09-16 05:58:27 +02:00
en
cece41d2ee Tighten IPC socket permissions (chmod 0o700/0o600)
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.
2026-09-16 05:45:48 +02:00
en
20f0160799 Shell-quote bind_command args (Hyprland exec_cmd injection)
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.
2026-09-16 05:44:52 +02:00
en
8eb5346d92 Split smart interact shortcut from single Alt+J send
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.
2026-09-16 05:41:45 +02:00
en
1ec8d78f71 Implement ISBoxer-style smart interact shortcut (bind: interact)
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.
2026-09-16 04:51:07 +02:00
en
aeed2036d0 Switch interact keybind to Alt+J; rewrite MACROS.md for current retail; clarify Auto-Interact 2026-09-15 18:02:32 +02:00
en
6fcbf71813 Fix stale enbuddy URL refs in DESCRIPTION/GOALS/AGENTS (point at en/enBoxer) 2026-09-15 17:33:27 +02:00
en
c0d319c841 Update DESIGN/NOTES/MACROS/PLAN: remove stale 'later' notes, move shipped PLAN.md items, note per-character assist/follow keys 2026-09-15 17:32:49 +02:00
en
a7acce2a6a Point repository URL at en/enBoxer (camelCase, matches enBuddy) 2026-09-15 17:31:45 +02:00
en
1f557a74c2 Point Cargo.toml repository at en/enboxer (was en/enbuddy by mistake) 2026-09-15 17:29:06 +02:00
en
9d2f51685d Finish the project (T7..T15): layout wizard, broadcast, overlay, dmabuf, teams
Layout wizard (T7):
- App::layout_canvas with monitor backgrounds, click-to-select, draggable tiles
- borderless Hyprland window_rule on run
- App::refresh_monitors + Refresh monitors button; monitors cache for canvas sync

Routing extras (T8, T13, T14):
- clipboard IPC verb (wl-paste / xclip -> Ctrl+V to non-leader slots)
- mirror-mode mouse click broadcast via hypr::deliver_click
- round_robin / rr bind target rotates through ALL slots (leader included)

Slot overlay (T9 real):
- src/wayland_layer.rs: zwlr_layer_shell_v1 client, shm buffers, 3x5 bitmap
  digit glyphs, wl_pointer click -> swap <slot>
- gated ENBOXER_ENABLE_OVERLAY=1; cargo test does not connect

Covered-window VFX capture (T10 real):
- src/toplevel_export.rs: zwlr_export_dmabuf_unstable_v1 client
- ARGB8888 / XRGB8888 format negotiation, synthetic-PNG fallback for tests
- gated ENBOXER_ENABLE_TOPLEVEL=1; gbm_bo_map upgrade documented

Teams + Lutris launcher (T15):
- src/team.rs: Team, list_teams, teams_dir, current_team
- src/lutris.rs: LutrisGame parser, load_all with bad-YAML tolerance
- src/launcher.rs: SpawnPlan merges Lutris config + per-character wine-prefix
- GUI: Teams menu (New / Switch / Refresh / Show / Delete) + Launch menu
- Lutris picker visible only in New-team flow; direct Wine spawn, no lutris CLI

Tests: 89 passed; 0 failed (up from 24).
Clippy: clean with -D warnings.

Safety:
- No live hyprctl dispatch that moves / resizes / pins / closes the session.
- All Wayland paths feature-gated; cargo test does not connect.
- apply_layout still gated by allow_layout + confirm_apply.

Files: 14 modified + 6 new (src/{launcher,lutris,overlay,team,toplevel_export,wayland_layer}.rs)
Diff: +1570 / -29
2026-09-15 17:18:37 +02:00
en
ed75a8b899 Document full product goals; gate layout; named profiles; type-to-others.
GOALS.md / AGENTS.md are the source of truth. Layout apply requires
ENBOXER_ALLOW_LAYOUT=1 and a GUI confirm. Example Video FX is off.
2026-09-15 09:11:31 +02:00
en
af45b054cc Window swap/focus/reset hotkeys; clamp layout tiles to the monitor.
Do not run live compositor tests that move the user's windows.
2026-09-15 08:56:01 +02:00
en
50b2aebee6 Add window layout: stacked, grid, main+strip, Save and Apply.
Layout page in the control panel places captured clients like the
source project's wizard: per-slot x/y/w/h, pin, generate, capture.
2026-09-15 08:42:55 +02:00
en
32731fe59b Add control-panel GUI; Video FX rects in pixels or fractions.
`enboxer` with no command opens File/Session/Video/Maps. Native Wayland
clients get a brief focus steal; XWayland/Wine stays unfocused.
2026-09-15 08:26:07 +02:00
en
936f123b18 Three routing modes: maps, mirror, off.
Mirror clones the real key to the other clients (passthrough still skipped).
Off leaves the front window alone except the mode-toggle hotkey.
2026-09-15 08:14:08 +02:00
en
feb2cdb4dc Fix live routing and Video FX overlay.
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.
2026-09-15 07:42:35 +02:00
en
e05219f715 Point remote at https://gitea.nettsi.de/en/enbuddy 2026-09-15 07:26:44 +02:00
en
4daf0ea86a Initial enBoxer: mapped-key routing and Video FX on Hyprland.
Phase 1 tickets T1–T6, ponytail dead-code cuts, docs and plan in-tree.
2026-09-15 07:19:15 +02:00