feat(nix): fullscreen and the user's maximize are shared scenarios

Platform.fullscreen and Platform.user_maximize take a window fullscreen or
maximize it as the user would, without asking the daemon. The scenarios
maximizing.user and fullscreen check that the daemon takes the window out
of the layout, leaves it alone, and puts it back. The KDE extras keep the
on-top screenshot, the daemon's log and the setup.
This commit is contained in:
LGUG2Z committed 2026-10-30 12:53:27 -07:00
1 parent 595706d66f
commit cb134e7908
12 files changed
+421 -219

No files matched your search

+18
View File
@@ -203,6 +203,24 @@ Ported whole as the extra `kwin_restart.run` (two restarts, 19 checks as run). `
that restarted KWin once with the same four checks; it is gone, and `core-e2e` now only opens the
window `afterwards` that its last section looks for.
## features and user-maximize
`Platform.fullscreen(client, on)` and `Platform.user_maximize(client, on)` are the user's fullscreen
and maximize, which the daemon is not asked to do.
| Was | Now |
|---|---|
| features A, the window fills the work area, is in the daemon's state as the maximized window, and goes back to its tile in a container | shared `maximizing.run`; extra `features.run` keeps what KWin says: the frame of the tile before and after, and that it is on top, by a screenshot |
| features E, a fullscreen window covers the monitor, a window opening meanwhile does not take it out, back from fullscreen it is in a tile and not on another window | shared `fullscreen` (the suite runs it after the extra, on the windows the sections leave: `features.fullscreen_setup`) |
| user-maximize 1, the user maximizes: the daemon has it as its maximized window, it fills the work area, its tile is gone | shared `maximizing.user` |
| user-maximize 2, what is asked of the layout does nothing; the others did not move; nothing floats or is in the monocle | shared `maximizing.user` |
| user-maximize 2, the daemon's log notes that it ignored it; 1, the log has the `MaximizeChange` event | extra `user_maximize.log` |
| user-maximize 3 and 4, `toggle-maximize` and the user's restore put it back in its tile | shared `maximizing.user` |
Not kept: for features A, "komorebi still has all three" (the shared scenario checks the count where
it is a check of the window it moves). The shared `fullscreen` is looser than E in one way: the
window covers the display up to a pixel, where E compared the frame with the monitor exactly.
## The KDE-only suites
Moved as they were into `komorebi_vm.platforms.kde.extras`, one module per suite (`kwin`,
@@ -252,6 +252,18 @@ class Platform(Protocol):
size."""
...
def fullscreen(self, client: Client, on: bool = True) -> None:
"""Takes the window fullscreen, as the program's own key does it (`on`), or brings it back
from fullscreen (`on` false). The daemon is not asked: it learns of it from the system, and
has to leave the window alone. Returns when the system has done it."""
...
def user_maximize(self, client: Client, on: bool = True) -> None:
"""Maximizes the window, as its title bar button does (`on`), or restores it from being
maximized (`on` false). The daemon is not asked: it learns of it from the system. Returns
when the system has done it."""
...
def work_area(self) -> Rect:
"""The area of the first display that windows may take, in the coordinates of the
desktop: the display less what the operating system keeps for itself (a panel, a dock, the
@@ -0,0 +1,73 @@
"""A window the user takes fullscreen is left alone by the daemon, and goes back to its tile."""
from komorebi_vm.core.checks import Checks
from komorebi_vm.core.geometry import Rect
from komorebi_vm.core.platform import Client, Platform
from komorebi_vm.core.scenarios.support import focus_agrees
def _covers(frame: Rect, display: Rect) -> bool:
"""The frame has the whole display, give or take a pixel."""
return (
frame.x <= display.x + 1
and frame.y <= display.y + 1
and frame.x + frame.w >= display.x + display.w - 1
and frame.y + frame.h >= display.y + display.h - 1
)
def run(platform: Platform, checks: Checks, clients: list[Client]) -> None:
"""Takes the last of `clients` fullscreen, opens a window, and brings it back.
`clients` are tiled windows of the first workspace on the first display, each in a container
of its own. The window opened is closed at the end.
"""
target = clients[-1]
count = platform.state().managed()
display = platform.displays()[0]
platform.activate(target)
platform.wait_for(lambda: focus_agrees(platform, clients, target.title))
# the daemon asks again for the frames of what it moved, for a little while after a command
platform.wait_for(lambda: False, timeout=3)
def covers() -> bool:
window = platform.os_window(target)
return window is not None and _covers(window.frame, display)
platform.fullscreen(target)
checks.check(
"a window taken fullscreen covers the display",
platform.wait_for(covers, timeout=20),
)
extra = platform.spawn("fullscreen-extra")
checks.check(
"the daemon manages a window that opens meanwhile",
platform.wait_for(lambda: platform.state().managed() == count + 1, timeout=20),
)
platform.wait_for(lambda: False, timeout=3)
checks.check("... and it does not take the window out of fullscreen", covers())
platform.fullscreen(target, on=False)
def in_a_tile() -> bool:
window = platform.os_window(target)
return (
window is not None
and window.frame.w < display.w - 100
and window.frame.inside(display, 1)
)
checks.check(
"back from fullscreen it is in a tile, inside the display",
platform.wait_for(in_a_tile, timeout=20),
)
frames = [w.frame for c in [*clients, extra] if (w := platform.os_window(c)) is not None]
checks.check(
"... not on any other window",
not any(a.overlaps(b) for i, a in enumerate(frames) for b in frames[i + 1 :]),
)
checks.check(
"... and the daemon still has every window", platform.state().managed() == count + 1
)
platform.close(extra)
platform.wait_for(lambda: platform.state().managed() == count, timeout=20)
@@ -56,3 +56,112 @@ def run(platform: Platform, checks: Checks, clients: list[Client]) -> None:
"... and every window is tiled where the layout says",
platform.wait_for(lambda: tiled(platform, clients), timeout=20),
)
# What is asked of the layout, which a maximized window is not in: none of it does anything.
IGNORED = (
"move left",
"move right",
"move up",
"move down",
"stack left",
"stack right",
"unstack",
"cycle-move next",
"promote",
"toggle-float",
"toggle-monocle",
"resize-edge left increase",
"send-to-workspace 1",
"move-to-workspace 1",
"toggle-lock",
)
def user(platform: Platform, checks: Checks, clients: list[Client]) -> None:
"""A window the user maximizes, not the command, is taken out of the layout like one that
`toggle-maximize` maximized: the daemon has it as its maximized window, the others are left
alone, what is asked of the layout does nothing, and it goes back to its tile when it is
restored, by `toggle-maximize` or by the user.
`clients` are three tiled windows or more of the first workspace, each in a container of its
own. The first workspace has the second one, so that `send-to-workspace 1` can name it.
"""
count = len(clients)
target, other = clients[1], clients[-1]
area = platform.work_area()
tiles = positions(platform, clients)
def maximized() -> str | None:
window = platform.state().workspace().maximized
return window.title if window else None
def containers() -> int:
return len(platform.state().workspace().containers)
def fills(client: Client) -> bool:
window = platform.os_window(client)
return (
window is not None
and window.frame.w >= area.w - 2 * platform.tolerance
and window.frame.h >= area.h - 4 * platform.tolerance
)
platform.user_maximize(target)
checks.check(
f"after the user maximizes {target.title} the daemon has it as its maximized window",
platform.wait_for(lambda: maximized() == target.title, timeout=20),
)
checks.check("... it fills the work area", platform.wait_for(lambda: fills(target), timeout=10))
checks.check(
"... its tile is gone, and the daemon still has every window",
containers() == count - 1 and platform.state().managed() == count,
)
beside = positions(platform, [c for c in clients if c != target])
for command in IGNORED:
platform.komorebic_status(*command.split())
platform.wait_for(lambda: False, timeout=1)
checks.check(
"what is asked of the layout does nothing: it is still maximized, and the others did not"
" move",
fills(target)
and maximized() == target.title
and positions(platform, [c for c in clients if c != target]) == beside,
)
workspace = platform.state().workspace()
checks.check(
"... and nothing floats or is in the monocle",
containers() == count - 1 and workspace.floating == [] and workspace.monocle is None,
)
platform.komorebic("toggle-maximize")
checks.check(
f"toggle-maximize restores it: nothing is maximized and {target.title} is in its tile",
platform.wait_for(
lambda: (
maximized() is None
and containers() == count
and positions(platform, [target]) == {target.title: tiles[target.title]}
),
timeout=20,
),
)
platform.user_maximize(other)
checks.check(
f"the user maximizes {other.title} too",
platform.wait_for(lambda: maximized() == other.title and containers() == count - 1),
)
platform.user_maximize(other, on=False)
checks.check(
f"restored by the user, {other.title} is back in its tile and nothing is maximized",
platform.wait_for(
lambda: (
maximized() is None
and containers() == count
and positions(platform, [other]) == {other.title: tiles[other.title]}
),
timeout=20,
),
)
@@ -226,6 +226,13 @@ class KdePlatform:
def move_window(self, client: Client, x: int, y: int) -> None:
self.kde.sh(f"kdotool windowmove {self._wid(client)} {x} {y}")
def fullscreen(self, client: Client, on: bool = True) -> None:
self.kde.fullscreen(self.kde.wid(client.title), on)
def user_maximize(self, client: Client, on: bool = True) -> None:
wid = self.kde.wid(client.title)
self.kde.maximize(wid) if on else self.kde.unmaximize(wid)
def work_area(self) -> Rect:
"""What KWin says windows may take of the first output, which is the output less the
panels and docks the shell has made (a headless session has none, so it is all of it).
@@ -11,20 +11,24 @@ daemon:
container that does not have the focus, a stack and a monocled window that have it, and a
floating window that has it
D animation curves: two curves put a window in different places at the same moment
E fullscreen: a fullscreen window covers the monitor, is not moved, and goes back to its tile
E fullscreen: the shared `fullscreen` scenario, which the suite runs after these, on the
windows they leave (`fullscreen_setup`)
Every window is a `foot` of one color all over (`Kde.colored_window`), and a color is read at the
pixels just outside the frame of the window (`pixels.ring`); section E reads the frames KWin
reports for the windows, not a screenshot. `komorebi/tests/maximize.rs`, `maximize_commands.rs`
and `fullscreen.rs` check what the window manager does with a maximize and a fullscreen against
the fake; section A checks that KWin's own maximize does it, and section E that a window KWin has
taken fullscreen is left alone.
pixels just outside the frame of the window (`pixels.ring`). `komorebi/tests/maximize.rs`,
`maximize_commands.rs` and `fullscreen.rs` check what the window manager does with a maximize and a
fullscreen against the fake; section A checks that KWin's own maximize does it, and the shared
`fullscreen` that a window KWin has taken fullscreen is left alone. The shared `maximizing` checks
that the maximized window has the state and goes back to its tile; section A keeps what it reads in
KWin: that it is on top, from a screenshot.
"""
import time
from typing import Any
from komorebi_vm import Checks, Kde, Rect, pixels
from komorebi_vm import Checks, Kde, pixels
from komorebi_vm.core.platform import Client
from komorebi_vm.kde_platform import KdePlatform
UNIT = "komorebi.service"
# the daemon's log of the day, without colors, which every run of the unit adds to
@@ -168,23 +172,12 @@ def run(machine: Any, checks: Checks, scale: float) -> None:
)
)
checks.check(f"and is the one on top ({inside})", near(inside, BLUE))
workspace = kde.state()["monitors"]["elements"][0]["workspaces"]["elements"][0]
checks.check(
"komorebi's state has it as the maximized window, out of its containers",
workspace["maximized_window"] is not None and len(workspace["containers"]["elements"]) == 2,
)
checks.check("komorebi still has all three", kde.managed_all() == 3)
kde.komorebic("toggle-maximize")
checks.check(
"toggled back, it is in its tile again",
kde.wait_for(lambda: kde.rect(blue) == tile, timeout=20),
)
workspace = kde.state()["monitors"]["elements"][0]["workspaces"]["elements"][0]
checks.check(
"... and in a container of its own",
workspace["maximized_window"] is None and len(workspace["containers"]["elements"]) == 3,
)
# the session started the module's unit when it logged in: section A used its daemon, and the
# sections after it start their own runs
@@ -304,56 +297,21 @@ def run(machine: Any, checks: Checks, scale: float) -> None:
checks.check("Linear is less than 60% of the way", 0 <= linear < 60)
checks.check("EaseOutExpo is at least 20 points further along than Linear", expo >= linear + 20)
print("== E. fullscreen")
# This section needs the daemon running and a red, a green and a blue window open and tiled, and
# does not depend on which section came before it: when one has stopped the daemon or closed the
# windows, they are started and opened again.
if not answering():
def fullscreen_setup(machine: Any, scale: float) -> tuple[KdePlatform, list[Client]]:
"""What the shared `fullscreen` needs: the daemon running, and a red, a green and a blue window
open and tiled. When an earlier section stopped the daemon or closed the windows, they are
started and opened again."""
kde = Kde.session(machine, scale=scale)
if kde.run("komorebic monitor-information")[0] != 0:
kde.systemctl("start", UNIT)
kde.wait_for(answering, timeout=60)
for title, background in [("red", "ff0000"), ("green", "00ff00"), ("blue", "0000ff")]:
if kde.wid(title) == "":
open_window(title, background)
kde.wait_for(lambda: kde.run("komorebic monitor-information")[0] == 0, timeout=60)
monitor = kde.state()["monitors"]["elements"][0]["size"]
platform = KdePlatform(kde, (monitor["right"], monitor["bottom"]), session=True)
clients = [Client(title) for title in ("red", "green", "blue")]
for client in clients:
if kde.wid(client.title) == "":
platform.spawn(client.title)
kde.wait_for(lambda: kde.managed() == 3, timeout=30)
kde.wait_for(lambda: kde.focused_title() == "blue", timeout=20)
blue = kde.wid("blue")
monitor = kde.state()["monitors"]["elements"][0]["size"]
whole = Rect(monitor["left"], monitor["top"], monitor["right"], monitor["bottom"])
# the bridge asks again for the position of a window that is not where komorebi put it for a second
# after it did, which is not the thing tested here
time.sleep(3 * SCALE)
kde.fullscreen(blue)
checks.check(
f"the window covers the monitor, panels included ({kde.rect(blue)}, the monitor is {whole})",
kde.wait_for(lambda: kde.rect(blue) == whole, timeout=20),
)
open_window("yellow", "ffff00")
checks.check("komorebi manages the fourth window", kde.wait_for(lambda: kde.managed() == 4))
time.sleep(3 * SCALE)
checks.check(
f"a window opening meanwhile does not take it out of fullscreen ({kde.rect(blue)})",
kde.rect(blue) == whole,
)
kde.fullscreen(blue, on=False)
def in_a_tile(r: Rect) -> bool:
"""Smaller than the monitor, and inside it."""
return (
r.w < whole.w - 100
and r.x >= whole.x
and r.y >= whole.y
and r.x + r.w <= whole.x + whole.w
and r.y + r.h <= whole.y + whole.h
)
checks.check(
"back from fullscreen it is in a tile",
kde.wait_for(lambda: in_a_tile(kde.rect(blue)), timeout=20),
)
back = kde.rect(blue)
print(f" blue is back at {back}")
checks.check(
"and it does not sit on any other window",
not any(back.overlaps(kde.rect(kde.wid(other))) for other in ("red", "green", "yellow")),
)
checks.check("komorebi still has all four", kde.managed_all() == 4)
return platform, clients
@@ -2,161 +2,58 @@ r"""A window the user maximizes, in a real headless KWin. The user's title bar m
without asking komorebi: KWin does it and reports it, and komorebi has to take the window out of
the layout as it does for `toggle-maximize`, leave the layout alone while the window is maximized,
and put the window back in its tile when it is restored, by the user or by komorebi.
`komorebi/tests/user_maximize.rs` checks what the window manager does against the fake; this
checks that KWin reports the user's maximize, and the restore, the way komorebi reads them.
`komorebi/tests/user_maximize.rs` checks what the window manager does against the fake.
The shared `maximizing.user` makes those checks through the neutral platform. What is here is the
start (`start`: a daemon with padding and no animation, and three tiled windows) and what only the
daemon's log of this port says (`log`): that KWin's report was read as a change of the window's
maximized state, and that what was asked of the layout was noted as ignored.
"""
import json
import shlex
import time
from typing import Any
from komorebi_vm import daemon
from komorebi_vm.core.checks import Checks
from komorebi_vm.core.platform import Client
from komorebi_vm.kde_platform import KdePlatform
from komorebi_vm import Checks, Kde, Rect, daemon
W, H = 1920, 1080
# The windows are tiled with a gap of 10 pixels around the workspace and between the tiles: a tile
# has padding, and a maximized window has none. The windows are not animated.
CONFIG = "/tmp/user-maximize-config.json"
CONFIG = {
"default_workspace_padding": 10,
"default_container_padding": 10,
"animation": {"enabled": False},
}
def run(machine: Any, checks: Checks, scale: float) -> None:
"""Runs the checks of the suite `user-maximize`, which the suite script calls."""
SCALE = scale # noqa: N806
kde = Kde.headless(machine, "main", size=(W, H), scale=SCALE)
def workspace() -> dict[str, Any]:
return kde.state()["monitors"]["elements"][0]["workspaces"]["elements"][0]
def containers() -> int:
return len(workspace()["containers"]["elements"])
def maximized_title() -> str | None:
window = workspace()["maximized_window"]
return None if window is None else window["details"]["title"]
def fills(r: Rect) -> bool:
"""The work area is the whole output: a virtual KWin has no panel."""
return r.w >= W - 40 and r.h >= H - 80
def rect_of(title: str) -> Rect:
return kde.rect(kde.wid(title))
def open_window(title: str) -> None:
kde.spawn(f"foot -T {title} sleep 3000")
kde.wait_for(lambda: kde.wid(title) != "", timeout=30)
print("== three windows, tiled")
kde.sh(
"printf %s "
+ shlex.quote(
json.dumps(
{
"default_workspace_padding": 10,
"default_container_padding": 10,
"animation": {"enabled": False},
}
)
)
+ f" > {CONFIG}"
)
kde.start_komorebi("--config", CONFIG)
kde.wait_for(lambda: kde.run("komorebic monitor-information")[0] == 0, timeout=30)
for title in ("um-a", "um-b", "um-c"):
open_window(title)
checks.check("komorebi has three tiles", kde.wait_for(lambda: containers() == 3, timeout=20))
# The bridge asks again for the position of a window that is not where komorebi put it for a second
# after it did (PLATFORM.md, "How clients answer a frame"), which would put a window that is
# maximized in that second back where its tile was: wait that second out, as a person would.
kde.wait_for(lambda: kde.focused_title() == "um-c", timeout=20)
time.sleep(3 * SCALE)
tiles = {title: rect_of(title) for title in ("um-a", "um-b", "um-c")}
print(f" the tiles are {tiles}")
print("== 1. the user maximizes a window with its title bar")
kde.maximize(kde.wid("um-b"))
def start(platform: KdePlatform, checks: Checks) -> list[Client]:
"""Starts the daemon with the config, opens three windows and waits for them to be tiled."""
platform.restart_with(CONFIG)
clients = [platform.spawn(title) for title in ("um-a", "um-b", "um-c")]
checks.check(
"komorebi has it as its maximized window",
kde.wait_for(lambda: maximized_title() == "um-b", timeout=20),
"komorebi has three tiles",
platform.wait_for(
lambda: len(platform.state().workspace().containers) == len(clients), timeout=20
),
)
checks.check(
f"it fills the work area ({rect_of('um-b')})",
kde.wait_for(lambda: fills(rect_of("um-b")), timeout=10),
)
checks.check("and its tile is gone: two are left", containers() == 2)
# The bridge asks again for the position of a window that is not where komorebi put it for a
# second after it did (PLATFORM.md, "How clients answer a frame"), which would put a window that
# is maximized in that second back where its tile was: wait that second out, as a person would.
platform.wait_for(lambda: platform.state().focused_title() == "um-c", timeout=20)
platform.wait_for(lambda: False, timeout=3)
return clients
def log(platform: KdePlatform, checks: Checks, clients: list[Client]) -> None:
"""The daemon's log says it handled KWin's report as a change of the maximized state, and
that it ignored what was asked of the layout meanwhile."""
text = platform.kde.komorebi_log()
checks.check(
"komorebi handled it as a change of the window's maximized state",
any(
event.kind == "MaximizeChange" and event.notification == "WindowMaximizedChanged"
for event in daemon.events(kde.komorebi_log())
for event in daemon.events(text)
),
)
checks.check("it still has all three windows", kde.managed_all() == 3)
print("== 2. what is asked of the layout does nothing")
beside = {title: rect_of(title) for title in ("um-a", "um-c")}
for command in [
"move left",
"move right",
"move up",
"move down",
"stack left",
"stack right",
"unstack",
"cycle-move next",
"promote",
"toggle-float",
"toggle-monocle",
"resize-edge left increase",
"send-to-workspace 1",
"move-to-workspace 1",
"toggle-lock",
]:
kde.komorebic(*command.split())
time.sleep(1 * SCALE)
checks.check(
"it is still maximized, and still komorebi's maximized window",
fills(rect_of("um-b")) and maximized_title() == "um-b",
"komorebi noted that it ignored what was asked of the layout",
"ignoring" in text and "the window in front is maximized" in text,
)
checks.check(
"the others did not move, and nothing floats or is in the monocle",
{title: rect_of(title) for title in ("um-a", "um-c")} == beside
and containers() == 2
and workspace()["floating_windows"]["elements"] == []
and workspace()["monocle_container"] is None,
)
checks.check(
"komorebi noted that it ignored them",
"ignoring" in kde.komorebi_log()
and "the window in front is maximized" in kde.komorebi_log(),
)
print("== 3. toggle-maximize restores it")
kde.komorebic("toggle-maximize")
checks.check(
f"it is back in its tile ({tiles['um-b']})",
kde.wait_for(lambda: rect_of("um-b") == tiles["um-b"], timeout=20),
)
checks.check(
"and nothing is maximized",
maximized_title() is None and containers() == 3,
)
print("== 4. the user restores it with the title bar")
kde.maximize(kde.wid("um-c"))
checks.check(
"maximized again",
kde.wait_for(lambda: maximized_title() == "um-c" and containers() == 2, timeout=20),
)
kde.unmaximize(kde.wid("um-c"))
checks.check(
f"restored by the user, it is back in its tile ({tiles['um-c']})",
kde.wait_for(lambda: rect_of("um-c") == tiles["um-c"], timeout=20),
)
checks.check(
"and nothing is maximized",
kde.wait_for(lambda: maximized_title() is None and containers() == 3, timeout=10),
)
kde.komorebic("stop")
kde.wait_for(lambda: not kde.komorebi_running())
kde.stop()
+54 -1
View File
@@ -16,6 +16,20 @@ from komorebi_vm.core.scenarios.commands import PRINTED
from komorebi_vm.core.snapshot import Snapshot, parse
SCREEN = Rect(0, 0, 1920, 1080)
# what a maximized window is not in, so that asking it does nothing
ASKED_OF_THE_LAYOUT = (
"move",
"stack",
"unstack",
"cycle-move",
"promote",
"toggle-float",
"toggle-monocle",
"resize-edge",
"send-to-workspace",
"move-to-workspace",
"toggle-lock",
)
def _place_index(place: str, count: int) -> int:
@@ -78,6 +92,8 @@ class FakePlatform:
follows_user_moves: bool = True,
resizable: bool = True,
follows_modes: bool = True,
keeps_fullscreen: bool = True,
ignores_commands_when_maximized: bool = True,
) -> None:
self.panel = panel
"""The height of a panel along the bottom of the first display, which the work area leaves
@@ -86,6 +102,11 @@ class FakePlatform:
"""A daemon that tiles on the whole display, under the panel."""
self.second = second
"""The rectangle of the second display, when it is not one of the first's size."""
self.keeps_fullscreen = keeps_fullscreen
self.ignores_commands_when_maximized = ignores_commands_when_maximized
self.fullscreened: str | None = None
self.max_home: tuple[int, int] | None = None
"""Where the window the user maximized was, to put it back."""
self.resizable = resizable
self.follows_modes = follows_modes
self.mode: dict[int, tuple[int, int]] = {}
@@ -311,6 +332,8 @@ class FakePlatform:
self.containers.append([title])
if len(self.containers) > len(self.tiles):
self._relayout()
if not self.keeps_fullscreen:
self.fullscreened = None
if self.applies_rules_on_event:
self.komorebic("enforce-workspace-rules")
return Client(title)
@@ -462,6 +485,26 @@ class FakePlatform:
def work_area(self) -> Rect:
return self.area
def fullscreen(self, client: Client, on: bool = True) -> None:
self.fullscreened = client.title if on else None
def user_maximize(self, client: Client, on: bool = True) -> None:
if on:
self.max_home = self._find(client.title)
self._find_remove(client.title)
self.maximized = client.title
self.focus = max(0, min(self.focus, len(self.containers) - 1))
elif self.maximized == client.title:
self._restore_maximized()
self._relayout()
def _restore_maximized(self) -> None:
if self.max_home is not None and self.maximized is not None:
space, index = self.max_home
self.spaces[space].insert(min(index, len(self.spaces[space])), [self.maximized])
self.max_home = None
self.maximized = None
def minimize(self, client: Client) -> None:
self._find_remove(client.title)
self.minimized.append(client.title)
@@ -487,6 +530,8 @@ class FakePlatform:
return f"/tmp/{name}"
def os_window(self, client: Client) -> OsWindow | None:
if client.title == self.fullscreened:
return OsWindow(client.title, Rect(0, 0, 1920, 1080), focused=True)
if client.title == self.maximized:
return OsWindow(client.title, self.tiled_area, focused=True)
if client.title in self.minimized:
@@ -532,6 +577,10 @@ class FakePlatform:
def komorebic(self, *args: str) -> str:
n = len(self.containers)
# a daemon that is wrong obeys one of them, to be told by the windows that moved
obeyed = not self.ignores_commands_when_maximized and args[0] == "promote"
if self.maximized is not None and args[0] in ASKED_OF_THE_LAYOUT and not obeyed:
return ""
match args:
case ("stop",):
self.running = False
@@ -637,7 +686,11 @@ class FakePlatform:
self.container_padding = int(size)
self._relayout()
case ("toggle-maximize",):
self.maximized = None if self.maximized else self.containers[self.focus][0]
if self.maximized is not None:
self._restore_maximized()
self._relayout()
else:
self.maximized = self.containers[self.focus][0]
case ("resize-axis", "horizontal", way):
d = 50 if way == "increase" else -50
i = self.focus
+40
View File
@@ -6,8 +6,10 @@ from komorebi_vm.core.geometry import Rect
from komorebi_vm.core.scenarios import (
commands,
display_change,
fullscreen,
handover,
hotplug,
maximizing,
monocle,
placement,
queries,
@@ -272,3 +274,41 @@ def test_a_platform_with_no_pointer_skips_the_edge_cases() -> None:
base = checks.passed
resize.edge_cases(platform, checks, clients)
assert (checks.passed, checks.failed) == (base, 0)
def test_a_daemon_that_leaves_a_fullscreen_window_alone_passes_fullscreen() -> None:
platform = FakePlatform()
checks = Checks()
clients = opened(platform, checks)
base = checks.passed
fullscreen.run(platform, checks, clients)
assert checks.failed == 0, checks.failures
assert checks.passed - base == 6
assert platform.state().managed() == len(clients)
def test_a_daemon_that_takes_a_window_out_of_fullscreen_fails_fullscreen() -> None:
platform = FakePlatform(keeps_fullscreen=False)
checks = Checks()
clients = opened(platform, checks)
fullscreen.run(platform, checks, clients)
assert any("does not take the window out of fullscreen" in f for f in checks.failures)
def test_a_daemon_that_takes_up_a_maximize_passes_user_maximize() -> None:
platform = FakePlatform()
checks = Checks()
clients = opened(platform, checks)
base = checks.passed
maximizing.user(platform, checks, clients)
assert checks.failed == 0, checks.failures
assert checks.passed - base == 8
def test_a_daemon_that_obeys_commands_when_maximized_fails_user_maximize() -> None:
platform = FakePlatform(ignores_commands_when_maximized=False)
checks = Checks()
clients = opened(platform, checks)
platform.activate(clients[-1])
maximizing.user(platform, checks, clients)
assert any("what is asked of the layout does nothing" in f for f in checks.failures)
+6
View File
@@ -106,6 +106,12 @@ class FakePlatform:
def activate(self, client: Client) -> None:
pass
def fullscreen(self, client: Client, on: bool = True) -> None:
pass
def user_maximize(self, client: Client, on: bool = True) -> None:
pass
def screenshot(self, name: str) -> Image.Image:
return Image.new("RGB", (4, 4))
+16 -4
View File
@@ -17,11 +17,12 @@
# pixels just outside the frame of the window (`pixels.ring`); section E reads the frames KWin
# reports for the windows, not a screenshot. `komorebi/tests/maximize.rs`, `maximize_commands.rs`
# and `fullscreen.rs` check what the window manager does with a maximize and a fullscreen against
# the fake; section A checks that KWin's own maximize does it, and section E that a window KWin has
# taken fullscreen is left alone.
# the fake; section A checks that KWin's own maximize does it, and the shared `fullscreen`, run
# after the sections, that a window KWin has taken fullscreen is left alone.
from komorebi_vm import Checks
from komorebi_vm.core.suite import platform_only, run_suite
from komorebi_vm.core.scenarios import fullscreen
from komorebi_vm.core.suite import platform_only, run_suite, shared
from komorebi_vm.platforms.kde.extras import features as kde_features
start_all()
@@ -29,5 +30,16 @@ machine.wait_for_unit("multi-user.target")
checks = Checks()
run_suite(checks, platform_only("kde", kde_features.run, machine, checks, SCALE))
def taken_fullscreen() -> None:
platform, clients = kde_features.fullscreen_setup(machine, SCALE)
fullscreen.run(platform, checks, clients)
run_suite(
checks,
platform_only("kde", kde_features.run, machine, checks, SCALE),
shared(taken_fullscreen),
)
checks.finish()
+22 -5
View File
@@ -2,18 +2,35 @@
# without asking komorebi: KWin does it and reports it, and komorebi has to take the window out of
# the layout as it does for `toggle-maximize`, leave the layout alone while the window is maximized,
# and put the window back in its tile when it is restored, by the user or by komorebi.
# `komorebi/tests/user_maximize.rs` checks what the window manager does against the fake; this
# checks that KWin reports the user's maximize, and the restore, the way komorebi reads them.
# `komorebi/tests/user_maximize.rs` checks what the window manager does against the fake. The shared
# scenario `komorebi_vm.core.scenarios.maximizing.user` makes those checks through the neutral
# platform; `komorebi_vm.platforms.kde.extras.user_maximize` starts the daemon and reads the
# daemon's log for what KWin reported.
from komorebi_vm import Checks
from komorebi_vm.core.suite import platform_only, run_suite
from komorebi_vm import Checks, Kde
from komorebi_vm.core.scenarios import maximizing
from komorebi_vm.core.suite import platform_only, run_suite, shared
from komorebi_vm.kde_platform import KdePlatform
from komorebi_vm.platforms.kde.extras import user_maximize as kde_user_maximize
W, H = 1920, 1080
start_all()
machine.wait_for_unit("multi-user.target")
machine.wait_for_unit("user@1000.service")
checks = Checks()
kde = Kde.headless(machine, "main", size=(W, H), scale=SCALE)
platform = KdePlatform(kde, (W, H))
clients = kde_user_maximize.start(platform, checks)
run_suite(checks, platform_only("kde", kde_user_maximize.run, machine, checks, SCALE))
run_suite(
checks,
shared(maximizing.user, platform, checks, clients),
platform_only("kde", kde_user_maximize.log, platform, checks, clients),
)
kde.komorebic("stop")
kde.wait_for(lambda: not kde.komorebi_running())
kde.stop()
checks.finish()