Files
komorebi/docs/dev/COMPARE.md
T
LGUG2Z e8497344e7 chore(deps): flake.lock: update
nixpkgs moves from d3b13ad (2026-10-24) to f05535a (2026-10-27) and rust-overlay
from c7e9d51 to 19ecdd0; the other six inputs have no newer revision. KWin,
Plasma and KRDP go from 6.8.1 to 6.8.2, so testedMax in nix/supported-kwin.nix
goes to 6.8.2, with its dated comment. Qt (6.12.0), FreeRDP (3.32.2) and the
glibc of nixpkgs (2.44) keep their versions. The stable Rust toolchain stays at
1.99.1; the nightly rustfmt moves from 2026-10-27 to 2026-10-28.

nixpkgs now has KDE Rounded Corners 0.11.0. nix/kde-rounded-corners.nix still
sets the version and the source, because its patch is written for the 0.11.0
source, so the package does not change: it builds with its patch, and Geometry
Change builds, against KWin 6.8.2. The dated sentences of README.md, DESIGN.md,
PLATFORM.md, COMPARE.md and of the docs of kwin/effects.rs say 2026-10-28 and
the new versions. Measurements taken on 6.8.1 keep their date and version, since
none was repeated on 6.8.2.

The glibc floor does not move: komorebi and komorebic still need 2.39 on aarch64
and on x86_64 (pidfd_getpid and pidfd_spawnp, both weak references), measured as
the build job measures it, before and after patchelf.

The gate with the nix checks passes on an aarch64 box and on x86-1,
nixos-module-eval among them (the KWin version scenarios included). The VM
suites run against the tip of these two commits; the result is in the message of
the next commit.
2026-10-28 11:27:44 -07:00

829 lines
59 KiB
Markdown

