Grok round-6 verification was No-Go. Fixes:
A. LayoutSlot.id was written as 0 by every constructor
(generate Stacked/Grid/main_strip, capture_from, the GUI pad
literals), so the find-by-id lookups in resize_slot / move_slot
never hit a tile and Free-mode Apply size/position always
failed with "no layout slot for slot id N". Constructors now
assign real 1-based ids (index+1, out.len()+1, i+2 for the
strip); capture_from uses the slot id from the window tuple.
layout::apply and reset_slot_lock now find the tile by id
instead of by Vec index.
B. The `slots` IPC formatter emitted "{id} 0x{address} ..." while
addresses already carry their own 0x prefix ("0xa"), producing
"1 0x0xa ..."; the parser split on whitespace so any multi-word
window title broke the field alignment. Both sides now use a
tab separator and the address passes through unchanged.
C. examples/profile.yaml still shipped the dropped schema
(window_match block + passthrough list). Replaced with a note
that matching is by process tree and every mapped hotkey is
intercepted.
D. CHANGELOG 0.1.0 still advertised passthrough (lines 19, 24)
and window_match (27, 78). Annotated as removed.
E. Lying comments: launcher.rs called the prefix "per-team" (it is
per-character); gui.rs::arm_auto_apply doc claimed it matched a
regex.
F. arm_auto_apply still hardcoded an empty spawned-pid set and
fell back to matching any client with a non-empty class -- the
round-4 Item-3 placeholder was what actually ran. It now takes
the real child pid and matches via pid_is_ancestor.
G. spawn_plan dropped the Child with no wait thread (zombie, same
bug round-6 fixed in launch_game). Now reaps in a background
thread.
H. page_session never refreshed the games list, so the dropdown
was empty on first paint. Added a games_loaded flag and a
one-shot refresh_games on first paint.
Plus: vfx env-var test race. toplevel_enabled_defaults_off_... and
capture_toplevel_is_gated_when_disabled both touch
ENBOXER_ENABLE_TOPLEVEL and cargo runs unit tests in parallel, so
the gated test intermittently saw the var set by its sibling (the
per-function `static` in session.rs does not serialise across
functions). Added a module-level ENV_LOCK in vfx::tests and
guarded both tests. Verified with three consecutive full runs.
Also: clippy unnecessary_cast in main_strip, and two rustdoc
warnings (raw <pid> and Arc<GbmDevice> read as HTML tags).
cargo test 103/103 (x3); clippy --all-targets -D warnings clean;
cargo doc --no-deps clean.
Grok round 5 found these surfaces either dead or lying:
2. Item 4 GUI never shipped. page_session had no game dropdown;
menu_launch was still Lutris. Added a Game row: ComboBox fed by
the daemon `list-games` verb (refresh_games helper), Launch
button fires `launch-game NAME`. New App fields game_list /
game_pick.
4. GUI local spawn never filled Session.spawned_pids. spawn_plan
now captures the child PID and pushes it via the new `track-pid`
IPC verb; status shows the pid. Daemon-side track-pid inserts
into Session.spawned_pids.
5. launch_game drop(Child) did not reap -> zombies (wine wrappers
exit fast). Replaced with a background thread that calls
child.wait(); the PID stays in spawned_pids.
6. Deleted the select_windows placeholder that returned a fake
first-N-visible list disagreeing with the daemon assignment.
select_windows now reads the daemon slot list via the new
`slots` IPC verb (layout::slots_from_daemon) and returns the
same (id, Client) shape.
7. resize_slot / move_slot used slot.saturating_sub(1) as an index
into layout.slots, which breaks when Session.slots has gaps
after a hide ([1a, 3b] -> resize-slot 3 bailed). LayoutSlot
gains a stable `id: u32` field; both handlers now find the
layout slot by id. LayoutSlot literals across the repo given
an id.
9. keybind_detail Map panel now exposes release_steps (Add /
Remove / edit per entry), matching the press steps editor.
cargo test 103/103; clippy clean.
Master wanted two modes:
- Managed (existing wizard behaviour): auto-arrange grid via the
layout wizard.
- Free-arrange: launch windows without applying layout. Per-window
initial resolution. Apply-size button (not position). Daemon does
NOT keep overwriting resolution if operator manually resized.
Reference: KWin scripts apply-size / apply-position for the live
resize plumbing.
Implementation:
src/profile.rs:
* LayoutMode enum: Managed (default) | Free. Serde round-trips
as "managed" / "free".
* Layout.mode field added (default = Managed via LayoutMode derive).
* LayoutSlot split into initial_size (Option<(u32,u32)>),
pos (Option<(i32,i32)>), size_locked (bool). The legacy x/y/w/h
fields stay for Managed mode (cached geometry).
src/hypr.rs:
* move_resize_window split into resize_window(w, h) + move_window(x, y).
* move_resize_window kept as a thin wrapper so the original
layout-apply IPC path still works.
src/layout.rs:
* apply(slots, windows) split into apply_size + apply_pos; apply()
is now the thin wrapper that calls both.
* All 4 LayoutSlot literals (Stacked preset, Grid helper, main_strip,
capture_from) gained ..Default::default().
src/session.rs:
* New IPC verbs: resize-slot N, move-slot N, reset-slot N.
* resize_slot: applies initial_size (or cached w/h), sets
size_locked=true so the daemon does not undo future operator
resizes.
* move_slot: applies pos (or cached x/y).
* reset_slot_lock: clears size_locked on slot-N.
* All three bail with a clear message if the mode is not Free.
src/gui.rs:
* Mode toggle (Managed / Free) at the top of page_layout.
* page_layout_managed: existing wizard (presets, drag tiles,
Save and Apply).
* page_layout_free: per-window initial W/H picker, Apply-size,
Apply-position, Reset-size buttons. Per-slot lock indicator.
Uses DragValue::range (clamp_range is deprecated).
* App.pending_ipc: Vec<String> queue; the main loop will drain
it and fire the IPC verbs.
cargo test 103/103 (unchanged from Item 4 + 3 games tests); clippy clean.
Master quote: "i do not know why you have regex for the games? If you are launching the games you should be able to find out what you launched instead and then place it in the correct spot in the grid. programs spawn under a tree structure on linux so you should easily be able to figure out what you spawned."
Implementation:
- src/process.rs (new): pid_is_ancestor(ancestor, candidate) walks /proc/<pid>/status PPid chain upward. Bounded to 64 hops so a malformed /proc cannot spin us. Unit-tested with three cases: self, zero-pids, and the running test process against its own ppid.
- src/lib.rs: pub mod process registered.
- src/profile.rs:
* Profile.window_match field removed (was Option<String> class + Option<String> title regex).
* WindowMatch struct removed.
* HashSet import kept (still used elsewhere).
- src/session.rs:
* Session.spawned_pids: HashSet<u32> field added + initialised.
* refresh_slots: replaces regex filter with crate::process::pid_is_ancestor(root, c.pid) against every spawned root.
* profile-init log line dropped window_match.class / .title args.
* borderless path: removed class_re use; for now re-applies apply_vfx_window_rules (per-spawn windowrulev2 lands once the launcher wires the pid in Item 4).
* matches_regex helper removed (was only used by arm_auto_apply).
- src/layout.rs:
* select_windows now a placeholder: returns the first N visible clients in z-order. Real picker is in session::refresh_slots.
* Regex import dropped.
- src/gui.rs:
* WindowMatch import removed.
* Default profile literal: window_match field removed.
* Profile editor: the four class/title text-edit lines replaced with a comment explaining Item 3.
* arm_auto_apply: regex compile block + class_pat / title_pat / any_pattern bindings removed; the thread body now uses the process-tree match with a placeholder for the spawned-pids source until Item 4 wires it.
- src/team.rs + src/engine.rs: Profile literals with window_match: Default::default() removed.
- src/hypr.rs: untouched. apply_borderless_rules kept but no longer called by the daemon.
cargo test 100/100 (3 new pid_is_ancestor tests); clippy clean.