mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-14 12:53:43 +01:00
3a3a0f8f24f89a80e49707cf06e4e5be23e7e044
36
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3a3a0f8f24 |
feat(wm): JSON schemas, and the module's typed options
komorebi.json is described as a JSON Schema, made from the Rust types that read it: komorebic prints the schema of komorebi.json, of an applications.json, of a notification and of a message, and the daemon answers the same four questions on its socket. schema.json and schema.asc.json are committed (just jsonschema). The module's config option is typed from the schema: schemars-nixgen makes nix/komorebi-options.nix from schema.json (just nixgen), so a layout komorebi does not have, or a value of the wrong kind, does not evaluate. The module writes config as a komorebi.json and gives it to the daemon with --config. configFile and the file the module writes are checked against the schema when the system is built. applicationSpecificConfiguration takes files from GitHub, and there are none by default, since no community file for Plasma is known. Two checks keep the generated files honest without the tools that make them. schema-fresh compares the schemas with what komorebic prints, and the hash on the first line of the options with the schema. nix-options-validation evaluates the options against the example configuration, written in Nix and in JSON. nixos-module-eval also evaluates the module with the sources turned off: with applicationSpecificConfiguration.enable = false the sources are left out of the config that is written, and with no config either there is nothing to write and the unit passes no --config. ARCHITECTURE has the guarantee row that names the two checks. |
||
|
|
ed34286bca |
feat(wm): application specific configuration
Rules for particular applications can be kept in files of their own, applications.json, which komorebi.json names with app_specific_configuration_path: one path or a list. The ignore, manage and floating rules of every application in a file are added to the lists the configuration's own rules go in, so a file can be shared or kept apart from the settings. A path with no file behind it is skipped with a warning, since one komorebi.json is meant to work on every platform and may name a file that only another platform has; a file that is there and is not an applications.json is an error. The titleless and tabbed rules of other platforms are read and do nothing. komorebic/applications.json is a start for Plasma: it floats the calculators, the screenshot tool, the authentication dialog and System Settings. A test opens the windows it names. |
||
|
|
5eaa613f50 |
feat(wm): configuration in komorebi.json
The daemon takes its settings from a komorebi.json: the file --config names, or the one in the config home ($KOMOREBI_CONFIG_HOME, else ~/.config/komorebi). Without a file it runs on its defaults, as before. StaticConfig reads the file in three steps, because the monitors are known only after the backend has been asked. preload makes the window manager and applies the settings that are process-wide, postload gives each monitor and workspace the entry of its place (by the index display_index_preferences gives a monitor's serial number id, else in order), and main asks KWin for a desktop for each workspace the file lists. A watcher on the file sends the daemon a ReloadStaticConfiguration about two seconds after it changes, whether an editor wrote the file in place or renamed a new one over it, and a file that does not parse changes nothing. A workspace can change its layout, its work area offset and its layout options with the number of its containers, from the file or with komorebic (workspace-layout-rule, its named form and the two that clear), and layout_defaults gives every workspace of a layout the options it sets none of. One file is meant to serve every platform, so a key that does nothing on Plasma is accepted and ignored. config_parity checks the example and the files of other platforms; the keys of features that are not in yet are listed there, and the test fails when a list names a key that is read. The file also sets what the stacks, the float override, the floating layer and monocle set while the daemon runs: window_container_behaviour, with its rules by container count in a workspace, float_override, float_override_placement, floating_layer_behaviour (for a monitor and a workspace too), floating_layer_placement and monocle_focus_behaviour. Their features come before this commit, but a file to put the keys in does not, so they are part of this one. komorebic gains configuration, generate-static-config and start --config. The NixOS module gets configFile, which the unit gives the daemon as --config. |
||
|
|
12b0aa9301 |
feat(wm): the floating layer
A workspace with floating windows had no way to say which of the two kinds of windows a focus or move command was about, so the commands always acted on the tiles. A workspace is now on one of two layers, tiling or floating, and `komorebic focus`, `cycle-focus`, `move` and the resize commands act on the windows of that layer. Floating a window puts the workspace on the floating layer, and so does a window that a float rule floats when it opens and a focus change to a floating window; putting the window back, tiling a new window and a focus change to a tile put it on the tiling layer. `komorebic toggle-workspace-layer` switches it by hand: the windows of the layer it goes to are raised, the focused one last, and one of them is given the focus. Going to the tiles hides the floating windows, and going back to the floating layer shows them. On the floating layer, focusing in a direction goes to the nearest floating window on that side and, where there is none, crosses to the next workspace or the monitor on that side as it does for tiles, and fails where there is none. Cycling goes round the floating windows in the order they were floated, `move` moves the focused floating window by the resize delta without leaving the work area, the resize commands change its frame, and `preselect-direction` does nothing. A floating window that is maximized leaves the floating windows. The layer is in the state of each workspace. A window that opens while a workspace is on its floating layer floats when the floating layer behavior says so: the workspace's own, else its monitor's, else the window manager's. All three tile by default, and only the configuration file, which is not here yet, sets the first two. komorebi/tests/floating_layer.rs drives the layers against the fake desktop, with the floating layer behavior as well, and the vm-commands sweep runs the new command. |
||
|
|
05de20749b |
feat(wm): maximize
A window could be given the work area only as a tile of the layout, which has padding around it. `komorebic toggle-maximize` has KWin maximize the focused window instead: it gets the work area of its monitor with the panels left alone, and it leaves the layout, so that it is in no container. Toggled again, the window is restored, put in a container of its own at the index it was taken from, and the workspace is tiled again. The window is kept in the workspace as its maximized window, with the index it goes back to, and the state shows both. While it is maximized the workspace asks KWin to maximize it again on every tiling, and does not move the containers behind it. The messages that are about the tiles (move, stack, resize, promote, preselect, lock, float, monocle, and the ones that send a container to another monitor or workspace) are dropped before they are handled, since there is no tile to act on. Focusing in a direction finds no container on any side of it, so the focus goes to the monitor on that side, where a maximized window or a monocle is given the focus whole. Showing the workspace again restores the maximized window and focuses it ahead of the containers' windows. A window in monocle that is maximized leaves the monocle, and one that closes takes the maximize with it. Backend gets a maximize command, which does nothing unless a backend implements it; the fake desktop and the KWin backend do, and the bridge carries it out with KWin's own `setMaximize`. A maximize also makes the bridge forget the position it was last asked to put the window at, which it would otherwise set again on the work area's size when the window's frame changed within a second of a tiling. komorebi/tests/maximize.rs and maximize_commands.rs drive it against the fake desktop, with one monitor and with two. The bridge's tests and the backend's tests read the maximize command from the same fixture, so that what one sends is what the other carries out. vm-features is a new session suite that reads screenshots: the first thing it checks is that the maximized window fills the work area, is the one on top, and goes back to its tile; the vm-commands sweep runs the new command. |
||
|
|
0d21dd332c |
feat(wm): monocle
A workspace could show its windows only side by side. Monocle gives one container the whole work area: the focused container is taken out of the workspace's containers, so the layout gives no container a tile, and the next tiling puts its window on the work area less the padding. Turning it off puts the container back at the index it came from, and the tiles come back as they were resized: the container keeps its resize adjustment, and the workspace does not shorten the list of adjustments while a monocle is on. The windows of the other containers are hidden on the parking desktop, as the windows behind the front one of a stack are, and the front window of each is shown again when the monocle is turned off or the window in monocle closes. A stack in monocle stays a stack. `komorebic toggle-monocle` turns it on and off. What focusing in a direction does meanwhile is set with `monocle-focus-behaviour` or switched with `toggle-monocle-focus-behaviour`: with `cycle` the monocle goes to the previous or the next container, with `no-op`, the default, no container is on any side and the focus falls through to the monitor on that side, where the monocle window of its workspace, if it has one, is given the focus, and the command fails where there is no monitor. A container that is moved to a monitor or sent to a workspace in monocle turns that monocle off first. `cycle-focus` always carries the monocle along. Giving the focus to another window gives it back to the monocle window, so that it stays in front. `move` and `cycle-move` are refused and `toggle-float` is ignored while a window is in monocle, since the focused container is not the window in front. The monocle container is in the state, with the index it goes back to, and a window in it that closes takes the monocle with it. komorebi/tests/monocle.rs drives it against the fake desktop, with one monitor and with two. vm-core-e2e checks in a real KWin that the focused window fills the work area, that the state has the monocle container, that toggling back restores the tile, that the cycle behavior gives another window the work area, and that the windows behind a monocle are parked, which a clean stop and the next start after a crash put right; vm-stacks checks a monocle over a stack; the vm-commands sweep runs the new commands. |
||
|
|
2e439414db |
feat(wm): cycle and focus in a stack
A stack shows one window, so there has to be a way to choose which. komorebic cycle-stack brings the next or the previous window of the focused stack to the front, wrapping around, and hides the one that was in front. focus-stack-window brings the window at an index, on the monitor the pointer is on, so that a status bar on another monitor can act on its own stack. cycle-stack-index moves the window in front one place along the stack, which changes the order the other two count by, and keeps it in front. Each command shows the window it chooses before it gives it the focus: KWin follows the focus of a window on another desktop to that desktop, and a hidden window is on the parking desktop. vm-stacks cycles a stack in a headless KWin and checks after each command that KWin and komorebi agree on the window in view and that exactly one window of the stack is out of sight. |
||
|
|
331db681e2 |
feat(wm): stack out of sight on a parking desktop
A container has one tile, so the windows of a stack that are not in front have to be out of sight. KWin has nothing a script can set to hide a window except a virtual desktop nobody looks at, which the first spike measured to cost no focus change where minimizing a window cost two. Backend::hide takes a window out of sight and Backend::restore brings it back; the KWin backend parks the window on a desktop named komorebi-park, which the table keeps out of the workspaces, and remembers where each hidden window came from so that it is still on its workspace. KWin saves a desktop in kwinrc as soon as it is made, so komorebi removes the parking desktop when it stops, after the windows are back, and the first snapshot of the next run finds one a killed run left, sends its windows home and removes it. The windows komorebi hid are recorded, so that a minimize the compositor reports for one is not mistaken for the user's. komorebic stack, unstack, stack-all and unstack-all stack and unstack windows. The window container behavior and the float override decide whether a window that opens joins the focused container or floats, for every workspace or for one. vm-stacks stacks windows in a headless KWin, shows a workspace and goes back, stops, and kills the daemon with a window parked. |
||
|
|
ac265de1e3 |
feat(wm): padding for named workspaces
workspace-padding and container-padding take the index of a workspace's monitor and its own index, so a script that names its workspaces still has to count them to pad one, while focus-named-workspace, named-workspace-layout and named-workspace-tiling already take the name. named-workspace-padding and named-workspace-container-padding take the name instead. They look the workspace up on every monitor the way the other named commands do, set the same padding the commands by index set, and change nothing for a name no workspace has. The tests check the messages the subcommands send, that a named workspace takes its paddings and the tile in front follows, and that a name nobody has changes nothing. The commands sweep of the VM suite sends both commands to a real session. |
||
|
|
189aeb1005 |
feat(wm): more than one monitor
Every monitor has workspaces of its own, which KWin's per-output desktops show independently, so a second monitor is mostly the window manager's business: which monitor has the focus, and how a window gets from one to the other. At the edge of a monitor, focus and move now cross to the monitor on that side, where they used to fail with "there is no container in this direction": the focus lands on the container nearest the monitor it came from, and a move swaps with the container at that edge (cross-monitor-move-behaviour insert puts it in among the containers there, no-op keeps it). A window that opens or gets the focus on a monitor that is not the focused one focuses that monitor, which also makes the scrolling test of a strip next to another monitor guard real code. The monitor commands come with it: focusing a monitor by index, in order or under the pointer, a workspace of another monitor, moving and sending the focused window to another monitor, to a workspace of it or to a named workspace (in the window manager these are moves between monitors, so they were left out of the workspace commands), moving and swapping workspaces between monitors, and a display index preference that keeps a monitor at the index the user gives it. eager-focus focuses the first window of an executable on any workspace of any monitor; its window is found in a container or among the floating windows, the two places a window can be until stacks, monocle and maximize exist. |
||
|
|
8467b03ac2 |
feat(wm): named workspaces and the other workspace commands
Workspace i is KWin's desktop i, and switching and moving by index came with the workspaces. What a user binds keys to besides is the rest of komorebi's workspace commands: going to the next or the previous workspace (or the next empty one), back to the last one, adding and closing workspaces, showing one index on every monitor, and setting the layout or the tiling of a workspace that need not be in front. They are made of the same two operations, showing a workspace and moving a container to one, so KWin keeps doing the showing and the hiding. A workspace gets a name, which focus-named-workspace, named-workspace-layout and named-workspace-tiling find it by on any monitor; focusing a named workspace focuses its monitor too, so focus_monitor comes with it. The name is komorebi's: naming a workspace does not rename KWin's desktop. The query focused-workspace-name answers with the name, or the index of a workspace that has none. The moves to a named workspace, and everything that addresses a monitor, are moves between monitors in the window manager and come with more than one monitor. |
||
|
|
62e37e44cf |
feat(kwin): workspaces are KWin's own desktops
komorebi's workspaces exist on Windows and macOS because neither has a virtual desktop API: switching hides every window of the workspace being left. KWin has virtual desktops, per output since 6.8, so here workspace i is KWin's desktop i on every monitor, and each monitor shows one. KWin shows, hides and focuses the windows of a desktop; komorebi asks for a switch, puts a window on a desktop, and follows what anything else does. The Backend gets the workspace methods (ensure_workspaces, switch_workspace, set_window_workspace, window_workspace, current_workspaces), and the fake its desktops. The bridge reports the desktops, the desktop each output shows and a window's desktops, and carries out output_desktop and desktop commands, typed and tested with the fixtures like the rest. The table turns a switch or a move komorebi did not make into a WorkspaceChange or a WorkspaceMove, and records its own when it sends them, so that KWin's report of those raises nothing. The window manager follows a switch with no command in reply (adopt_workspace_change), puts windows on their desktops before a desktop is shown (focusing a window first would take the monitor back), and loads the windows open at startup onto the workspace of their desktop. Per-output desktops are a kwinrc setting that is off by default, and kwriteconfig6 was not on the PATH of the user service in a live session, so komorebi edits kwinrc itself (kwin::kwinrc) and puts the user's value back when it stops, also after a crash. focus-workspace, move-to-workspace and send-to-workspace come with this commit, since the tests of switching and moving, and vm-workspace-switch, need them; the named and cycling workspace commands follow. vm-workspace-switch runs the daemon against a real KWin; vm-kwin-contract checks what komorebi relies on from KWin's per-output desktops, on a headless KWin with two outputs and no komorebi. |
||
|
|
1b10611022 |
feat(wm): ignore, manage and float rules
komorebi managed every normal window that is alive, and there was no way to keep an application out of the layout. The rule engine decides whether a window is managed: an ignore rule keeps the windows it matches out, unless a manage rule matches them too, and a float rule puts a window in the floating windows of its workspace when it opens. A rule matches a window by its title, its executable, its class or the path of its executable. Plasma's shell and KRunner are ignored from the start. `komorebic ignore-rule` and `manage-rule` add a rule, `session-float-rule` floats the focused window and the windows like it until the daemon stops, `clear-session-float-rules` removes those rules, and `global-state` shows the rules. The `DebugWindow` message says why a window is managed or not. Applications often open a window before they give it its title, so the bridge now reports a title change, and a window komorebi does not have is asked about again when its title changes, for a rule on the title to decide. The vm-commands sweep adds and shows rules, and a section of its own checks that a window an ignore rule matches by its first title is tiled once it has another. vm-kwin-events checks that a window giving itself another title reaches the daemon as a title update. |
||
|
|
c95be9391e |
feat(wm): padding and work-area offsets
Every workspace was tiled with the default padding of 5 pixels around and between its tiles, and the work area offsets could not be set. The padding commands set a workspace's own workspace padding and container padding, by the indices of its monitor and itself or for the focused workspace, and the adjust commands make the focused workspace's larger or smaller. `global-work-area-offset`, `monitor-work-area-offset` and `workspace-work-area-offset` keep a part of the work area free of tiles, and a workspace's offset now wins over its monitor's, which wins over the global one. A monitor's window-based offset applies while the workspace in front has no more windows than the monitor's limit, and `toggle-window-based-work-area-offset` turns it off and on for the focused workspace. The vm-commands sweep sets and resets every offset and padding. |
||
|
|
56294e1cd3 |
feat(wm): manage, minimize and close
Every normal window KWin reports is tiled, and the only way to take one out of the layout was to float it, which keeps it a window komorebi manages. `komorebic unmanage` takes the focused window out of the layout, and komorebi leaves it alone until `komorebic manage` takes it back or the window closes. Both are events komorebi raises itself on the channel KWin's events go through, so they are handled in order with what KWin reports. `komorebic minimize` and `close` ask KWin to minimize or close the focused window, which leaves the layout when KWin reports that it did. `komorebic toggle-pause` pauses the daemon: it acts on no event, and takes only `toggle-pause`, `state` and `stop` on its socket until it is resumed. With `unmanaged-window-operation-behaviour no-op`, the daemon refuses every message while the focused window is not one it tiles. The vm-commands sweep pauses and resumes, sets the operation behavior, manages and closes the focused window, and a section of its own minimizes, unmanages and manages a window. |
||
|
|
3cc5dceb7d |
feat(wm): lock
Containers can be locked since the model came in, and a locked container keeps its index while others are inserted, removed or swapped around it; nothing could lock one. `komorebic toggle-lock` locks the focused container, or unlocks it, and the `LockMonitorWorkspaceContainer` and `UnlockMonitorWorkspaceContainer` messages lock and unlock a container by the index of its monitor, its workspace and itself, for a program such as a status bar. The vm-commands sweep locks and unlocks a container. |
||
|
|
062fe57fac |
feat(wm): float
Some windows are better left out of the layout. `komorebic toggle-float` takes the window that has the focus out of its container into the floating windows of the workspace, which komorebi gives no tile and does not move, and the other windows share the work area; on a floating window it puts it back in a container at the focused place. A floating window is in the index of managed windows like any other, so its events find it, and it is in the state as the workspace's `floating_windows`. Where a floated window goes is the `toggle_float_placement` of the window management behaviour: by default it stays where it is, since the spike's centering of floated windows waited for a configuration file. Centering and resizing to the floating aspect ratio come with it. Opening windows floating (the float override and the float rules), the floating layer and its own focus and move commands come later. The vm-commands sweep floats a window and puts it back, and its health check counts the floating windows. |
||
|
|
554d77aed9 |
feat(wm): promote
The first tile of a layout is its largest, and the place for the window being worked on. `komorebic promote` moves the focused window there, with the windows that were before it each moving one tile on; `promote-swap` swaps it with the window that has the first tile, and from the first tile swaps it back with the window it was last swapped with; `promote-window left|right|up|down` promotes the window on that side of the focused one; and `promote-focus` gives the focus to the first tile and moves nothing. The grid layout has no largest tile, so the four do nothing there. The vm-commands sweep gains the four. |
||
|
|
95eb4d5039 |
feat(wm): resize from the CLI
`komorebic resize-edge left|right|up|down increase|decrease` moves one edge of the focused tile, and `resize-axis` both edges along an axis, by the delta that `resize-delta` sets (50 pixels to start with; komorebi-layouts moves an edge by half of it). The neighbors give up the room. A resize is an adjustment of the container's tile that the workspace keeps, one per container, and that the layout applies whenever it tiles. Before each tiling, the workspace clears the parts its layout cannot apply, such as the height of a column. No tile may end up narrower or shorter than a tenth of the work area, or than it already is: a resize that would make one so is granted as far as it can go, not dropped. `retile` drops the adjustments, and the `RetileWithResizeDimensions` message tiles with them. `save-resize` and `load-resize` write them to a file and read them back, and `quick-save-resize` and `quick-load-resize` do the same with one file in the daemon's temporary directory. The vm-commands sweep gains every resize command, and the saving and loading. |
||
|
|
37b06993d2 |
feat(wm): tune layouts, and the scrolling layout
`komorebic layout-ratios --columns ... --rows ...` sets the share of the work area the first columns and rows of the focused workspace's layout take, and `scrolling-layout-columns N` how many columns of the scrolling layout are in view. Both are layout options of the workspace, which komorebi-layouts reads. The scrolling layout is a strip of columns that runs past the edges of the work area, so its tiles are not clamped to it: the windows of the columns out of view are off the screen. When a window of a scrolling workspace gets the focus, the workspace is tiled after the focused container changed, which scrolls the strip to it. KWin moves a window anywhere, but keeps 100 pixels of a window it resizes on an output once the client has drawn at the new size, which pulls the columns out of view back onto the screen. The bridge remembers the position it was asked for, for a second, and asks again for the position alone when KWin moved the window elsewhere, three times at most. komorebi-layouts compiles `Rect::percentage_within_horizontal_bounds` only with its darwin feature, which this repository does not enable, so the workspace has its own copy of it. A new VM suite, vm-scrolling, after the spike's m2-scrolling script, checks in a real KWin that the columns out of view stay where komorebi put them and that the strip follows `komorebic focus`; vm-commands sweeps the two new commands. |
||
|
|
41c32aad8e |
feat(wm): layouts
Every workspace started in BSP and stayed there. `komorebic change-layout` gives the focused workspace one of the layouts of komorebi-layouts, `cycle-layout next|previous` steps through them, and `flip-layout` mirrors the tiles along an axis, on top of the flip there is. A flip mirrors the tiles, not the order of the containers, and the directions of focus and move are flipped with it, so `focus left` still goes to the tile on the left. `toggle-tiling` turns the tiling of the focused workspace off and on: while it is off, no window of the workspace is moved, and the windows that open are taken in and tiled once it is on again. `preselect-direction` reserves the place on one side of the focused window for the next window: an empty container holds it, the layout gives it a tile, and the next window takes it. `cancel-preselect` gives it back. A new VM suite, vm-commands, after the spike's m8 script, runs every layout with five, three, two and one windows against a real KWin with two outputs, and then every komorebic command there is, checking after each that the daemon is alive, answers, and has every window KWin has, and that KWin has the windows where the daemon asked. Every command that comes later adds its line to that sweep. |
||
|
|
1bbb534844 |
feat(wm): move and cycle containers
With the focus moving, the next thing a key has to do is move the focused window. `komorebic move left|right|up|down` swaps the focused container with the one whose tile is on that side, as the layout sees it, and `komorebic cycle-move next|previous` with the next or the previous one in the workspace's order. The workspace is tiled again, the window that moved keeps the focus, and the pointer follows it. The swap goes through the lockable sequence, so a lock stays with its index: the container moved onto a locked index is locked in its place. Moving towards a side with no window fails and moves nothing. Moving into the next monitor comes with more than one monitor, and the floating layer's own move with the floating layer. The VM test moves a window in a real KWin and back, and checks that KWin has every window where komorebi asked. |
||
|
|
20f93ba8c4 |
feat(wm): focus in a direction
A tiling window manager is driven from the keyboard, and the first thing a key has to do is move the focus. `komorebic focus left|right|up|down` gives the focus to the window whose tile is on that side of the focused one, as the layout sees it, and `komorebic cycle-focus next|previous` to the next or the previous window in the workspace's order, wrapping around. Neither moves a window: the focus changes, the tiles stay. The pointer follows the focus, through KWin's "Move Mouse to Focus", unless `mouse-follows-focus disable` (or `toggle-mouse-follows-focus`) says not to. Focusing towards a side with no window fails and changes nothing. Crossing into the next monitor, as komorebi does on the other platforms, comes with more than one monitor; so do eager-focus and the parts that depend on the monocle, maximized and floating windows. The VM test now drives the focus with komorebic in a real KWin and checks that KWin and komorebi agree on the focused window afterwards. |
||
|
|
a86e2bdc63 |
build(nix): a NixOS module for komorebi
On NixOS, running komorebi should take one flake input and one module line. The flake exports `services.komorebi` as nixosModules.default (and .komorebi). It adds the flake's overlay, which gains komorebi-full, installs komorebi and komorebic, and runs the daemon in every Plasma session as a systemd user unit, wanted by plasma-workspace.target and ordered after plasma-core.target, which Plasma orders after KWin. systemd starts a daemon that fails again; the exit status of a SIGTERM (130) counts as success, so a stopped unit is not failed. The unit's PATH is the one a terminal in the session has, instead of the few store paths NixOS gives a unit. The options are enable, package, logLevel and a read-only execStart, the command line the unit runs. There is no configFile: the daemon reads no configuration file yet, and an option that linked one would do nothing. nixos-module-eval evaluates seven scenarios as real NixOS systems and checks their assertions, the unit and execStart, with builtins.tryEval for the two that must not evaluate. Nothing in it builds a Linux package, so it runs on a Mac as well. The VM nodes import the module, so every suite runs komorebi as the module installs it. The headless node's komorebi@NAME runs execStart, which has --log-level, so the nodes set logLevel to debug and the suites stop passing it; latency, which times the daemon, keeps the default level. The session node starts the module's unit at login; render-smoke turns it off, since a tiled terminal would cover the desktop it looks at. A new fast suite, vm-module, logs in to Plasma and checks that the unit runs the daemon with the module's log level and PATH, that a window opened in the session is tiled, that a killed daemon is started again, and that a stop is not a failure. docs/installation.md covers NixOS first, and building from source on other distributions. There is no binary cache: the Komorebi license does not allow third parties to redistribute binaries. |
||
|
|
e2d757bbb0 |
feat(wm): subscribe to notifications
A program such as a status bar has to follow the window manager, and so far it could only ask: komorebi on Windows and on macOS sends notifications for that. The daemon now keeps subscribers, each a Unix socket in its data directory that a program listens on, and after each event and each message it handles, it connects to every subscriber and writes the event or the message with the state after it. A subscriber can ask to be sent only what changed the state; a window that is shown and a new subscription are always sent, so a subscriber has the state as soon as it subscribes. A subscriber that cannot be connected to is dropped with its socket file and not tried again, and a stopping daemon closes an empty connection to each one. komorebic gains subscribe-socket and unsubscribe-socket, and komorebi-client subscribe and subscribe_with_options, which bind the socket and subscribe. The tests subscribe through real sockets against the fake desktop: a shown window is sent with the state that has it, a subscriber that filters is not sent a question that changed nothing, and one that is gone is dropped. |
||
|
|
6f657e53e3 |
feat(cli): komorebic
The daemon answers on its socket, but a person has no way to write a message on it. komorebic is that way: each subcommand that asks something of the daemon is one message, sent with komorebi-client, and a question's answer is printed. komorebic is a separate program because the daemon takes no input but its socket, so a key binding, a script and a terminal all drive it the same way. It has what the daemon can answer so far: state, monitor-information, query, retile and stop. Around them, start runs the komorebi on the PATH in the background and passes it what follows a --, such as --fake-backend to try it without a compositor; log follows the daemon's log, which the daemon now writes to a file a day in LOG_DIR as well as to stdout; data-directory shows where the socket is; completions prints a shell completion script. The hidden docgen writes each subcommand's help to docs/cli, in fences that name their language and wrapped at 100 columns whatever the width of the terminal, so that the pages are the same wherever they are generated; just docgen runs it. The user guide starts here: docs/index.md for KDE Plasma, and a page on running komorebi. tests/cli.rs runs each subcommand against a socket the test serves in place of the daemon. In the VM tests the guests get komorebic next to the daemon, and vm-core-e2e reads the daemon's state with it (the windows it manages, the one it focuses, after KWin restarts too) and stops it with komorebic stop, which unloads the bridge and removes the socket. In the VM test library, Kde.state asks again, a second (times the scale) after an empty answer or a failed command, up to 10 times. The daemon drops a query when it cannot take hold of the window manager within a second, and komorebic then prints an empty line and succeeds; komorebic itself gives the daemon a second to answer and fails when that is not enough. A daemon busy with the first windows does either on a machine that is loaded or slow, and a suite that polled the state died on JSONDecodeError. An answer that is not JSON still fails at once. |
||
|
|
88aa78b874 |
feat(wm): a command socket and the first messages
Until now the daemon tiled what KWin reported and nothing else could reach it. komorebi takes its orders on a socket, as it does on Windows and on macOS: it handles no key itself, and whatever binds keys runs a command that writes a message. The daemon binds komorebi.sock in $XDG_RUNTIME_DIR/komorebi (DATA_DIR), reads one JSON SocketMessage per line on a thread per connection, and handles each message with the window manager locked, as the thread that handles events does. The first messages are the ones the first komorebic needs: State (the window manager as JSON, from state.rs), MonitorInformation, Query with a StateQuery, Retile and Stop. A question is answered on the same connection, and the answer ends when the daemon closes it. Stop unloads the bridge from KWin, removes the socket and exits; so does a stop signal. The socket is bound once the daemon is connected to KWin, so a komorebi that cannot connect, such as a second one in the same session, which the bus refuses, leaves the socket of the one that runs alone. With --fake-backend the daemon answers on its socket with no compositor at all, which is how the tests run it. komorebi-client is the other end: send_message, send_batch and send_query, and the types the messages are made of. Its doctests serve the socket themselves, tests/daemon.rs runs it against a window manager over the fake desktop, and komorebi/tests/socket.rs runs the daemon binary from its start to a Stop. |
||
|
|
4386003e30 |
feat(kwin): survive KWin restarting
KWin can go away under a running daemon while the session bus stays: it crashed, or kwin_wayland --replace took its place. Its windows go with it, and so does the bridge, which would have reported them closing; in the spikes the old windows vanished without a single removed event. What the daemon does see is the bus, so the thread that serves the connection watches NameOwnerChanged for org.kde.KWin. When the name loses its owner, Table::compositor_lost marks every window closed and returns a Destroy for each, so the window manager takes them out of its layout, and their ids are not given out again. When the name has an owner again, the daemon loads the bridge into the new KWin, trying for up to 15 seconds while KWin is not ready for it, and the bridge reports the new desktop in a snapshot. zbus does not end a held call when its caller leaves the bus, so the Poll of the old bridge is still waiting when the new bridge calls, and it was woken first: it took the commands for the new KWin and answered them to nobody. vm-kwin-restart lost the tile of a window opened in the third KWin that way in one run of three, and a recording of the bus showed the batch going to the old bridge. A Poll held since before KWin last left the bus is answered with nothing and passes its wake-up on to the next call, which a unit test holds. The VM suite vm-kwin-restart (from the spike's spike7-resync) restarts the headless KWin twice under a running daemon, through Kde.restart_kwin in the VM library, and checks that the daemon sees KWin go, takes the old windows out of its layout, loads the bridge into the new KWin, and tiles the windows that open there. |
||
|
|
b463191d00 |
feat(kwin): move and focus windows through a held call
With the commands of the Backend trait carried out, the daemon tiles a real KWin. A command looks up the UUID KWin knows the window by, is queued as JSON for the bridge, and returns; the held Poll call is answered with the queue, one millisecond after the first command, so that a retile reaches KWin as one batch. The bridge sets the frame, activates, raises, minimizes, restores or closes the window, and reports what it could not do. set_rect records the frame it asked for in the table, so the window manager sees it at once, and the geometry event that follows says what the client made of it. A KWin script cannot move the pointer, so the pointer follows the focus through KWin's own "Move Mouse to Focus" action, invoked over kglobalaccel once KWin reports the window active. The commands are typed in bridge.js, and the declarations of KWin's API let the bridge set a window's frame, its minimized state and the active window, raise and close it, and nothing else. The session the tests of both sides share grows a second batch: the backend's tests check that set_rect, focus, raise, minimize, unminimize and close send it, and the bridge's tests answer the bridge with it and check what KWin is asked to do, down to the command for a window that has closed, which comes back as failed. The bridge's tests also cover findWindow's fallback to KWin's list, a frame set as a plain object, and a command KWin throws on. Three VM suites check it against a real KWin. vm-core-e2e (from the spike's m2-e2e, the sections that need no komorebic) compares what the daemon asked for, read from its debug log, with what KWin has, read with kdotool: three windows inside the screen, not overlapping, sharing it, the same focused window on both sides, and the same again after a minimize, a restore and a close. vm-client-geometry (slow) has eight real clients answer the frames komorebi asks for: Qt programs and xterm take them exactly, foot sizes itself to whole cells, GTK 4 programs and Firefox keep to their minimum sizes. vm-latency (slow) times retiles from a recording of the session bus: on HVF, from the daemon's answer to the last frame KWin reports, p50 17.3 ms, p95 27.7 ms. vm-kwin-events no longer checks that nothing moves. The VM library gives session commands the test user's home and data directories rather than root's, which a GTK client was refused. The bridge's second request for a position after a resize, which matters for windows off the screen, comes with the scrolling layout. |
||
|
|
15ec3c1274 |
feat(kwin): read the desktop through a script in KWin
KWin has no protocol that lets another process read or set the geometry of a window, but its scripting API has both, and a signal for every change the window manager follows. So the KWin backend is in two parts: bridge.js, a script the daemon loads into KWin, which reports what KWin sees and holds no window manager logic, and a D-Bus service in the daemon, org.komorebi.Bridge, which the script calls with every event as a JSON string. Every decision about what an event means is made by the table in kwin::state, which is pure and is unit-tested with the JSON the script sends: which windows may be managed, the ids komorebi gives KWin's windows, and the Show, Destroy, FocusChange, Minimize and Restore events of the window manager. Events reach the window manager in the order KWin raised them: the Event interface handles its calls inline (zbus reordered them otherwise), and one channel and one thread, window_manager_event_listener and listen_for_events, carry them on. Commands go to the script as the answer to a Poll call that the script always has open and the daemon holds until it has something to send: in the spikes this was as fast as any other channel, and it makes one call every 10 seconds when nothing happens. Here the only command is the cursor query. Every command of the Backend trait is refused with "the KWin backend only reads the desktop", so the daemon follows KWin and moves nothing. The bridge is JavaScript that KWin runs as it is, and it gets types and tests without a JavaScript build chain. tsc checks it as strict TypeScript through JSDoc types of every message, against kwin-api.d.ts, a declaration of the part of KWin's scripting API it uses, in which every property is read-only and callDBus takes one string. bridge.test.js runs it under node --test against a fake KWin in a node:vm context. The messages of a session are JSON fixtures that both sides' tests read: the bridge's tests check that it sends those events, and the table's and the backend's tests read the same events and commands, so a message cannot change on one side alone. just check-js and the flake's komorebi-bridge-js check run both, with nodejs and typescript from nixpkgs; there is no package.json. The daemon now connects to KWin by default, and --fake-backend is hidden. The VM suite vm-kwin-events starts it against a headless KWin and checks, from its debug log, that the script loads and reports what was open, that KWin's signals arrive as events and in order, and that stopping the daemon unloads the script. docs/dev/PLATFORM.md starts with what komorebi needs from Plasma. zbus 5's zvariant, zbus_names and zvariant_derive are flagged unmaintained, since their code moved into zbus 6; deny.toml ignores the three advisories. |
||
|
|
02231d232c |
feat(wm): tile what the backend reports
With the model in place, the window manager can do its job: load the desktop from the backend, tile it, and keep it tiled as the compositor reports what happens to windows. The events are the Windows implementation's, each about one window and carrying it: a window is shown, destroyed, given the focus, minimized or restored. A new window gets a container of its own after the focused one, a closed one leaves whichever workspace has it, a window the user minimizes is out of the layout until it is restored, and every event ends with the workspace in front tiled again. At startup every monitor is loaded, and every open window that is normal, alive and not minimized goes on the monitor its middle is on. An index from each managed window to its monitor and workspace is rebuilt around every event, so that the first event komorebi sees can be the close of a window it picked up at startup. Whether a window is managed is, for now, only whether the compositor calls it a normal window and it is alive; the rules that ignore, manage or float applications come later. When a window closes, komorebi leaves the choice of the next focus to KWin unless KWin focuses nothing, so the two do not fight. The daemon runs this against a backend chosen on the command line. --fake-backend, an in-memory desktop with one monitor, is the only one, and without it komorebi exits with a usage error instead of pretending to manage a desktop it cannot reach. |
||
|
|
c0345eb0e9 |
feat(wm): windows, containers, workspaces and monitors
The window manager tiles a tree of four levels: monitors, each with a ring of workspaces, each with a ring of containers, each with a ring of windows. It is the model komorebi has on Windows and macOS, and the first thing the daemon keeps that the backend does not give it. A window is a Copy handle that carries the compositor's id and asks the backend for everything else, so moving one is a call on the handle and needs no context passed to it. A workspace tiles itself: the layout gives each container a tile of the work area its monitor hands it, less the offset and the padding, and every window of the container is asked to move there. A locked container keeps its index because the workspace inserts, removes and swaps its containers through LockableSequence. This is tiling only. Stacks, monocle, maximize, the floating layer, resize adjustments, placement rules and the link between workspaces and KWin's desktops come with the commits that use them. impl_ring_elements!, which gives every holder of a ring the same accessors, lands here with its first users. |
||
|
|
ed6a8e96c3 |
feat(backend): reach the compositor through one trait
The window manager decides where every window goes, and asks a compositor to put it there. If it asked KWin directly, every test of its logic would have to boot a virtual machine, so everything it needs from the desktop goes through one trait, `Backend`: queries for windows, monitors, the focus and the pointer, and commands to move, focus, raise, minimize and close a window and to put the pointer somewhere. A real backend answers from a table that compositor events keep up to date, so a query never waits for the compositor; a command returns once it has been accepted, not once the client has acted. `FakeBackend` is the other implementation: an in-memory desktop that does what it is told, at once and exactly, and records every command so that a test can assert on what the window manager asked for. A test can also do what a user does without the window manager asking: move a window, close it, change the focus, turn a window into a dialog. It is strict where KWin is (a command about a window that is gone is an error) and clamps where a GTK4 client does (a minimum size). `with_backend` overrides the process-wide backend for the current thread only and puts the previous one back on drop, also on a panic, which is what keeps one test's desktop out of the next. The module is the first to have `#![deny(missing_docs)]`. The contract (what must be implemented, that a backend is `Send + Sync`) is proved by two `compile_fail` examples with their error codes, and the rules nothing checks are listed as such. docs/dev/ARCHITECTURE.md describes the trait, its roles and the layer that enforces each rule, and docs/dev/DESIGN.md has the first decisions with their reasons. Both grow with the code. |
||
|
|
0725705298 |
ci(github): run the VM tests on KVM
`discover` asks the flake for the suites of each tier (`legacyPackages.x86_64-linux.vmTiers`) and hands them to two matrices, so a new directory under `nix/tests/vm/suites` is run without an edit to the workflow. `vm-fast` runs the fast tier on every pull request, one machine per suite, as a sandboxed `nix build` of the check. `vm-slow` runs on master, every week, and on a pull request labeled `ci:vm-slow`; it builds the driver and runs it by hand, outside the sandbox, so that the screenshots and journals of a failed run are uploaded with `if: always()`. A composite action installs nix with the store cached and, for the VM jobs, frees disk space (a Plasma guest is a closure of about 10 GB), writes the udev rule that makes /dev/kvm writable for the runner's user, checks it with `test -w`, and adds `kvm` to nix's system features. KVM on hosted runners is documented for public repositories only. Whether this private repository has it is not known: the first run of these jobs says, and if it does not, they need a different runner. `lint` no longer runs `nix flake check`: on Linux that now boots every VM suite, which a runner with no KVM cannot do. It builds the checks that need no virtual machine, and `vm-lib`, the library's own tests. CONTRIBUTING.md and docs/dev/vm-tests.md say what runs where. |
||
|
|
fec9c2773a |
build(nix): a Plasma session node for tests
A virtual KWin has no output to draw on, so every test about pixels, and later about the pointer, needs a real session. The `session` node is the headless node plus a login: plasma-login-manager logs the test user in by itself, the way nixpkgs' own `plasma6` test does, and Plasma draws on the machine's display. The choice of login manager is made in `nodes/session.nix` and nowhere else, and the file says how to switch to sddm. The node adds no display device. The test driver gives every aarch64 guest one virtio-gpu and takes screenshots of it; a second device is a second KWin output that screenshots never show, which is what a first attempt did (a blank 1280x800 console). `Kde.session` attaches to the session once the desktop is up: the shell is running, the user manager knows the Wayland display, and the splash screen has gone. The display is read from the user manager because plasma-login-manager's greeter has its own KWin and takes `wayland-0`, and the user's session gets `wayland-1`. The splash condition is there because KSplash has over a thousand colors on it, so a "not blank" check passed on the splash screen. NixOS wraps programs, which is why processes are found by their command lines: `pgrep -x plasmashell` never matches `.plasmashell-wrapped`, and an exact match for the splash would have passed because nothing matched. `Kde.shot` returns a screenshot as a Pillow image. `vm-render-smoke` is the first slow suite: it starts a session, opens a terminal, and checks the middle of the window is dark and that the desktop behind it has thousands of colors. On aarch64 under HVF five runs gave the same screenshot, with the desktop up after 34 seconds. docs/dev/vm-tests.md describes the node and the two traps. |
||
|
|
3c51b9b4bf |
build(nix): a NixOS test framework for headless KWin
The unit tests run against a fake desktop and cannot say whether KWin does what the daemon asks. This adds the other half: NixOS machines with Plasma in them, a real KWin, and a Python library to drive it. Every directory under `nix/tests/vm/suites` is a test. The generator lists the directory, so a new suite appears as `checks.<system>.vm-NAME` on Linux and as `legacyPackages.<system>.vm.NAME` everywhere, with no edit to the flake. A suite names its tier and its node and has a `test.py`; `legacyPackages.<system>.vmTiers` lists the names of each tier as JSON for the CI matrix, `vmGuests` the guests to build on a Linux builder, and `vmTcg` the same suites for a machine with no KVM. `scale` multiplies every wait: 1 for KVM, 1.5 for HVF, 8 for TCG. The headless node runs `kwin_wayland --virtual` as user units of a lingering user, one pair of units per session (`session-bus@NAME`, `kwin-virtual@NAME`), so a test can run several sessions with their own bus and socket. A virtual KWin has no output to draw on, so anything that needs pixels or a pointer needs the session node. The library, `komorebi_vm`, has `Checks` (counted checks that fail once, at the end) and `Kde` (start a session, run clients and tools inside it, read windows through kdotool, poll with `wait_for`). It depends on a small `Machine` protocol rather than on the test driver. The `vm-lib` check runs its unit tests against a fake machine, with ruff and ty. The driver type checks each `test.py` with ty, which is how a protocol that did not match `QemuMachine.execute` was caught. `vm-kwin` starts a KWin with two outputs, spawns a foot window, reads it back, starts a second session and checks that it cannot see the first. On a Mac the guest is built on a Linux builder and the driver run directly, because the macOS nix sandbox cannot give QEMU the hypervisor: `scripts/vm-mac`, `just vm NAME`. docs/dev/vm-tests.md covers this, adding a test, and the builder. |