# komorebi for KDE Plasma compared with komorebi for Windows and for macOS
This document compares komorebi for KDE Plasma on KWin and Wayland (kfde) with komorebi for
Windows (kfw), the original, and komorebi for macOS (kfm). It is written for a reader who knows
komorebi on one of those two systems and wants to know what is the same on KDE, what is different,
what KDE does by itself that komorebi does on the other systems, and what is not supported.
It states facts that can be checked against the code, and does not assign value judgments. "kfw
draws the borders itself and kfde asks the compositor to" is a fact. "The compositor's borders are
better" is not, and is not said here. The reasons behind kfde's choices are in
[DESIGN.md](DESIGN.md), where they are opinions and say so.
It is written for kfde, so it leans toward kfde, and it was written from the sources of the three
programs, not from running them side by side. Where something could not be verified, it says so.
A correction is welcome.
## How to read this document
Every table in a marked block is generated by `scripts/compare-gen.py` from the source trees
of the three programs, so a count or a name in it is what the code had when the block was last
generated. The text around the tables is written by hand and states what the tables say.
```sh
python3 -I scripts/compare-gen.py --update docs/dev/COMPARE.md # rewrite the blocks
python3 -I scripts/compare-gen.py --check docs/dev/COMPARE.md # exit 1 when one is stale
```
The script reads this checkout and checkouts of [kfw](https://github.com/LGUG2Z/komorebi) and
[kfm](https://github.com/LGUG2Z/komorebi-for-mac) (`~/komorebi` and `~/komorebi-for-mac` by
default; `--kfw` and `--kfm` name others). Continuous integration has neither checkout, so it
does not run the script: a change to a command, a message, a key of `komorebi.json`, an event or
a method of `Backend` is followed by `--check` on a machine that has them, and by `--update` and
a read of the text around the block when it reports a stale one. The script also checks the
hand-written notes it holds (why a Windows command has no counterpart, which KWin call stands in
for a Win32 one) against the sources, and the sentences of this document that depend on a
generated fact, and stops on a note or a sentence that is no longer true. The revisions it read
are:
<!-- gen:sources -->
| Name | What | Read from | Revision |
|---|---|---|---|
| kfw | komorebi for Windows, the upstream | `~/komorebi` | 173aa802, 2026-10-27 |
| kfm | komorebi for macOS | `~/komorebi-for-mac` | 125a9a5, 2026-10-27 |
| kfde | this project | this repository | the commit that has this document |
<!-- /gen:sources -->
kfm changes often, so the numbers for kfm in particular are those of that revision.
Terms used below:
- **KWin's script** (the bridge) is a small JavaScript program that komorebi loads into KWin. It is
the only code of kfde that runs inside the compositor, and it contains no window manager logic.
- **A workspace** is komorebi's name for one of the desktops of a monitor, in all three programs.
- **A parking desktop** is a KWin virtual desktop that nobody looks at, which kfde moves windows to
in order to hide them.
- **The strip** is the scrolling layout of kfm, in the crate `komorebi-scrolling`. It is not the
`Scrolling` layout of `komorebi-layouts`.
## What is the same
### One tiling model, and the same layouts
The model is the same in all three programs: a list of monitors, each with workspaces, each with
containers, each with windows. kfde adds no layer to it. The layouts come from the crate
`komorebi-layouts`, which kfw owns and kfm and kfde use as a git dependency:
<!-- gen:layouts -->
| `DefaultLayout` | kfw | kfm | kfde |
|---|---|---|---|
| `BSP` | yes | yes | yes |
| `Columns` | yes | yes | yes |
| `Rows` | yes | yes | yes |
| `VerticalStack` | yes | yes | yes |
| `HorizontalStack` | yes | yes | yes |
| `UltrawideVerticalStack` | yes | yes | yes |
| `Grid` | yes | yes | yes |
| `RightMainVerticalStack` | yes | yes | yes |
| `Scrolling` | yes | yes | yes |
The crate each tree builds: kfw: `komorebi-layouts` in the tree; kfm: `komorebi-layouts` at git rev `f08aa55`; kfde: `komorebi-layouts` at git rev `f08aa55`.
Compared file by file with the crate of kfw, kfm: 0 of 13 source files differ from the ones of kfw; kfde: 0 of 13 source files differ from the ones of kfw.
<!-- /gen:layouts -->
The layouts of `komorebi-layouts` are calculated by the same code in the three programs, so the same
windows in the same order get the same rectangles, apart from what the window manager takes off for
the work area, the padding and the borders. The exception is the `Scrolling` layout of kfm, which kfm
lays out with the strip (see the section on configuration). What differs between the programs is
what the system does with the rectangles: the next sections.
### One configuration file, largely portable
A `komorebi.json` written for kfw or kfm loads in kfde. The keys that kfde does not read are
accepted and ignored, not rejected. The tables of the sections on configuration say which keys
those are, and why.
## Commands
`komorebic` has the same shape in the three programs: a command writes a message on the socket of the
daemon, and the message is a variant of the `SocketMessage` of `komorebi-client`. A command's name is
the variant of `SubCommand` in kebab case, which is what the pages of `docs/cli` are named after.
<!-- gen:commands-counts -->
| | kfw | kfm | kfde |
|---|---|---|---|
| Commands `komorebic` has | 192 | 158 | 158 |
| In all three | 148 | 148 | 148 |
| Also in kfw, not in kfm | 7 | - | 7 |
| Also in kfm, not in kfw | - | 1 | 1 |
| Only in kfde | - | - | 2 |
| In kfw or kfm, not in kfde | 37 | 9 | - |
| Same command under another name | - | - | 0 |
<!-- /gen:commands-counts -->
No command of kfw or kfm is kept in kfde under another name, not even as an alias.
### kfde has the commands kfw and kfm share
These commands exist in all three programs, with the same name. The ones that the tiling and the
workspaces are driven with do the same on KDE; where they do it differently (a workspace is a KWin
desktop) the section on workspaces says how.
<!-- gen:commands-all3 -->
`adjust-container-padding`, `adjust-workspace-padding`, `animation`, `animation-duration`, `animation-fps`, `animation-style`, `application-specific-configuration-schema`, `border`, `border-colour`, `border-offset`, `border-width`, `cancel-preselect`, `change-layout`, `clear-all-workspace-rules`, `clear-named-workspace-layout-rules`, `clear-named-workspace-rules`, `clear-session-float-rules`, `clear-workspace-layout-rules`, `clear-workspace-rules`, `close`, `close-workspace`, `configuration`, `container-padding`, `cross-monitor-move-behaviour`, `cycle-empty-workspace`, `cycle-focus`, `cycle-layout`, `cycle-monitor`, `cycle-move`, `cycle-move-to-monitor`, `cycle-move-to-workspace`, `cycle-move-workspace-to-monitor`, `cycle-send-to-monitor`, `cycle-send-to-workspace`, `cycle-stack`, `cycle-stack-index`, `cycle-workspace`, `data-directory`, `disable-autostart`, `display-index-preference`, `docgen`, `eager-focus`, `enable-autostart`, `enforce-workspace-rules`, `ensure-named-workspaces`, `ensure-workspaces`, `flip-layout`, `focus`, `focus-last-workspace`, `focus-monitor`, `focus-monitor-at-cursor`, `focus-monitor-workspace`, `focus-named-workspace`, `focus-stack-window`, `focus-workspace`, `focus-workspaces`, `focused-workspace-container-padding`, `focused-workspace-padding`, `generate-static-config`, `global-state`, `global-work-area-offset`, `ignore-rule`, `initial-named-workspace-rule`, `initial-workspace-rule`, `layout-ratios`, `load-resize`, `log`, `manage`, `manage-rule`, `minimize`, `monitor-information`, `monitor-work-area-offset`, `monocle-focus-behaviour`, `mouse-follows-focus`, `move`, `move-to-last-workspace`, `move-to-monitor`, `move-to-monitor-workspace`, `move-to-named-workspace`, `move-to-workspace`, `move-workspace-to-monitor`, `named-workspace-container-padding`, `named-workspace-layout`, `named-workspace-layout-rule`, `named-workspace-padding`, `named-workspace-rule`, `named-workspace-tiling`, `new-workspace`, `notification-schema`, `preselect-direction`, `promote`, `promote-focus`, `promote-swap`, `promote-window`, `query`, `quick-load-resize`, `quick-save-resize`, `quickstart`, `replace-configuration`, `resize-axis`, `resize-delta`, `resize-edge`, `retile`, `save-resize`, `scrolling-layout-columns`, `send-to-last-workspace`, `send-to-monitor`, `send-to-monitor-workspace`, `send-to-named-workspace`, `send-to-workspace`, `session-float-rule`, `session-float-rules`, `socket-schema`, `stack`, `stack-all`, `start`, `state`, `static-config-schema`, `stop`, `subscribe-socket`, `swap-workspaces-with-monitor`, `toggle-cross-monitor-move-behaviour`, `toggle-float`, `toggle-float-override`, `toggle-lock`, `toggle-monocle`, `toggle-monocle-focus-behaviour`, `toggle-mouse-follows-focus`, `toggle-pause`, `toggle-tiling`, `toggle-window-based-work-area-offset`, `toggle-window-container-behaviour`, `toggle-workspace-float-override`, `toggle-workspace-layer`, `toggle-workspace-window-container-behaviour`, `unmanage`, `unmanaged-window-operation-behaviour`, `unstack`, `unstack-all`, `unsubscribe-socket`, `visible-windows`, `workspace-layout`, `workspace-layout-rule`, `workspace-name`, `workspace-padding`, `workspace-rule`, `workspace-tiling`, `workspace-work-area-offset`
<!-- /gen:commands-all3 -->
### kfde has the commands kfw has and kfm lacks, for the features it took from kfw
kfm has no counterpart for these. kfde has them because it has the feature:
<!-- gen:commands-adopted -->
| Command | What the help of kfw says |
|---|---|
| `check` | Check komorebi configuration and related files for common errors |
| `focus-follows-mouse` | Enable or disable focus follows mouse for the operating system |
| `toggle-focus-follows-mouse` | Toggle focus follows mouse for the operating system |
| `toggle-maximize` | Toggle native maximization for the focused window |
| `toggle-transparency` | Toggle transparency for unfocused windows |
| `transparency` | Enable or disable transparency for unfocused windows |
| `transparency-alpha` | Set the alpha value for unfocused window transparency |
<!-- /gen:commands-adopted -->
`focus-follows-mouse` and `toggle-focus-follows-mouse` are hidden from `--help` in kfw and kfde
alike.
### kfde has the command kfm has and kfw lacks
<!-- gen:commands-kfm -->
| Command | What it does |
|---|---|
| `completions` | Generate komorebic CLI completions for the target shell |
<!-- /gen:commands-kfm -->
### kfde has commands that neither has
These commands are about KDE: a key binding file is turned into KDE's global shortcuts, and the
keys the daemon registers itself are looked at and let go.
<!-- gen:commands-kde -->
| Command | What it does |
|---|---|
| `export-kde-shortcuts` | Turn an skhdrc into KDE global shortcuts (they work over RDP, unlike evdev hotkey daemons) |
| `hotkeys` | Key bindings: KDE global shortcuts from a key binding file, and the keys of the daemon |
<!-- /gen:commands-kde -->
[The user guide on key bindings](../usage/keybindings.md) describes them. `hotkeys` has
subcommands: `komorebic hotkeys compile` does what `export-kde-shortcuts` does for all three
formats, and `state`, `reload` and `release` act on the keys the daemon has registered.
### kfde does not start helper programs
`komorebic start` of kfw can start whkd, AutoHotkey, the bar and masir along with the daemon. In
kfde the daemon reads the key binding files itself and there is no bar, so `start` has one flag the
others share and none of the flags that launch another program:
<!-- gen:commands-flags -->
| Command | Flag | kfw | kfm | kfde |
|---|---|---|---|---|
| `start` | `--ahk` | yes | - | - |
| `start` | `--await-configuration` | yes | - | - |
| `start` | `--bar` | yes | yes | - |
| `start` | `--clean-state` | yes | - | - |
| `start` | `--config` | yes | yes | yes |
| `start` | `--ffm` | yes | - | - |
| `start` | `--masir` | yes | - | - |
| `start` | `--tcp-port` | yes | - | - |
| `start` | `--whkd` | yes | - | - |
| `stop` | `--ahk` | yes | - | - |
| `stop` | `--bar` | yes | yes | - |
| `stop` | `--ignore-restore` | yes | yes | yes |
| `stop` | `--masir` | yes | - | - |
| `stop` | `--whkd` | yes | - | - |
| `check` | `--configuration-only` | - | no command | yes |
| `check` | `--komorebi-config` | yes | no command | yes |
<!-- /gen:commands-flags -->
### kfde has no counterpart for these commands
Every command that kfw or kfm has and kfde lacks is here, with the reason. A group that is a
"Win32 quirk" is a workaround for how Windows treats windows, which a Wayland compositor does not
need.
<!-- gen:commands-missing -->
| Group | Command | In | Why kfde has no counterpart |
|---|---|---|---|
| AutoHotkey and PowerShell | `ahk-app-specific-configuration` | kfw | Writes application rules for `komorebi.ahk`. |
| AutoHotkey and PowerShell | `complete-configuration` | kfw | Part of the legacy `komorebi.ahk` and `komorebi.ps1` configurations. |
| AutoHotkey and PowerShell | `pwsh-app-specific-configuration` | kfw | Writes application rules for a PowerShell script. |
| AutoHotkey and PowerShell | `reload-configuration` | kfw | Part of the legacy `komorebi.ahk` and `komorebi.ps1` configurations. |
| AutoHotkey and PowerShell | `watch-configuration` | kfw | Part of the legacy `komorebi.ahk` and `komorebi.ps1` configurations. `komorebi.json` is watched always. |
| Win32 quirks | `alt-focus-hack` | kfw | Works around the rules Windows has for taking the foreground. |
| Win32 quirks | `force-focus` | kfw | Works around the rules Windows has for taking the foreground. |
| Win32 quirks | `identify-layered-application` | kfw | A Win32 window style. There is no counterpart in a Wayland compositor. |
| Win32 quirks | `identify-object-name-change-application` | kfw | A Win32 event some applications send on launch. |
| Win32 quirks | `identify-tray-application` | kfw | A Win32 notion: an application that closes to the system tray. |
| Windows tooling | `bar-configuration` | kfw | `komorebi-bar` is not ported: the Plasma panel is used, and the work area accounts for it. |
| Windows tooling | `gui` | kfw | Launches `komorebi-gui`, which is not ported. |
| Windows tooling | `kill` | kfw | Stops the helper programs `komorebic start` launches on Windows (whkd, AutoHotkey, the bar, masir). It launches none on KDE. |
| Windows tooling | `toggle-shortcuts` | kfw | Toggles `komorebi-shortcuts`, which is not ported. |
| Windows tooling | `whkdrc` | kfw | Shows the path of the whkd file. The daemon reads key binding files itself. |
| borders | `border-implementation` | kfw | How a Win32 border window is drawn. The compositor draws the border. |
| borders | `border-style` | kfw | How a Win32 border window is drawn. The compositor draws the border. |
| borders | `identify-border-overflow-application` | kfw | A border window that overflows is a Win32 problem. The compositor draws the border. |
| borders | `invisible-borders` | kfw | The size of the invisible frame Windows adds around a window. |
| custom layouts | `load-custom-layout` | kfw | Not supported yet. |
| custom layouts | `named-workspace-custom-layout-rule` | kfw | Not supported yet. |
| custom layouts | `named-workspace-custom-layout` | kfw | Not supported yet. |
| custom layouts | `workspace-custom-layout-rule` | kfw | Not supported yet. |
| custom layouts | `workspace-custom-layout` | kfw | Not supported yet. |
| hiding | `restore-windows` | kfw | Debugging command for windows komorebi hid itself. On KDE `stop` and the next start send parked windows home. |
| hiding | `window-hiding-behaviour` | kfw | One native mechanism hides windows, so there is nothing to choose. |
| kfm scrolling strip | `center-focus` | kfm | Belongs to the scrolling strip of kfm (`komorebi-scrolling`). The Scrolling layout of kfde is the one of `komorebi-layouts`. |
| kfm scrolling strip | `consume` | kfm | Belongs to the scrolling strip of kfm (`komorebi-scrolling`). |
| kfm scrolling strip | `expel` | kfm | Belongs to the scrolling strip of kfm (`komorebi-scrolling`). |
| kfm scrolling strip | `scrolling-layout-full-width` | kfm | Belongs to the scrolling strip of kfm (`komorebi-scrolling`). |
| kfm scrolling strip | `toggle-column-stack` | kfm | Belongs to the scrolling strip of kfm (`komorebi-scrolling`). |
| license | `license` | kfw and kfm | No license check was ported. |
| license | `splash` | kfw and kfm | No splash screen was ported. |
| macOS | `permissions` | kfm | Shows the System Settings dialogs macOS asks for. KDE asks for none. |
| monitors | `monitor-index-preference` | kfw | Identifies a monitor by its size. kfde has `display-index-preference`, which names the display, as kfm does. |
| named pipes | `subscribe-pipe` | kfw | A named pipe is a Windows facility. Subscribers use a Unix domain socket. |
| named pipes | `unsubscribe-pipe` | kfw | A named pipe is a Windows facility. Subscribers use a Unix domain socket. |
| stackbar | `stackbar-mode` | kfw | komorebi draws the tab bar of a stack on Windows. KDE has no counterpart; a stack is cycled with `cycle-stack` and `focus-stack-window`. |
| title bars | `remove-title-bar` | kfw | A window without a title bar has no shadow in KWin, and so no border. |
| title bars | `toggle-title-bars` | kfw | A window without a title bar has no shadow in KWin, and so no border. |
| upstream applications file | `convert-app-specific-configuration` | kfw | Maintains the YAML file of the Windows applications list. |
| upstream applications file | `fetch-app-specific-configuration` | kfw and kfm | Not supported yet: there is no applications file for KDE upstream, so there is nothing to fetch. |
| upstream applications file | `format-app-specific-configuration` | kfw | Maintains the YAML file of the Windows applications list. |
<!-- /gen:commands-missing -->
The groups "hiding", "borders" and "title bars" are Windows workarounds that KDE does natively,
which is the reason the project recorded when it decided not to port them
([DESIGN.md](DESIGN.md#what-is-komorebi-for-kde-the-port-of)). The other reasons say what the code
of kfw and kfm shows each command is for. For "upstream applications file": at time of writing
(2026-10-28) no applications file for KDE exists upstream, so `fetch-app-specific-configuration`
has nothing to fetch. The configuration key that points at a file of application rules works (see
the section on keys), and the user supplies the file.
## The socket protocol
The messages that `komorebi-client` sends are the variants of `SocketMessage`. The counts follow
the ones of the commands, since nearly every command is one message.
<!-- gen:messages-counts -->
| | kfw | kfm | kfde |
|---|---|---|---|
| `SocketMessage` variants | 187 | 153 | 156 |
| In all three | 148 | 148 | 148 |
| In kfde and kfw, not kfm | 6 | - | 6 |
| In kfde and kfm, not kfw | - | 0 | 0 |
| Only in kfde | - | - | 2 |
| In kfw or kfm, not in kfde | 33 | 5 | - |
<!-- /gen:messages-counts -->
Of the messages kfde has, these are not in both of the other programs:
<!-- gen:messages-only -->
- Only in kfde: `HotkeysState`, `ReloadHotkeys`.
- In kfde and kfw, not in kfm: `FocusFollowsMouse`, `ToggleFocusFollowsMouse`, `ToggleMaximize`, `ToggleTransparency`, `Transparency`, `TransparencyAlpha`.
- In kfde and kfm, not in kfw: none.
<!-- /gen:messages-only -->
The two messages that only kfde has are the ones behind `komorebic hotkeys state` and
`komorebic hotkeys reload`. The names of the variants are the same in the programs that share them.
Whether every variant has the same payload in the three programs was not compared. Subscribers use
a Unix domain socket, as in kfm and, next to named pipes, in kfw.
## Configuration
### kfde reads the keys of komorebi.json that both others read
The counts, by struct of `static_config.rs`. "Not read by kfde" is the number of keys that kfw or
kfm has and kfde does not:
<!-- gen:keys-counts -->
| Struct | kfw | kfm | kfde | All three | kfw and kfde | kfm and kfde | kfde only | Not read by kfde |
|---|---|---|---|---|---|---|---|---|
| `StaticConfig` (top level) | 53 | 34 | 37 | 30 | 4 | 3 | 0 | 20 |
| `WorkspaceConfig` (workspace) | 22 | 19 | 20 | 19 | 1 | 0 | 0 | 2 |
| `MonitorConfig` (monitor) | 8 | 9 | 9 | 8 | 0 | 1 | 0 | 0 |
| Total | 83 | 62 | 66 | 57 | 5 | 4 | 0 | 22 |
<!-- /gen:keys-counts -->
The keys that kfw, kfm and kfde all read, which mean the same, apart from what the next
sections say:
<!-- gen:keys-same -->
- top level (`StaticConfig`): `animation`, `app_specific_configuration_path`, `border`, `border_colours`, `border_offset`, `border_width`, `cross_boundary_behaviour`, `cross_monitor_move_behaviour`, `default_container_padding`, `default_workspace_padding`, `display_index_preferences`, `float_override`, `float_override_placement`, `float_rule_placement`, `floating_applications`, `floating_layer_behaviour`, `floating_layer_placement`, `floating_window_aspect_ratio`, `global_work_area_offset`, `ignore_rules`, `layout_defaults`, `manage_rules`, `monitors`, `monocle_focus_behaviour`, `mouse_follows_focus`, `resize_delta`, `theme`, `toggle_float_placement`, `unmanaged_window_operation_behaviour`, `window_container_behaviour`.
- workspace (`WorkspaceConfig`): `apply_window_based_work_area_offset`, `container_padding`, `float_override`, `floating_layer_behaviour`, `initial_workspace_rules`, `layout`, `layout_flip`, `layout_options`, `layout_options_rules`, `layout_rules`, `name`, `tile`, `wallpaper`, `window_container_behaviour`, `window_container_behaviour_rules`, `work_area_offset`, `work_area_offset_rules`, `workspace_padding`, `workspace_rules`.
- monitor (`MonitorConfig`): `container_padding`, `floating_layer_behaviour`, `wallpaper`, `window_based_work_area_offset`, `window_based_work_area_offset_limit`, `work_area_offset`, `workspace_padding`, `workspaces`.
<!-- /gen:keys-same -->
### kfde reads keys from kfw and keys from kfm
kfde reads the keys kfw has for the features it took from kfw, and the ones kfm has that kfw does
not:
<!-- gen:keys-adopted -->
| Key | Level | Also in | What it is |
|---|---|---|---|
| `focus_follows_mouse` | top level | kfw | Focus the window under the pointer. |
| `transparency` | top level | kfw | Add transparency to unfocused windows (default: false) |
| `transparency_alpha` | top level | kfw | Alpha value for unfocused window transparency [[0-255]] (default: 200) |
| `transparency_ignore_rules` | top level | kfw | Individual window transparency ignore rules |
| `border_radius` | top level | kfm | Radius of window borders |
| `tabbed_applications` | top level | kfm | Accepted so that a configuration written for another platform loads, and ignored: a KWin window has no native tabs |
| `titleless_applications` | top level | kfm | Accepted so that a configuration written for another platform loads, and ignored: a KWin window is always a window |
| `initial_window_placement_rules` | workspace | kfw | Initial window placement rules that determine where new tiled windows are placed in the container list. |
| `window_hiding_position` | monitor | kfm | Accepted so that a configuration written for another platform loads, and ignored: there is no hiding position on KDE Plasma |
<!-- /gen:keys-adopted -->
### No key of komorebi.json is read only on KDE
<!-- gen:keys-kde-only -->
none
<!-- /gen:keys-kde-only -->
That is by decision: a file should load on every platform that has the feature
([why](DESIGN.md#why-does-one-komorebijson-work-on-every-platform)). The keys that mean something
else on KDE, or do less, are these:
<!-- gen:keys-differ -->
| Key | Level | What is different on KDE |
|---|---|---|
| `border_width` | top level | At most 20 px: the effect draws the ring in the window's shadow, and a window without a shadow has none. While borders are drawn, the layout leaves `border_width` plus `border_offset` on each side of a window, also past 20. |
| `border_colours` | top level | Six colors, one for each kind of window, when komorebi's build of the border effect is installed. With the stock effect only `single` and `unfocused` take effect, and komorebi says so once in its log. |
| `animation` | top level | `style` is the Qt easing curve of that name (`EaseInOutSine` is `InOutSine`) and a custom cubic Bezier curve is an ease out, when komorebi's build of the Geometry Change effect is installed; the stock effect always eases out exponentially. `fps` is read and ignored: the compositor animates at the refresh rate of the screen. |
| `focus_follows_mouse` | top level | Both implementations kfw names (`Windows` and `Komorebi`) ask KWin for the same thing: its focus policy, put back as the user had it when it is turned off or komorebi exits. |
| `transparency` | top level | KWin's window opacity, set for the unfocused windows of each monitor's shown workspace by kfw's rules (the focused window and a monocled one stay opaque). |
| `theme` | top level | A theme by name or a palette of your own gives the six border colors, and the palette of a wallpaper takes over from it when its workspace is shown. |
| `wallpaper` | workspace | Set through the Plasma shell, which keeps one wallpaper for each screen and not for each desktop, so komorebi changes it as the workspace in front changes. The picture fills the screen. |
| `workspaces` | monitor | Workspace `i` is KWin's desktop `i` on every monitor, so every monitor has as many desktops as the largest workspace count, and where the names differ the desktop takes the first monitor's. |
| `app_specific_configuration_path` | top level | Read as in kfm and kfw. There is no applications file for KDE upstream, so komorebi has none to download. |
<!-- /gen:keys-differ -->
### Some keys are read and do nothing on KDE
They are read so that a file written for another platform loads, and `komorebic check` lists them
once as accepted and ignored:
<!-- gen:keys-no-effect -->
| Key | Level | Also in | Effect on KDE |
|---|---|---|---|
| `tabbed_applications` | top level | kfm | Read, so that a file written for kfm loads, and does nothing on KDE. |
| `titleless_applications` | top level | kfm | Read, so that a file written for kfm loads, and does nothing on KDE. |
| `window_hiding_position` | monitor | kfm | Read, so that a file written for kfm loads, and does nothing on KDE. |
| `animation.fps` | nested | kfw, kfm | Read, and ignored: the compositor animates at the refresh rate of the screen. |
<!-- /gen:keys-no-effect -->
### Some keys are not read at all
These are keys of kfw or kfm that kfde does not have. They are not an error: serde skips a key it
does not know, so the file loads, and `komorebic check` tells them apart from a typo.
<!-- gen:keys-not-read -->
| Key | Level | In | Why kfde does not read it |
|---|---|---|---|
| `bar_configurations` | top level | kfw | `komorebi-bar` is not ported. |
| `border_implementation` | top level | kfw | How a Win32 border window is drawn. The compositor draws the border. |
| `border_overflow_applications` | top level | kfw | How a Win32 border window is drawn. The compositor draws the border. |
| `border_style` | top level | kfw | How a Win32 border window is drawn. The compositor draws the border. |
| `border_z_order` | top level | kfw | How a Win32 border window is drawn. The compositor draws the border. |
| `invisible_borders` | top level | kfw | The size of the invisible frame Windows adds around a window. |
| `layered_applications` | top level | kfw | A Win32 window style. There is no counterpart in a Wayland compositor. |
| `minimum_window_height` | top level | kfw | Marked DISCOURAGED in komorebi for Windows. Commented out in kfde. |
| `minimum_window_width` | top level | kfw | Marked DISCOURAGED in komorebi for Windows. Commented out in kfde. |
| `monitor_index_preferences` | top level | kfw | Identifies a monitor by its size. kfde reads `display_index_preferences`, which names the display, as kfm does. |
| `object_name_change_applications` | top level | kfw | A Win32 event some applications send on launch. |
| `object_name_change_title_ignore_list` | top level | kfw | A Win32 event some applications send on launch. |
| `remove_titlebar_applications` | top level | kfw | A window without a title bar has no shadow in KWin, and so no border. |
| `scrolling` | top level | kfm | The `ScrollingConfig` of the strip in kfm (`komorebi-scrolling`). kfde has the Scrolling layout of `komorebi-layouts`, tuned with `layout_options`. |
| `slow_application_compensation_time` | top level | kfw | A Win32 workaround for applications that are slow to move. |
| `slow_application_identifiers` | top level | kfw | A Win32 workaround for applications that are slow to move. |
| `stackbar` | top level | kfw | KDE has no counterpart of the tab bar komorebi draws on Windows. |
| `tray_and_multi_window_applications` | top level | kfw | A Win32 notion: an application that closes to the system tray. |
| `window_handling_behaviour` | top level | kfw | A Win32 quirk of how windows are moved. |
| `window_hiding_behaviour` | top level | kfw | One native mechanism hides windows, so there is nothing to choose. |
| `custom_layout` | workspace | kfw | Custom layouts are not supported yet. |
| `custom_layout_rules` | workspace | kfw | Custom layouts are not supported yet. |
<!-- /gen:keys-not-read -->
The nested structs have the same keys, except for these:
<!-- gen:keys-nested -->
| Struct | kfw | kfm | kfde | In kfw, not kfde | In kfm, not kfde |
|---|---|---|---|---|---|
| `BorderColours` | 6 | 6 | 6 | - | - |
| `ThemeOptions` | 11 | 8 | 8 | `stackbar_background`, `stackbar_focused_text`, `stackbar_unfocused_text` | - |
| `Wallpaper` | 3 | 3 | 3 | - | - |
| `AnimationsConfig` | 5 | 4 | 4 | `ghost_movement` | - |
<!-- /gen:keys-nested -->
There is one more difference that is not a key. kfm has a scrolling strip, with columns that
hold several windows, a `scrolling` key and the commands in the "kfm scrolling strip" group above.
kfde has the `Scrolling` layout of `komorebi-layouts`, tuned with `layout_options` and the command
`scrolling-layout-columns`, which kfw and kfm have too. A `scrolling` key in a file written for kfm
is skipped by kfde.
## Events, and what drives them
The window manager reacts to events about windows. In kfw the operating system raises them, through
`SetWinEventHook`, and in kfm through the accessibility API and the notification center. In kfde
the script in KWin connects to the signals of KWin's `workspace` and of each window and sends every
one to the daemon as a JSON string over D-Bus. The daemon turns a signal into a
`WindowManagerEvent`, the way kfw and kfm turn what they get.
<!-- gen:event-sources -->
| Tree | What the OS layer reports | Kinds |
|---|---|---|
| kfw | `WinEvent`, the events of `SetWinEventHook` | 84 |
| kfm | `AccessibilityNotification` and `AppKitWorkspaceNotification` | 41 and 21 |
| kfde | `KWinNotification`, the signals of the KWin script | 16 |
<!-- /gen:event-sources -->
The events follow kfw's model: each is about a window, and a window is a small `Copy` handle. kfm's
events carry a process id and a window id instead. The table puts the events of kfde next to the
ones of the other two that play the same part:
<!-- gen:events -->
| kfde event | Source on KDE | kfw | kfm |
|---|---|---|---|
| `Show` | `workspace.windowAdded` | `Show`, `Uncloak` | `Show` |
| `Destroy` | `workspace.windowRemoved` | `Destroy` | `Destroy` |
| `FocusChange` | `workspace.windowActivated` | `FocusChange`, `Raise` | `FocusChange` |
| `Minimize` | `window.minimizedChanged`, to true | `Minimize` | `Minimize` |
| `Restore` | `window.minimizedChanged`, to false | - | `Restore` |
| `TitleUpdate` | `window.captionChanged` | `TitleUpdate` | - |
| `Manage` | komorebi itself (`manage`, a rule) | `Manage` | `Manage` |
| `Unmanage` | komorebi itself (`unmanage`, a rule) | `Unmanage` | `Unmanage` |
| `MoveStart` | `window.interactiveMoveResizeStarted`, for a move | `MoveResizeStart` | `MoveStart` |
| `MoveEnd` | `window.interactiveMoveResizeFinished`, after a move | `MoveResizeEnd` | `MoveEnd` |
| `ResizeStart` | `window.interactiveMoveResizeStarted`, for a resize | `MoveResizeStart` | `ResizeStart` |
| `ResizeEnd` | `window.interactiveMoveResizeFinished`, after a resize | `MoveResizeEnd` | `ResizeEnd` |
| `ResizeStep` | `window.interactiveMoveResizeStepped` | - | - |
| `WorkspaceChange` | `workspace.currentDesktopChanged` | - | `SpaceChange` |
| `WorkspaceMove` | `window.desktopsChanged` | - | - |
| `OutputChange` | `window.outputChanged` | - | - |
| `MaximizeChange` | `window.maximizedChanged` | - | - |
| `FullscreenChange` | `window.fullScreenChanged` | - | - |
| Event with no counterpart in kfde | In | Why |
|---|---|---|
| `Hide` | kfw | Win32 hides and cloaks windows; kfde has no counterpart because KWin shows and hides windows by desktop, and says so with `WorkspaceChange`. |
| `Cloak` | kfw | Win32 hides and cloaks windows; kfde has no counterpart because KWin shows and hides windows by desktop, and says so with `WorkspaceChange`. |
| `MouseCapture` | kfw | Windows tells komorebi that a mouse button went down on a window. KWin reports the start of a move or resize instead. |
| `ScreenLock` | kfm | KDE locks and unlocks the screen with `org.freedesktop.ScreenSaver`; komorebi hears it in `kwin/power.rs` and reconciles, with no event of this kind. |
| `ScreenUnlock` | kfm | KDE locks and unlocks the screen with `org.freedesktop.ScreenSaver`; komorebi hears it in `kwin/power.rs` and reconciles, with no event of this kind. |
<!-- /gen:events -->
Three things about this are worth knowing:
- A signal reaches the daemon in 0 to 1 ms, from `Date.now()` in the script to the receipt in the
daemon ([the spikes](PLATFORM.md#vocabulary), 2026-10-26, KWin 6.8.1 in a virtual machine on
HVF).
- KWin emits `windowActivated` before `windowAdded` for a new window, so the script announces the
window from the activation, and no event mentions a window before the event that adds it.
- The script contains no logic. [Why](DESIGN.md#why-does-the-bridge-hold-no-logic) and
[why the commands come back to the script as the answer to a held call](DESIGN.md#why-do-commands-go-to-the-bridge-as-the-answer-to-a-held-call)
are in DESIGN.md.
### What the operating system layer is
kfw calls Win32 from `WindowsApi`, kfm calls the accessibility API and Core Graphics from
`MacosApi` and its modules. kfde puts the whole of what the window manager needs behind one trait,
`Backend`, which a test replaces with `FakeBackend` (a desktop in memory). The sizes are not a
measure of the effort: the Win32 and macOS calls are scattered over more modules than the ones the
counts name.
<!-- gen:backend-counts -->
| Tree | The operating system layer | Size |
|---|---|---|
| kfw | `WindowsApi` in `windows_api.rs` | 92 public functions |
| kfm | `MacosApi` in `macos_api.rs`, and the `accessibility`, `core_graphics` and `skylight` modules | 14 public functions in `MacosApi` |
| kfde | `Backend`, with `FakeBackend` for tests and `KWinBackend` for KWin | 31 methods (27 are required, 4 are provided), and a script of 684 lines that runs inside KWin |
<!-- /gen:backend-counts -->
What each method of `Backend` is on KDE, and the function of kfw and kfm that does the same job,
where there is one:
<!-- gen:backend -->
| `Backend` method | On KDE | kfw function | kfm function |
|---|---|---|---|
| `windows` | `workspace.windowList()` once, then the events of the bridge | `enum_windows` | `window_elements` |
| `window` | the table the bridge keeps, fed by events | `window_rect`, `exe_path` | `window_by_title` |
| `foreground_window` | `workspace.activeWindow` | `foreground_window` | `foreground_window` |
| `monitors` | `workspace.screens` and `clientArea(KWin.PlacementArea, ...)`, which leaves out the panel | `load_monitor_information` | `load_monitor_information` |
| `cursor_pos` | `workspace.cursorPos` | `cursor_pos` | `cursor_pos` |
| `drag_origin` | the frame the bridge saw at `interactiveMoveResizeStarted` | none | none |
| `drag_end` | the frame the bridge saw at the last `interactiveMoveResizeStepped` | none | none |
| `set_rect` | `frameGeometry`, and a second request for the position once the size settles | `position_window` | `set_position` |
| `focus` | `workspace.activeWindow = w` | `raise_and_focus_window` | `focus` |
| `raise` | `workspace.raiseWindow(w)` | `raise_window` | `raise` |
| `hide` | moves the window to the parking desktop (`desktops`) | `hide_window` | `hide` |
| `restore` | moves the window back from the parking desktop (`desktops`) | `restore_window` | `restore` |
| `minimize` | `minimized = true` | `minimize_window` | `minimize` |
| `unminimize` | `minimized = false` | `restore_window` | `unminimize` |
| `close` | `closeWindow()` | `close_window` | `close` |
| `center_cursor_in` | the "Move Mouse to Focus" action (`MoveMouseToFocus`) | `center_cursor_in_rect` | `center_cursor_in_rect` |
| `ensure_workspaces` | `createDesktop` on `org.kde.KWin.VirtualDesktopManager` | none | none |
| `switch_workspace` | `setCurrentDesktopForScreen` | none | none |
| `set_window_workspace` | `desktops` of the window | none | none |
| `window_workspace` | `desktops` of the window | none | none |
| `current_workspaces` | `currentDesktopForScreen` | none | none |
| `apply_look` | writes the effects' groups of `kwinrc`, `loadEffect` and `reconfigureEffect` | none | none |
| `set_border_colours` | `org.komorebi.BorderColours1`, which only komorebi's build of the border effect has | none | none |
| `set_opacity` | `opacity` | `set_transparent` | none |
| `set_focus_follows_mouse` | the `FocusPolicy` of `kwinrc`, then `reconfigure` | `enable_focus_follows_mouse` | none |
| `set_wallpaper` | `org.kde.PlasmaShell` `setWallpaper` | `set_wallpaper` | `set_wallpaper` |
| `maximize` | `setMaximize` | `maximize_window` | none |
<!-- /gen:backend -->
A method with "none" in both columns is something kfw and kfm do not need an operating system call
for, because the window manager keeps its own workspaces there, or something they do not have.
[Why everything is behind one trait](DESIGN.md#why-is-everything-the-window-manager-needs-behind-one-trait)
is in DESIGN.md.
## What KDE does by itself
The table lists what kfw and kfm do themselves and what KDE does, for the features where the
compositor, the shell or the desktop has a mechanism of its own. The sections below say what is
different for each.
<!-- gen:native -->
| What | kfw | kfm | On KDE |
|---|---|---|---|
| Workspaces | komorebi's own workspaces; the windows of the other workspaces are hidden | komorebi's own workspaces; the windows of the other workspaces are hidden | KWin's virtual desktops, one set per output (Plasma 6.8): workspace `i` is desktop `i` on every monitor |
| Hiding windows (a stack, a monocle) | `window_hiding_behaviour`: cloak, hide or minimize | moves the window to a corner of the screen | a virtual desktop nobody looks at (`komorebi-park`) |
| Borders | a topmost tool window per border, drawn by komorebi with Direct2D | an `NSWindow` per border, drawn by komorebi | the KDE Rounded Corners effect, with komorebi's patch for one color per kind of window |
| Animation | komorebi moves the window step by step | komorebi moves the window step by step | the Geometry Change effect, with komorebi's patch for the easing curve |
| Transparency of unfocused windows | `SetLayeredWindowAttributes`, called by komorebi | no key or command | KWin's window opacity, through the same patched effect as the borders |
| Focus follows mouse | the Windows setting, or komorebi's own implementation (`focus_follows_mouse`) | no key or command | KWin's focus policy; the user's policy is put back when it is turned off or komorebi exits |
| Maximize | `toggle-maximize`: the Win32 maximize | no key or command | `toggle-maximize`: KWin's maximize, which respects the panel. A window the user maximizes is taken out of the layout until restored |
| Fullscreen windows | no handling of its own in the sources | no handling of its own, except that the reaper keeps a window that fails to move | left alone, and put back in its tile when it leaves fullscreen |
| Wallpapers | the desktop wallpaper of each monitor | the desktop picture of each display | the Plasma shell, asked over D-Bus, one wallpaper per screen |
| Hotkeys | a separate program: `whkd` (or AutoHotkey) | a separate program: `skhd` | KDE global shortcuts: `.desktop` files for plain bindings, and the daemon through KGlobalAccel for modes, chords and scopes |
| Monitors added, removed or changed | a reconciliator thread, fed by `WM_DISPLAYCHANGE` in a hidden window | a reconciliator thread, fed by the display reconfiguration callback | the same thread, fed by the script (when KWin's list of outputs changes) and by `kwin/power.rs`; `reconcile.rs` follows the windows the compositor moved |
| Sleep, wake and lock | power setting and session notifications (`register_power_setting_notification`, `wts_register_session_notification`) | `ScreenLock` and `ScreenUnlock` events | `PrepareForSleep` of logind and `ActiveChanged` of the KDE screen saver, on D-Bus |
| Reloading the configuration | `watch-configuration` is about `komorebi.ahk` and `komorebi.ps1`; `komorebi.json` is watched | `komorebi.json` is watched | `komorebi.json` is watched |
<!-- /gen:native -->
### Workspaces are KWin's virtual desktops
kfw and kfm have a workspace layer of their own: komorebi keeps a list of workspaces for each
monitor, and when one is left it hides the windows of that workspace, because Windows and macOS
have no virtual desktop API to ask. On KDE, KWin 6.8 can give each output its own current desktop,
so komorebi's workspace `i` is KWin's desktop `i` on every monitor, and each monitor shows one at a
time. A switch is one request per monitor, and KWin shows, hides and focuses. komorebi
follows a switch made by something else (the pager, a shortcut, an application raising a window)
and answers it with no command. [Why](DESIGN.md#why-are-komorebis-workspaces-kwins-virtual-desktops)
is in DESIGN.md.
This has consequences that are measured, or read from KWin's sources:
- A switch made by an application raising a window produces one focus event and one switch
(spike 10, 2026-10-27). Answering a switch with commands makes KWin and komorebi fight: a design
in which komorebi did the hiding flipped between two workspaces about 280 times a second, because
KWin delivered focus events it had generated before it applied the commands, and komorebi read
them as new intent. komorebi therefore issues no commands in reply to a switch.
- The workspaces are visible in the pager and in the overview: they are the user's desktops, and a
stop leaves them in place (it removes the parking desktop).
- Desktops are global: every monitor has as many as the largest workspace count, and the name of a
desktop is shared by the monitors (the first monitor's, where they differ).
- A window that komorebi does not manage (a dialog, an ignored application) belongs to the desktop it
opened on, and goes out of view with it. kfw and kfm hide what a workspace holds (its containers,
its floating windows and its monocle) when they leave it, so a window they do not manage stays
visible across workspaces.
- The floor is KWin 6.8, because per-output desktops arrived there. Older KWin versions were not
tested. At time of writing (2026-10-28) the versions run are 6.8.1 (the spikes) and 6.8.2 (the VM
suites), in Plasma 6.8.1 and 6.8.2.
- komorebi turns `PerOutputVirtualDesktops` on in `kwinrc` when it starts and puts it back when it
exits. It edits the file itself: `kwriteconfig6` was not on the path of the user service in the
first live run.
### Hiding windows: cloaking, a corner, or a parking desktop
komorebi hides windows for a stack (the other windows of a container) and for a monocle (the other
containers). kfw offers three ways, chosen with `window_hiding_behaviour`: cloak (an undocumented
Win32 call), minimize, and the end-of-life `SW_HIDE`. kfm moves the window to a bottom corner of
its display, which `window_hiding_position` of a monitor chooses. kfde moves the window to a
parking desktop and moves it back, and there is nothing to choose. The scrolling layout needs none
of this: KWin keeps a window at a position outside the screen when it is moved there.
Three ways were measured on six tiled windows (spike 3, 2026-10-26, KWin 6.8.1):
| Way | Spurious focus events | Geometry restored |
|---|---|---|
| minimize | 2 (KWin picks a new active window as each window minimizes) | 6 of 6 |
| parking desktop | 0 | 6 of 6 |
| a position outside the screen | 0, but the windows stay mapped on the current desktop (task switcher, overview) | 6 of 6 |
KWin saves a virtual desktop in `kwinrc` the moment it is made, so a run that dies leaves the
parking desktop for the next session. komorebi removes it on exit, and the next start finds a stale
one, sends its windows home and removes it. A parked window still appears in the task manager unless
that is set to show the current desktop only (by construction, not measured).
[Why a parking desktop](DESIGN.md#why-are-the-windows-behind-the-front-one-of-a-stack-moved-to-a-parking-desktop)
is in DESIGN.md.
### Borders: komorebi draws them, or the compositor does
kfw draws a topmost tool window for each border, with Direct2D, and kfm an `NSWindow` for each. kfde draws nothing: the
KDE Rounded Corners effect draws the outer outline of every window, and komorebi writes its settings
to `kwinrc` and tells it to read them again. KWin keeps the copy of `kwinrc` it read at start, so
komorebi asks for a reconfiguration every time, also straight after it loads the effect.
The limits, as of 2026-10-27:
- The ring is drawn in the window's shadow, so a window without a shadow has none, and its thickness
is at most 20 px. That is why kfde has no counterpart of `remove_titlebar_applications`: removing
the title bar removes the shadow.
- The stock effect has a color for the active window and one for the others. komorebi's build of it
takes a color for each window over D-Bus (`org.komorebi.BorderColours1`), which gives the six kinds
of kfw's rules: `single`, `stack`, `monocle`, `floating`, `unfocused` and `unfocused_locked`.
Without that build, every kind falls back to `single` and `unfocused`, and komorebi says so once in
its log.
- KDE Rounded Corners 0.10.0 draws the ring about 24 px too low on KWin 6.8 (measured on screenshots
of KWin 6.8.1). kfde builds 0.11.0, which nixpkgs has too at time of writing (2026-10-28).
- KWin finds effects when it starts, so an effect installed later is not loaded until the next
login.
[Why the compositor draws the borders](DESIGN.md#why-does-the-compositor-draw-the-borders-and-not-komorebi),
[why the effects are patched](DESIGN.md#why-are-the-effects-patched) and
[why the border is the outer outline](DESIGN.md#why-is-the-border-the-outer-outline-of-the-effect)
are in DESIGN.md.
### Animation: komorebi moves the window step by step, or the compositor animates
kfw and kfm animate by moving the window a little at a time, at `animation.fps`. kfde sends the final
rectangle and the Geometry Change effect animates the move, so `fps` has nothing to map to: the
compositor animates at the refresh rate of the screen. The styles are Qt's easing curves of the same
name, which komorebi's build of the effect reads from a `Curve` setting. The stock effect eases out
exponentially, whatever the style says. Cross-fading is off.
KWin refuses effects that animate under software rendering, which includes every virtual machine the
tests run in, unless `KWIN_EFFECTS_FORCE_ANIMATIONS=1` is in KWin's environment. A screenshot cannot
show a short animation, because `spectacle` takes seconds and the curve is front-loaded: the look test
uses a 30 s animation to see the effect at work.
### Transparency, themes and wallpapers
Transparency is KWin's window opacity, set from the same rules as kfw. A window drawn through the
border effect comes from a texture with its opacity in it, which the effect redrew only when the
contents changed. komorebi's build redraws it when the opacity changes.
A wallpaper is set through the Plasma shell, over D-Bus (`org.kde.PlasmaShell`), for one screen. The
shell keeps one wallpaper for each screen, not for each virtual desktop, so komorebi sets it when a
workspace comes into view, as kfw and kfm do. If the shell is not running, komorebi tries again for
about half a minute and then says once that it leaves wallpapers alone. A palette made from a
wallpaper is cached in `$XDG_CACHE_HOME/komorebi`, by the name of the picture and a hash of its
path, size and time of change; a large picture takes seconds (not measured further).
### Focus follows mouse
kfw has two implementations to choose from, `Windows` and `Komorebi`, and calls the first legacy. kfm
has none. kfde asks KWin to use its focus policy, "focus follows mouse", with the same result for
both names, and puts the user's policy back when the setting is turned off or komorebi exits. KWin
waits for the pointer to settle before it moves the focus: `DelayFocusInterval`, 300 ms by default
in KWin 6.8.1. kfde sets it to 0 while the setting is on, and puts the user's value back with the
policy; why 0 is not recorded
([DESIGN.md](DESIGN.md#why-is-focus-follows-mouse-kwins-own-focus-policy)).
### Maximize and fullscreen
`toggle-maximize` is kfw's command, and does KWin's maximize on KDE: the window leaves the layout
and goes back to its tile when toggled again, and the panel is respected. A window that the user
maximizes (the title bar, a double click, a shortcut) is the same from then on: the commands that act
on the layout do nothing until it is restored. [Why](DESIGN.md#why-does-a-maximized-window-leave-the-layout-instead-of-being-given-the-work-area)
is in DESIGN.md. A fullscreen window covers its monitor, komorebi never moves it, and it is put back
in its tile when it leaves fullscreen. kfw has no code for fullscreen, and kfm has one
mention of it in its reaper.
## Dragging
All three programs let the user move a tile by dragging it and resize it by dragging an edge. KWin
reports where the pointer has the edge at every step of a resize
(`interactiveMoveResizeStepped`), so in kfde the neighbors follow the edge while it is dragged, and
not only when it is let go. The way a drag is measured, from the frame the window had when the drag
began, and granted as far as the layout can take it, is written down in
[drag-resize.md](../drag-resize.md), for the three programs. The frame KWin reports at the end of a
resize lags the pointer by a moment ([PLATFORM.md](PLATFORM.md#how-clients-answer-a-frame)). kfm
reads the frame of a window with an accessibility call when it needs it (`MacosApi::window_rect`);
whether that frame lags was not measured.
## Key bindings
kfw and kfm leave the keys to a separate program: kfw uses whkd, or AutoHotkey, kfm uses skhd, and
the program runs `komorebic`. On Plasma under Wayland no program sees the keys of another, so kfde
cannot do that: the keys are KDE's global shortcuts, in one of two ways, and komorebi reads the same
three formats a hotkey daemon would (`skhdrc`, `whkdrc`, `sxhkdrc`).
| | kfw | kfm | kfde |
|---|---|---|---|
| Who sees the keys | whkd, or AutoHotkey | skhd | KWin, through KDE's global shortcuts |
| The file the user writes | `whkdrc` | `skhdrc` | `skhdrc`, `whkdrc` or `sxhkdrc`, each read by a parser written for komorebi |
The measurements that decide when each is used, at time of writing (2026-10-27), on aarch64 under HVF
with four cores, KWin 6.8.1:
| What | Result |
|---|---|
| A static shortcut, key to the start of its `Exec` | 165 to 436 ms, median 233 ms, 8 runs of 8 |
| The daemon, key to the signal it gets | median 2.5 ms (min 2.2, max 3.9), 20 of 20 delivered |
| 60 calls to `setInactive` and `setShortcutKeys`, pipelined | answered in 2.4 ms in total |
| A native multi-key shortcut (`Meta+A, B`) | did not fire: the second key reached the focused client |
| A client killed after registering a key | the grab stays, and the key is swallowed, until `unRegister` |
The last two are why the daemon emulates chord chains, and why the module's unit releases the keys
when the daemon stops. A static shortcut works with the daemon stopped and shows in System Settings,
but costs about 0.2 seconds on each press; the daemon's keys are only as reliable as the daemon. The
default mode is the daemon for every binding (`keybindings.mode = "engine"`), which these
measurements favor ([why](DESIGN.md#why-does-the-engine-handle-every-binding-by-default), which
also says that the default is still open for a decision), and `auto` and `static` use shortcut
files for the bindings that can be one.
What is not emulated, and approximated, is listed in
[the user guide](../usage/keybindings.md#what-is-realized-and-what-is-not): side-specific modifiers,
mouse buttons, skhd's `fn`, and the sxhkd chain that a wrong key does not end. The reasons are in
DESIGN.md: [why some bindings are shortcut files](DESIGN.md#why-are-some-bindings-shortcut-files-and-the-others-the-engines)
and [why the engine grabs keys instead of using KDE's key sequences](DESIGN.md#why-does-the-engine-grab-and-give-back-keys-instead-of-using-kdes-own-key-sequences).
Keystrokes over RDP: KDE's shortcuts sit inside KWin's input pipeline and see them, and `swhkd`
reads the kernel's input devices and would not (read from the sources of KRDP and swhkd, not run).
Keys sent over RDP to a session were not verified end to end: the scripted client fails NTLM
against KRDP, and a real client was not tried.
## Multiple monitors, sleep and lock
All three programs have a monitor reconciliator: a thread that takes notifications one at a time to
move the windows of a monitor that went away and to bring them back. In kfw a hidden window
(`WM_DISPLAYCHANGE`) feeds it, and in kfm a display reconfiguration callback. In kfde the script feeds
it, with the whole list of outputs whenever KWin's changes, and so does the code that hears of a wake
or an unlock. The daemon takes the windows of an unplugged monitor over (each container, a stack whole, to the workspace of the same number), keeps what is left
of the monitor for when it comes back, and follows a window that something else moved to another
output. A moment after each notification (1.5 s) it looks at every window again, because KWin has
moved some. A monitor that changes size or scale is tiled again: every coordinate komorebi uses is
KWin's logical one. [Why](DESIGN.md#why-does-komorebi-take-the-windows-of-an-unplugged-monitor-over-itself)
is in DESIGN.md.
For the screen lock, kfm has `ScreenLock` and `ScreenUnlock` events, and kfw registers for power and
session notifications. kfde listens on D-Bus: logind's `PrepareForSleep` for a wake, and KDE's screen
saver `ActiveChanged` for a lock. With the screen off or locked KWin sends clients no frame callbacks,
so what komorebi asked for in that time is not done: komorebi waits 600 ms after an unlock and 2.5 s
after a wake and looks at every window again. The lock screen is a window of KWin's that is in no
task list and on every desktop; komorebi does not manage it.
At time of writing (2026-10-27) a real suspend and resume was not tested: the QEMU guests have no
way to wake from one. The signals are tested on private buses, and the lock screen against a real
KWin in a Plasma session. [PLATFORM.md](PLATFORM.md#sleep-and-lock) says so too.
## Checking the configuration
kfw and kfde have `komorebic check`, and kfm has none. kfw's reads the files in the configuration
home (`komorebi.json`, `komorebi.ahk` and `whkdrc` among them) and shows what is wrong with them.
kfde's reads `komorebi.json` the way the daemon will, tells a key of another platform from a typo
(with the nearest key that is a setting), looks at the key binding file and at whether it has been
made into KDE shortcuts, and then looks at the desktop: a Plasma session on Wayland, KWin 6 on the session bus, komorebi's
script in KWin, which build of each effect is installed, the Plasma shell when there are wallpapers,
and what starts komorebi at login. It exits with status 1 when something stops komorebi from working
(a file that is not JSON, no KWin), and 0 with warnings when it only will not do what was meant.
`--komorebi-config FILE` checks another file and `--configuration-only` leaves the desktop out; the
first flag is kfw's too.
kfw's `check` also asks GitHub for the latest release. kfde's does not look for a newer release, and
it does not know the AutoHotkey and PowerShell files of Windows.
## What kfw and kfm have that kfde does not
From the tables above, and from what has no row in them:
- The bar. `komorebi-bar` is not ported: the Plasma panel is used, and the work area accounts for it
(a panel moved is a change of the work area, and the tiles are made again).
- The stackbar and its settings. A stack is cycled with `cycle-stack` and `focus-stack-window`.
- Custom layouts, which kfw has and kfm does not. Not supported yet.
- The Win32 workarounds of the "Win32 quirks" group, the AutoHotkey and PowerShell configurations,
the named-pipe subscribers, and whkd as a separate program.
- The license check and the splash screen of the macOS and Windows builds.
- kfm's scrolling strip: columns that hold several windows, and its commands.
- The tab and title bar commands (`remove-title-bar`, `toggle-title-bars`).
## What kfde has that kfw and kfm do not
- Native workspaces: they show in KWin's pager and overview, and KWin focuses the window of the
desktop it switches to.
- Borders, animation, transparency and focus follows mouse as a compositor does them, and `check`
for a desktop.
- Key bindings that work from a physical keyboard, the QEMU console and RDP, with modes, chords and
scopes realized by the daemon, and the `skhdrc`, `whkdrc` and `sxhkdrc` formats.
- A NixOS module (`services.komorebi`) with a user unit, the effects, and the key bindings as options,
and a version check of KWin. kfm has a module for nix-darwin, in its own form.
- A test suite that runs the window manager against a real KWin in virtual machines, and a `Backend`
that tests replace with a desktop in memory. [vm-tests.md](vm-tests.md) describes it.
## Costs, measured
At time of writing (2026-10-27), on an Apple silicon Mac, in virtual machines under HVF, KWin 6.8.1
(Plasma 6.8.1, nixpkgs `d3b13ad` of 2026-10-24). No number here compares kfde with kfw or kfm:
nothing of the same kind was measured on them.
| What | Result | Source |
|---|---|---|
| A retile of 10 `foot` windows on a 3840x2160 output, from the answer to `Poll` to the last frame KWin reported (5.5 windows moved on average) | p50 17.3 ms, p95 27.7 ms, max 31.4 ms | `vm-latency`, [PLATFORM.md](PLATFORM.md#how-long-a-retile-takes) |
| The same channel alone, 20 windows in each batch | p50 16 ms, p95 19 ms, max 20 ms | [the spikes](PLATFORM.md#vocabulary) |
| A client's answer to a frame | exact for Qt, GTK 4 down to its minimum, XWayland; `foot` rounds to its cells; GTK 4 Calculator and Firefox keep a minimum size | `vm-client-geometry`, [PLATFORM.md](PLATFORM.md#how-clients-answer-a-frame) |
| A window resized off the screen | KWin keeps 100 px of it on an output | [PLATFORM.md](PLATFORM.md#how-clients-answer-a-frame) |
| KWin restarting under a running daemon | the script is loaded again about 1 s after KWin is back | [the spikes](PLATFORM.md#vocabulary) |
What was not measured is listed in [PLATFORM.md](PLATFORM.md#what-was-not-measured): a real suspend
and resume, KWin before 6.8, activities, outputs beyond a second one, applications in a sandbox,
keys from an RDP client and graphics other than software rendering, among others.