enBoxer/README.md
en 5aabf0d36f Round 5 batch A (Grok review): games.yaml seed, pending_ipc fix, doc residue, dead passthrough_id.
Grok round 5 found:

1. examples/games.yaml was actually src/main.rs (227 lines of clap
   CLI; my round-4 Item 4 commit message was wrong). Replaced with
   a real seed YAML. The example shows the env format as
   [[KEY, VALUE], ...] (a list of pairs), NOT KEY=*** (serde_yaml
   will not parse KEY=*** into Vec<(String, String)>).

3. App.pending_ipc was written by Free-mode buttons but never
   drained by the main loop. Replaced with direct self.ipc(...)
   calls. Hoisted the closure borrow by collecting verbs into a
   Vec<(&str, u32)> and draining after the slots borrow is
   released. Dropped the pending_ipc field + App literal init.

8. Doc residue: passthrough / window_match claims still in
   README.md, CHANGELOG.md, docs/DESIGN.md. Updated README.md to
   point at round-4 Item 6 + Item 3. Added a round-4 section at
   the top of CHANGELOG.md. Dropped the duplicate passthrough
   bullet in docs/DESIGN.md.

10. Dead passthrough_id helper in hotkey.rs (unused after the
    passthrough feature dropped in round-4 Item 6). Dropped the
    function + its doc comment.

cargo test 103/103; clippy clean.
2026-09-17 07:40:24 +02:00

160 lines
10 KiB
Markdown

