Skip to content

hook: ask who is in front before hooking over them - #545

Merged
mgth merged 1 commit into
masterfrom
hook-exclusion-at-run
Aug 16, 2026
Merged

hook: ask who is in front before hooking over them#545
mgth merged 1 commit into
masterfrom
hook-exclusion-at-run

Conversation

@mgth

@mgth mgth commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Addresses the exclusion half of #541.

The exclusion list is only ever consulted when the foreground changes. on_focus_changed is its sole reader, and it fires on a WinEvent — so a game already in front when the engine starts, or when a layout is re-applied over it, was never announced, the pause never engaged, and LBM hooked straight over an excluded process. It stayed that way until the user alt-tabbed out and back, which is the only thing that produces the event the decision was waiting for.

That is why the reporter of #541 saw no effect from adding the game: the entry was right, it was simply never consulted at a moment when it could still matter.

// before — daemon/mod.rs, run()
load_excluded(shared);
if !shared.paused.load(Ordering::SeqCst) {
    hook::request_hook(shared);   // paused is only ever set by a focus *change*
}

Starting LBM after the game was always enough to reproduce it. Live update (#525) made it ordinary rather than rare, by re-Running on every edit of the layout.

The change

Run is the one moment the daemon decides to hook, so it is the one place the answer changes anything. It now reads the foreground itself, right after re-reading the list, and lets go of the hook if an earlier Run had already taken it over an excluded app.

Three deliberate choices:

  • An unknown foreground leaves the flag as it stands. No window, an owner that will not resolve, a platform that does not answer — all mean "unknown", not "nobody". Guessing not excluded would unpause over a game, which is the failure this PR exists to remove.
  • Linux answers None by design. The query would mean opening an X connection from whichever thread asked; the focus watcher already owns one and already reports the active window once when it connects, so the case is covered there by that initial report instead.
  • The decision is split from the OS query, so it can be tested without a desktop.

Tested

  • cargo test 84/84 on Linux — three new tests covering the excluded, not-excluded and unknown paths
  • cargo check --all-targets clean for x86_64-pc-windows-gnu, which actually compiles the Windows paths this touches rather than only the Linux ones
  • cargo clippy quiet on the touched files (the pre-existing warnings elsewhere are unchanged), cargo fmt clean on them

Not tested — needs Windows

The behaviour itself. The checklist:

  • a game from the exclusion list already in the foreground when the engine starts: the engine must not hook
  • alt-tab away from it: the engine hooks, and border crossing works again
  • alt-tab back into it: the engine lets go again
  • hitting Apply while the game is in front: still no hook (this is the live-update path that made it frequent)

Scope

This is the part of #541 that is a bug on our side. The reporter also has Pause in games switched off and border sections set to Move: 0, which explain the escapes independently of this — asked about both in the issue.

🤖 Generated with Claude Code

The exclusion list is only ever consulted when the foreground *changes*.
`on_focus_changed` is the sole reader, and it fires on a WinEvent — so a
game already in front when the engine starts, or when a layout is
re-applied over it, was never announced, the pause never engaged, and LBM
hooked straight over an excluded process. It stayed that way until the
user alt-tabbed out and back, which is the only thing that produces the
event the decision was waiting for.

Reported in #541, where adding the game to the exclusion list changed
nothing: the entry was right, it was simply never consulted at a moment
when it could still matter. Starting LBM after the game was always enough
to reproduce it, but live update (#525) made it ordinary rather than
rare by re-Running on every edit of the layout.

`Run` is the one moment the daemon decides to hook, so it is the one
place the answer changes anything: it now reads the foreground itself,
right after re-reading the list, and lets go of the hook if an earlier
Run had already taken it over an excluded app.

An unknown foreground — no window, an owner that will not resolve, a
platform that does not answer — leaves the flag as it stands rather than
guess. Guessing "not excluded" would unpause over a game, which is the
failure this commit exists to remove.

Linux answers `None` by design: the query would mean opening an X
connection from whichever thread asked, and the focus watcher already
owns one and already reports the active window once when it connects, so
the case is covered there by that initial report.

The decision is split from the OS query so it can be tested without a
desktop; three tests cover the excluded, not-excluded and unknown paths.

Tested: cargo test 84/84 on Linux, cargo check --all-targets clean for
x86_64-pc-windows-gnu (which compiles the Windows paths this touches),
clippy quiet on the touched files, fmt-clean on them.

Not tested: the Windows behaviour itself. Needs a run with a game in the
exclusion list already in the foreground when the engine starts — the
engine must not hook — and the same after alt-tabbing away and back.

Co-Authored-By: Claude <noreply@anthropic.com>
@mgth
mgth merged commit 7db2e35 into master Aug 16, 2026
2 checks passed
@mgth
mgth deleted the hook-exclusion-at-run branch August 16, 2026 14:08
@mgth

mgth commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

Correction to this PR's rationale, from the reporter of #541 after it was merged.

The commit message and the description above both say live update (#525) is what made this frequent. That is wrong for the case that produced the report: the reporter uses Apply On Click, not Live Update, and never had live update on when the escapes happened.

The real trigger is display changes. The daemon unhooks itself over any display change, and the UI re-hooks once the configuration settles (MainService.DisplayChangedAsyncEnsureEngineHookedAsyncStartAsync) — which sends Run. With three monitors and only two powered on, plus a game changing display mode on fullscreen transitions and DPMS on the idle one, that path runs often. That is the loop the reporter described: escapes appear while the cursor is captured, and stop once an alt-tab finally produces the focus event the decision was waiting for.

The fix itself is unaffected — Run was and is the one place the foreground had to be read, and it is now read there. But the "how you get there" in the commit message names the wrong path, so it is recorded here rather than left to mislead whoever reads it next.

Worth noting that the UI already assumed the pause would survive that re-hook: MainService.EnsureRunningAfterResumeAsync carries a comment about "a legitimately-paused engine (excluded app focused at wake)", and bounds its retries for exactly that reason. The daemon simply did not honour the assumption.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant