feat(nix): the focus_follows_mouse scenario, with a pointer

With focus follows mouse on, the pointer over a window gives it the
focus, and the daemon follows; turned off, it moves nothing; turned on
again, it follows again. The scenario needs a pointer and is skipped
without one. The toggle passes --implementation windows, which kfw's
subcommand requires. drag-resize runs it after resize.
This commit is contained in:
LGUG2Z committed 2026-10-30 12:03:00 -07:00
1 parent 7e271955d7
commit ddab35b5b9
4 files changed
+122 -1

No files matched your search

@@ -0,0 +1,78 @@
"""With focus follows mouse on, the window under the pointer has the focus; with it off, not."""
from komorebi_vm.core.checks import Checks
from komorebi_vm.core.platform import Client, Platform
def _middle(platform: Platform, client: Client) -> tuple[int, int] | None:
window = platform.os_window(client)
if window is None:
return None
return window.frame.x + window.frame.w // 2, window.frame.y + window.frame.h // 2
def _focused(platform: Platform, client: Client) -> bool:
window = platform.os_window(client)
return window is not None and window.focused
def _settle(platform: Platform) -> None:
"""Gives a platform the time to take up a setting: KWin reads its focus policy about 200 ms
after it is written, and a pointer that is moved before that is moved under the old one."""
platform.wait_for(lambda: False, timeout=1)
def run(platform: Platform, checks: Checks, first: Client, second: Client) -> None:
"""Turns focus follows mouse on, moves the pointer over `first` and `second` in turn, turns it
off, and on again, and checks where the focus is.
`first` and `second` are two tiled windows in view, not on each other. The setting is left off
at the end. The platform needs a pointer; without one the scenario says so and stops.
"""
if not platform.has("pointer"):
print(" skipped: this platform has no pointer")
return
a, b = _middle(platform, first), _middle(platform, second)
checks.check("both windows have a middle for the pointer", a is not None and b is not None)
if a is None or b is None:
return
platform.komorebic("focus-follows-mouse", "enable")
_settle(platform)
platform.pointer.move(*a)
checks.check(
"the pointer over the first window gives it the focus",
platform.wait_for(lambda: _focused(platform, first), timeout=10),
)
platform.pointer.move(*b)
checks.check(
"over the second, the second",
platform.wait_for(lambda: _focused(platform, second), timeout=10),
)
platform.pointer.move(*a)
checks.check("and back", platform.wait_for(lambda: _focused(platform, first), timeout=10))
checks.check(
"the daemon follows: it has the first window focused",
platform.wait_for(lambda: platform.state().focused_title() == first.title, timeout=10),
)
platform.komorebic("toggle-focus-follows-mouse", "--implementation", "windows")
_settle(platform)
platform.pointer.move(*b)
checks.check(
"turned off, the pointer over the second window no longer moves the focus",
not platform.wait_for(lambda: _focused(platform, second), timeout=3)
and _focused(platform, first),
)
platform.komorebic("focus-follows-mouse", "enable")
_settle(platform)
# the pointer rests over the second window: it has to go into a window to be followed
platform.pointer.move(*a)
platform.wait_for(lambda: _focused(platform, first), timeout=10)
platform.pointer.move(*b)
checks.check(
"on again, the pointer over the second window gives it the focus",
platform.wait_for(lambda: _focused(platform, second), timeout=10),
)
platform.komorebic("focus-follows-mouse", "disable")
+10
View File
@@ -31,6 +31,10 @@ class FakePointer:
def move(self, x: int, y: int) -> None:
self.at = (x, y)
if self.platform.follows_mouse:
for i, tile in enumerate(self.platform.tiles[: len(self.platform.containers)]):
if tile.x <= x < tile.x + tile.w and tile.y <= y < tile.y + tile.h:
self.platform.focus = i
def down(self) -> None:
pass
@@ -68,6 +72,8 @@ class FakePlatform:
self.rules: dict[str, int] = {}
self.ignored: set[str] = set()
self.placing = False
self.follows_mouse = False
self.mouse_ignored = False
self.prefix_rules: list[tuple[str, int]] = []
self.unmanaged: list[str] = []
self.display_count = 2
@@ -284,6 +290,10 @@ class FakePlatform:
match args:
case ("stop",):
self.running = False
case ("focus-follows-mouse", "enable" | "disable" as state):
self.follows_mouse = state == "enable" and not self.mouse_ignored
case ("toggle-focus-follows-mouse", *_):
self.follows_mouse = not self.follows_mouse and not self.mouse_ignored
case ("focus", "right"):
self.focus = min(self.focus + 1, n - 1)
case ("focus", "left"):
@@ -5,6 +5,7 @@ from komorebi_vm.core.scenarios import (
closing,
commands,
focus,
focus_follows_mouse,
hotplug,
lifecycle,
monocle,
@@ -338,3 +339,32 @@ def test_a_daemon_that_ignores_the_rules_of_its_file_fails_startup_rules() -> No
checks = Checks()
startup_rules.run(platform, checks)
assert checks.failed >= 3, checks.failures
def test_a_daemon_that_follows_the_mouse_passes_focus_follows_mouse() -> None:
platform = FakePlatform()
checks = Checks()
clients = opened(platform, checks)
base = checks.passed
focus_follows_mouse.run(platform, checks, clients[0], clients[1])
assert checks.failed == 0, checks.failures
assert checks.passed - base == 7
assert not platform.follows_mouse
def test_a_daemon_that_does_not_follow_the_mouse_fails_focus_follows_mouse() -> None:
platform = FakePlatform()
platform.mouse_ignored = True
checks = Checks()
clients = opened(platform, checks)
focus_follows_mouse.run(platform, checks, clients[0], clients[1])
assert checks.failed >= 2, checks.failures
def test_focus_follows_mouse_is_skipped_without_a_pointer() -> None:
platform = FakePlatform(pointer=False)
checks = Checks()
clients = opened(platform, checks)
base = checks.passed
focus_follows_mouse.run(platform, checks, clients[0], clients[1])
assert checks.failed == 0 and checks.passed == base
+4 -1
View File
@@ -19,7 +19,7 @@ import time
from komorebi_vm import Checks, Kde, Pointer, daemon
from komorebi_vm.core.platform import Client
from komorebi_vm.core.scenarios import resize
from komorebi_vm.core.scenarios import focus_follows_mouse, resize
from komorebi_vm.kde_platform import KdePlatform
# the module's unit, which the session starts when it logs in: the daemon the suite drags against
@@ -274,4 +274,7 @@ resize.run(
[Client(f"win{n}") for n in range(1, WINDOWS + 1)],
)
print("== focus follows mouse, through the neutral platform")
focus_follows_mouse.run(KdePlatform(kde, (W, H), pointer), checks, Client("win1"), Client("win3"))
checks.finish()