# enBoxer
Mapped-key routing and live window crops for **Arch Linux + Hyprland**. Rust rewrite of a Windows multibox controller — the project layout, slot overlay, and key-routing engine follow the same shape, but nothing game-side is touched. Keys and clicks only. Pushes to `https://gitea.nettsi.de/en/enBoxer` over HTTPS only.
You list which hotkeys go where. Everything you do not list stays on the focused primary. Round-4 (Item 6) removed the `passthrough` skip-list: every mapped hotkey is intercepted. Alt-tab off the team and the desktop is normal.
Nothing is loaded into the game. Hyprland delivers keys with `hl.dsp.send_shortcut` / `send_key_state`. You bind macros in WoW yourself.
## Routing (three modes)
Toggle with `Shift+Alt+M` (configurable) or `enboxer mode maps|mirror|off` while the daemon runs. A Hyprland notification shows the new mode.
1. **maps** — only keys you listed under `maps` are intercepted and sent where the map says. Everything else goes to the front window.
2. **mirror** — the front window still gets the real key; the same key is cloned to the other game windows. Mirror cloning applies to every mapped hotkey (round-4 Item 6 dropped the `passthrough` skip-list).
3. **off** — nothing is intercepted. All keys go to the front window. The mode-toggle hotkey still works.
A **map** is a hotkey plus steps (send a key, send a named `game_bind`, wait) plus a **target**.
Targets:
| Target | Meaning |
| --- | --- |
| `current` | The leader slot only |
| `others` | Every captured slot except the leader |
| `all` | Every captured slot including the leader |
| `group:<name>` | Slots listed in `groups.<name>` |
| `slot:<n>` | One specific slot |
| `slots:<n>,<m>,…` | An explicit list |
| `round_robin` / `rr` | Each press advances to the next slot (1→2→3→…→1, **includes the leader**, wrapping). State is per-map. |
### Mode-extras
- **Mouse click broadcast (mirror).** When mode is `mirror`, left and right clicks on the primary are also cloned to every other captured slot via the Hyprland compositor (cursor stays where you put it). Wired through the same compositor path as the keys. No press-and-hold guard yet — TODO.
- **Clipboard broadcast.** `Session → Send clipboard to others (Ctrl+V)` copies the current clipboard (`wl-paste` then `xclip`) and pastes into every non-leader captured slot.
- **Round-robin example.** See `examples/profile.yaml``rez_cycle`: pressing `Alt+R` fires `F8` to slot 1, then slot 2, then slot 3, wrapping back to 1 on the next press. Each map gets its own cursor.
## Interact / loot
WoW's **Interact with Target** bind (plus Click to Move) walks an alt to the target and loots or talks. One press. Spamming it makes characters circle. The stock `loot` map: assist others → CTM on → interact once → wait `walk_delay_ms` → CTM off. See [docs/MACROS.md](docs/MACROS.md).
Per-character `assist_key` / `follow_key` resolve `/assist ThisName` / `/follow ThisName` against the current main's slot. Update the name in `examples/profile.yaml` per team.
## Window layout
The layout wizard builds a tile grid for `N` captured game clients:
- Presets: **stacked**, **same-size grid**, **main + strip** (one large main plus a row of minions).
- Per-slot `x y w h` in pixels.
- Drag tiles directly on the GUI canvas — the layout page shows a draggable, clickable monitor rectangle per output.
- **Generate** fills in defaults from the selected preset; **Capture current positions** reads `hyprctl -j clients` and stores whatever is on screen.
- **Save and Apply** calls `enboxer layout-apply` and uses `hyprctl dispatch` to float / move / resize. **Gated** by `ENBOXER_ALLOW_LAYOUT=1` (env or GUI checkbox on Session) **and** a confirm dialog before apply.
- **Swap next / Make main** rotates which slot is in the large tile. **Reset all** restores the saved layout.
- **Stay on top** pins the new tiles. **Borderless** installs a Hyprland `window_rule` (rounding 0, border_size 0) for the captured class when the daemon starts.
## Slot overlay (click-to-swap)
When `ENBOXER_ENABLE_OVERLAY=1` is set in the daemon's environment, `enboxer run` spawns a `wlr-layer-shell` surface per captured slot showing the slot number, anchored to the slot's top-left. Clicking the badge calls `swap <slot>` so you can change which client is main without alt-tabbing. With the env unset (default), `cargo test` and `enboxer doctor` never connect to Wayland. See [docs/DESIGN.md](docs/DESIGN.md).
## Video FX
A region of another client is captured and shown on the primary. While the cursor is over the viewer, keys and clicks go to the **source** slot only.
Two capture paths:
1. **Visible source — `grim`** (default). Fast, zero-copy when a source window sits on a visible output. This is what `mpv` reloads each frame.
2. **Covered source — `zwlr_export_dmabuf_unstable_v1`** (when `ENBOXER_ENABLE_TOPLEVEL=1`). Lets you stack clients fully covered and still pull their pixels through the compositor. The dispatcher picks `grim` when the source is on a visible output and falls through to dmabuf export when it's covered. The dmabuf pixel-read path runs end-to-end via a runtime dlopen of `libgbm.so.1` (see `src/gbm_runtime.rs` + `src/toplevel_export.rs`); no `libgbm-dev` build dep. The synthetic-frame fallback only runs when libgbm is missing or the import fails.
See [docs/VIDEO.md](docs/VIDEO.md).
## Teams and the Lutris launcher
Multiple teams can live side-by-side. Each team is a separate YAML in `~/.config/enboxer/teams/<slug>.yaml`; the active team is recorded in `~/.config/enboxer/current_team`. Use the GUI **Teams** menu (top bar) to:
- **New team…** — opens a wizard with name, member count, and a Lutris game picker. The Lutris picker is visible **only** in this flow.
- **Switch team…** — load any saved team.
- **Refresh Lutris for current team** — re-read `~/.config/lutris/games/<slug>.yml` so changes Lutris made to the runner / env / DLL overrides take effect on the next launch.
- **Show Lutris config for current team** — display the parsed runner / exe / env / prefix / DLL overrides for transparency.
- **Delete team…** — removes the team YAML. Does **not** touch Lutris.
The `Launch` menu (under `Session`) shows one button per character that has a `lutris_game` set, plus **Launch all**. enBoxer spawns Wine directly with the merged env (`WINEPREFIX`, `WINEDLLOVERRIDES`, `WINEESYNC`, `WINEFSYNC`, `DXVK_*`, `VKD3D_CONFIG`) — never the `lutris` CLI, because Lutris is not built for launching the same game five times.
Optional `auto_apply: true` on a character polls `hyprctl -j clients` for up to 30 s after spawn and runs `layout-apply` once the matched window appears. Off by default. The auto-apply path is the only code that touches window geometry after Launch; everything else stays read-only until you click **Save and Apply** in the GUI.
## Build
```bash
cd ~/git/enBoxer
cargo build --release
install -Dm755 target/release/enboxer ~/.local/bin/enboxer
```
Runtime deps on the host: Hyprland 0.56 (Lua dispatchers), `hyprctl`, `grim`, `mpv` (Video FX overlay), `slurp` (rectangle picker), `wl-paste` and `xclip` (clipboard broadcast). For the Lutris launcher: a working `wine` on `PATH` and the user's Lutris install.
For the optional live paths: the Wayland protocol bindings (`wayland-client` 0.31, `wayland-protocols-wlr` 0.3.12) are already pulled in by `Cargo.toml`.
## Use
```bash
enboxer # control panel (File / Session / Layout / Maps / Video / Macros / Teams)
enboxer run -c ~/.config/enboxer/profile.yaml # daemon only
enboxer doctor
enboxer press Alt+G # fire a map without a Hyprland bind
enboxer layout-apply -c ... # one-shot layout apply (gated by ENBOXER_ALLOW_LAYOUT)
```
The GUI's **Teams** menu is the first thing to use when you have not configured a team yet. Use **New team…** to pick a Lutris game and member count. The **Launch** menu then has one button per character.
`ENBOXER_ALLOW_LAYOUT=1` (env) or the Session checkbox is required for **Save and Apply**, swap-geometry, and reset.
## Environment variables (gates)
| Var | Effect | Default |
| --- | --- | --- |
| `ENBOXER_ALLOW_LAYOUT` | Permit `layout-apply`, swap, reset | unset = off |
| `ENBOXER_ENABLE_OVERLAY` | Spawn the live `wlr-layer-shell` slot overlay | unset = stub (geometry + routing only) |
| `ENBOXER_ENABLE_TOPLEVEL` | Spawn the live `wlr_export_dmabuf_unstable_v1` capture path | unset = grim fallback |
| `ENBOXER_MOUSE_REPEAT_MS` | Cadence (ms) between repeat clicks while a mouse button is held | unset = 50 ms (20 Hz); clamped 1..=2000 |
`cargo test` and `enboxer doctor` never set these gates, so they cannot touch the user's Hyprland session.
## Files
| Path | Purpose |
| --- | --- |
| `~/git/enBoxer` | The Rust crate |
| `examples/profile.yaml` | Stock profile (`team`, main+strip layout, stock loot map, round-robin example) |
| `examples/hyprland.lua` | Hyprland bind helper (snippet, optional) |
| `~/.config/enboxer/profile.yaml` | Active profile (single-profile legacy / no-team fallback) |
| `~/.config/enboxer/teams/<slug>.yaml` | One team = one file |
| `~/.config/enboxer/current_team` | One-line file with the active team's slug |
| `~/.config/lutris/games/*.yml` | Lutris game store (read by the launcher) |
## Safety (non-negotiable)
- Default `enboxer run`: keys only. No float / move / resize / pin.
- `ENBOXER_ALLOW_LAYOUT=1` is required for apply / swap-geometry / reset. The GUI confirms before Apply.
- The example profile has `auto_apply: false`, Video FX disabled, and empty layout tiles.
- No DLL, no memory inject, no required WoW addon in v1.
- `cargo test`, `enboxer doctor`, and the un-gated stub paths do not connect to Wayland or move windows.
## Docs
| Doc | What |
| --- | --- |
| [GOALS.md](GOALS.md) | What this project is for (read this first) |
| [AGENTS.md](AGENTS.md) | Rules for Grok / OpenCode |
| [PLAN.md](PLAN.md) | Shipped vs next |
| [DESCRIPTION.md](DESCRIPTION.md) | One-page description |
| [AGENT.md](AGENT.md) | Designer / PM / worker contract |
| [TASKS.md](TASKS.md) | OpenCode tickets |
| [CHANGELOG.md](CHANGELOG.md) | What landed |
| [docs/DESIGN.md](docs/DESIGN.md) | Routing model and overlay capture model |
| [docs/NOTES.md](docs/NOTES.md) | WoW interact / Hyprland capture |
| [docs/VIDEO.md](docs/VIDEO.md) | Live crop of another client |
| [docs/MACROS.md](docs/MACROS.md) | In-game macros |
## License
GPL-3.0-or-later.