mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-14 12:53:43 +01:00
2268 lines
224 KiB
Markdown
2268 lines
224 KiB
Markdown
# Architecture
|
|
|
|
komorebi is a tiling window manager. It decides where every window goes and asks a
|
|
compositor to put it there. Everything it needs from the compositor is behind one trait,
|
|
[`Backend`](../../../crates/komorebi-linux/src/backend/mod.rs), so the window manager is written, and
|
|
tested, without one.
|
|
|
|
This page describes that trait, who implements and calls it, where each of its rules is
|
|
enforced, the model of the desktop the window manager tiles, the backend that reaches KWin,
|
|
how an event about a window becomes a tile, how workspaces are the compositor's desktops, how
|
|
a stack hides its windows, how the window manager follows a monitor that changes and sets the
|
|
desktop in order after a sleep or a lock, how a message on the daemon's socket is answered, how
|
|
subscribers are notified of all three, what the orders a message gives do, what a drag with the
|
|
pointer does, how `komorebi.json` configures the daemon and is checked, how the compositor is
|
|
asked to draw the borders and to animate the windows, how a theme, a wallpaper, transparency and
|
|
focus follows mouse reach it, how the NixOS module runs the daemon, and how a key binding file
|
|
becomes shortcuts that run commands. It grows with the code: each part of the daemon that is
|
|
added has its place here.
|
|
[DESIGN.md](DESIGN.md) says why it is built this way, and [COMPARE.md](COMPARE.md) what is the
|
|
same and what is different in komorebi for Windows and komorebi for Mac.
|
|
|
|
## Vocabulary
|
|
|
|
- **Window**: a top-level surface a client has asked the compositor for, named by the
|
|
compositor's `u32` id.
|
|
- **Monitor**: a connected output, with its bounds and its work area (its bounds minus
|
|
the panels).
|
|
- **Wallpaper**: the picture a monitor shows while one of its workspaces is in front, and the theme
|
|
made of it. A workspace's own wins over its monitor's.
|
|
- **Workspace**: the containers a monitor shows, in order, and the layout that tiles
|
|
them. A monitor has a ring of workspaces and shows one of them. Workspace `i` is the
|
|
compositor's desktop `i` on that monitor. A workspace can have a name, which the named
|
|
commands find it by.
|
|
- **Desktop**: one of KWin's virtual desktops. KWin shows the windows of one desktop on each
|
|
output and hides the others; with its per-output virtual desktops on, each output shows a
|
|
desktop of its own.
|
|
- **Container**: what a layout places. It gets one tile, and every window in it is put on
|
|
that tile.
|
|
- **Stack**: a container with more than one window. It has one tile, and the window in front
|
|
(the container's focused window) is in sight; the others are hidden.
|
|
- **Hidden window**: a window of a stack that is not in front, or of a container a monocle
|
|
leaves out, which komorebi has taken out of sight ([`Backend::hide`]) and recorded in
|
|
[`HIDDEN_WINDOWS`]. It is alive, not minimized, and on its workspace.
|
|
- **Parking desktop**: the virtual desktop, named `komorebi-park`, that the KWin backend hides
|
|
windows on. It is not a workspace.
|
|
- **Layout**: the arrangement (BSP, columns, rows, ...) that turns a work area and a
|
|
number of containers into one rectangle, a **tile**, for each container.
|
|
- **Monocle**: a workspace's one container that has the whole work area. It is held out of the
|
|
workspace's containers, so the layout gives no container a tile, and the windows of the
|
|
containers are hidden, until it is put back where it was ([`Workspace::monocle_container`]).
|
|
- **Layer**: which of a workspace's two kinds of windows the focus, move and resize commands
|
|
act on: its tiles (`Tiling`) or its floating windows (`Floating`) ([`WorkspaceLayer`]).
|
|
- **Maximized window**: a workspace's one window that the compositor has maximized, whether
|
|
komorebi asked for it or the user did. It is in no container, so it has left the layout, until
|
|
it is put back in a container of its own where it was ([`Workspace::maximized_window`]).
|
|
- **Event**: what the compositor reports about a window: it was shown, destroyed, given
|
|
the focus, minimized or restored, fullscreen or back from it, maximized or restored from it,
|
|
dragged by the user, put on another workspace's desktop, or moved to another output; and that a
|
|
monitor shows another workspace ([`WindowManagerEvent`]). Two events come from komorebi itself:
|
|
a command asks it to manage the focused window, or to stop managing it.
|
|
- **Message**: what a client sends the daemon on its socket, a [`SocketMessage`]: an order
|
|
(focus a window, move it, change the layout, resize it, show a workspace, retile, stop) or a
|
|
question (the state, the monitors, one [`StateQuery`]).
|
|
- **Configuration**: `komorebi.json`, which the daemon reads when it starts and again when it
|
|
changes ([`StaticConfig`]). It lives in the **config home**, [`HOME_DIR`], unless `--config`
|
|
names another.
|
|
- **Rule**: a condition on a window's title, executable, class or path ([`MatchingRule`]), in
|
|
one of the process-wide lists that decide whether a window is managed (the ignore and the
|
|
manage rules) and whether it floats when it opens (the float rules).
|
|
- **Placement rule**: where in the list of containers the container of a new window goes: a place,
|
|
such as the first or the last, for every window, or a place for the windows that match a
|
|
condition ([`InitialWindowPlacementRules`]). A workspace has none unless it is given them, and a
|
|
window goes after the focused container.
|
|
- **Direction**: where an order goes from the focused container: a side of its tile
|
|
([`OperationDirection`]), or the next or the previous container of the workspace
|
|
([`CycleDirection`]).
|
|
- **State**: the window manager's monitors, workspaces, containers and windows, and its
|
|
settings, as the daemon answers a `State` message with them ([`State`]).
|
|
- **Subscriber**: a program that listens on a socket of its own in [`DATA_DIR`] and has
|
|
asked the daemon, by the name of that socket, to send it notifications.
|
|
- **Monitor notification**: what the KWin backend sends the monitor reconciliator when the
|
|
outputs change, and what [`kwin::power`] sends it when the machine wakes or the screen is
|
|
unlocked ([`MonitorNotification`]).
|
|
- **Notification**: what a subscriber is sent after each event, each message and each change
|
|
of the monitors the window manager handles: the event, the message or the change, and the
|
|
state after it ([`Notification`]).
|
|
- **Look**: how the compositor is asked to dress the windows, which it draws ([`Look`]):
|
|
a border, with its width, its corner radius and two colors, and the animation of windows going
|
|
to their places, with its duration and its easing curve.
|
|
- **Opacity**: how opaque a window is drawn, 0 to 255, a property of the window in the compositor,
|
|
which [`transparency_manager`] sets for the windows that do not have the focus.
|
|
- **Effect**: a plugin of the compositor that changes how windows are drawn. KDE Rounded Corners
|
|
is the effect that draws the border, and Geometry Change the one that animates windows.
|
|
- **Compositor**: the program that owns the screen and the windows. On KDE Plasma it is
|
|
KWin.
|
|
- **Backend**: the implementation of the [`Backend`](../../../crates/komorebi-linux/src/backend/mod.rs)
|
|
trait that talks to a compositor.
|
|
- **Fake**: [`FakeBackend`](../../../crates/komorebi-linux/src/backend/fake.rs), the in-memory backend
|
|
the tests use.
|
|
- **Bridge**: the script komorebi loads into KWin ([`bridge.js`]). It reports what KWin
|
|
tells it and carries out what the daemon asks.
|
|
- **Table**: the KWin backend's model of the desktop ([`Table`]), which the bridge's events
|
|
keep up to date and the queries are answered from.
|
|
- **Held call**: the `Poll` call the bridge always has open, which the daemon answers when it
|
|
has commands for the bridge, or after 10 seconds with none.
|
|
- **Module**: `services.komorebi`, the NixOS module ([`nixos-module.nix`]), which installs
|
|
komorebi and runs the daemon in every Plasma session as the systemd user unit
|
|
`komorebi.service`.
|
|
|
|
## Design
|
|
|
|
The window manager reads the desktop and changes it through [`Backend`] and through
|
|
nothing else. A real backend answers from a table that compositor events keep up to date,
|
|
and sends commands to the compositor. The fake answers from a table the test fills in, and
|
|
records the commands. The window manager cannot tell them apart, which is the point:
|
|
the same logic runs against both. The reasons are in
|
|
[DESIGN.md](DESIGN.md#why-is-everything-the-window-manager-needs-behind-one-trait).
|
|
|
|
## The model
|
|
|
|
The desktop, as the window manager sees it, is two lists and a pointer:
|
|
|
|
- **Windows**, each a [`WindowInfo`]: its id, process, title, `app_id`, executable, frame,
|
|
whether it is minimized, whether it is fullscreen, whether it is maximized, whether it is a
|
|
normal application window (as opposed to a dialog, a splash screen or a dock), and whether it is
|
|
alive. A window that has been destroyed stays in the table for a short time with `alive` false,
|
|
so that the event that mentions it can still be logged.
|
|
- **Monitors**, each a [`MonitorInfo`]: its id, name, an identity that survives a
|
|
hot-plug, its bounds and its work area.
|
|
- **The pointer**, a position if the compositor says where it is, and the **focus**, the id
|
|
of the window that has it.
|
|
|
|
A command is one of: move and resize a window, focus it, raise it, hide it or restore it,
|
|
minimize it or bring it back, maximize it or unmaximize it, close it, put the pointer in the
|
|
middle of a rectangle, make desktops for the workspaces, show a workspace on a monitor, and put
|
|
a window on a workspace. The workspace a window is on, and the one each monitor shows, are
|
|
queries.
|
|
|
|
The window manager tiles a tree of four levels, each level a [`Ring`]: a list with one
|
|
element focused, so that "the focused workspace" or "the focused container" means
|
|
the same thing at every level.
|
|
|
|
- A **monitor** ([`Monitor`]) is one of the backend's monitors: its bounds, its work area,
|
|
its padding, an optional work area offset, an optional window-based offset with the number of
|
|
windows up to which it applies, and its workspaces. It shows the focused one.
|
|
- A **workspace** ([`Workspace`]) has a layout, its padding, an optional work area offset of its
|
|
own, whether it takes the monitor's window-based offset, its containers, in order, its
|
|
monocle container and its maximized window, if it has them, and its floating windows, which
|
|
are in no container and which the layout does not tile, which layer it is on, and its placement
|
|
rules, if it has them.
|
|
- A **container** ([`Container`]) has its windows. A new window gets a container of its
|
|
own, unless the window container behavior says to put it in the focused one. With more than
|
|
one window it is a stack: the focused window is in front and the others are hidden. A
|
|
container can be locked, which keeps it at its index while others are inserted, removed or
|
|
swapped around it.
|
|
- A **window** ([`Window`]) is a `Copy` handle that carries only the compositor's id.
|
|
Everything else about the window, and every change to it, goes through the backend.
|
|
|
|
Tiling a workspace ([`Workspace::update`]) is a function of this tree and a work area. The
|
|
monitor gives its focused workspace the work area, the padding to fall back to, the offset
|
|
(its own, or else the window manager's) and its window-based offset ([`WorkspaceGlobals`]).
|
|
The workspace takes the offset (its own, or else the monitor's) off the work area, then the
|
|
window-based offset while it has no more containers than the monitor's limit, then its
|
|
padding; the layout gives one tile per container, in order, each shrunk by the container
|
|
padding, and every window of a container is asked to move onto its tile with `set_rect`. A
|
|
tile is clamped to the work area, so it cannot reach onto a neighboring monitor.
|
|
|
|
The [`WindowManager`] keeps the monitors, the windows the user minimized (they are in no
|
|
workspace until they are restored), and an index from each window it manages to the
|
|
monitor and workspace that have it. It is loaded from the backend at startup: every
|
|
monitor, focused on the workspace the compositor shows on it, and every open window that is
|
|
normal, alive, not minimized and not ignored by a rule, on the workspace whose desktop it is
|
|
on, of the monitor its middle is on. After that it changes only when an event comes in, and
|
|
each event ends with the workspace in front tiled again.
|
|
|
|
## The KWin backend
|
|
|
|
[`KWinBackend`] is the backend of a real Plasma session. KWin has no protocol that lets
|
|
another process read and set window geometry, so the backend is in two processes:
|
|
|
|
- **The bridge**, [`bridge.js`], a script KWin runs inside itself. It subscribes to KWin's
|
|
signals and sends each as a JSON `Event` call over the session bus, and it carries out the
|
|
commands it gets as the answer to its held `Poll` call. When it starts it sends a snapshot
|
|
of the desktop. It holds no window manager logic.
|
|
- **The daemon's side**, [`kwin`], owns `org.komorebi.Bridge` on the session bus, with one
|
|
object for `Event` and one for `Poll`. `Event` calls are handled one at a time, in the order
|
|
they arrive, and never wait. A `Poll` call is held until there are commands, so that the
|
|
bridge gets them as soon as they are queued, without a timer and without a call from the
|
|
daemon into KWin.
|
|
|
|
Every decision about what an event means is made in the daemon, by the table
|
|
([`kwin::state`]): an event goes in, the table is updated, and what follows comes out, a
|
|
window event for the window manager or the answer to a query. The table is pure (no D-Bus,
|
|
no threads), so the unit tests drive it with the JSON the bridge sends.
|
|
|
|
The bridge is JavaScript, which no Rust build compiles, and KWin loads the file as it is, so
|
|
it is checked without a build step. `tsc` checks it as strict TypeScript through its JSDoc
|
|
comments, which type every message, against [`kwin-api.d.ts`], the part of KWin's scripting
|
|
API the bridge uses. [`bridge.test.js`] runs it with `node --test` against a fake of that API.
|
|
The two sides share the messages of a session in [`fixtures`]: the bridge's tests check that
|
|
it sends exactly those events, and answer it with those commands, and the Rust tests feed
|
|
the events to the table and check that the backend sends those commands. A change to a
|
|
message on one side fails a test on the other.
|
|
|
|
The path of an event: a KWin signal, the bridge's handler, `Event(json)` on the session bus,
|
|
[`Table::handle`], a [`WindowManagerEvent`] on the channel of
|
|
[`window_manager_event_listener`], the thread that [`listen_for_events`] starts, and
|
|
[`WindowManager::process_event`]. The queries (`windows`, `window`, `foreground_window`,
|
|
`monitors`) are answered from the table. `cursor_pos` is the exception: only the bridge can
|
|
read the pointer, so it is asked through the held call, and given 250 ms to answer before
|
|
the table's last known position is returned.
|
|
|
|
A command goes the other way. The backend looks up the UUID KWin knows the window by,
|
|
queues a JSON command for the bridge, and returns. The held `Poll` call is answered with the
|
|
queue, as one batch: the daemon waits a millisecond after the first command for the rest, so
|
|
that a retile of every window of a workspace reaches KWin at once. The bridge carries the
|
|
commands out in order and reports the ones it could not carry out (a window that closed in
|
|
the meantime) as a `failed` event, which is logged. `set_rect` also records the frame it
|
|
asked for in the table, so that the window manager sees it straight away; the `geometry`
|
|
event that follows says what the client made of it. The pointer cannot be moved from a
|
|
script: when it follows the focus, the backend invokes KWin's "Move Mouse to Focus" shortcut,
|
|
which centers it on the window KWin has focused. When komorebi stops, the backend asks KWin to
|
|
unload the bridge.
|
|
|
|
KWin can restart under a running daemon, on the same session bus. The thread that serves
|
|
the connection watches the owner of `org.kde.KWin` on the bus. When KWin leaves it, the
|
|
bridge has gone with it and its windows too, and nothing will report them closing:
|
|
[`Table::compositor_lost`] marks every window closed and returns a `Destroy` for each, so the
|
|
window manager takes them out of its layout. When KWin is back, the daemon loads the bridge
|
|
into it again, the bridge sends a snapshot of the new KWin's desktop, and its events are
|
|
handled as before. The `Poll` call the old bridge held is still open in the daemon, since
|
|
zbus does not end a call when its caller leaves the bus: it is answered with nothing as soon
|
|
as it wakes, and the commands go to the new bridge's call.
|
|
|
|
[`connect`] makes the backend: it connects to the session bus, loads the bridge into KWin,
|
|
and returns once the snapshot has arrived, so that the window manager reads a full table when
|
|
it starts. [PLATFORM.md](PLATFORM.md) has the D-Bus names, the files and the environment
|
|
variables; [DESIGN.md](DESIGN.md#why-does-komorebi-run-a-script-inside-kwin) has the reasons.
|
|
|
|
## Workspaces
|
|
|
|
komorebi's workspaces are the compositor's virtual desktops: workspace `i` is desktop `i` on
|
|
every monitor, and each monitor shows one at a time. The compositor shows the windows of the
|
|
desktop in front, hides the others, and keeps which window of each desktop had the focus; the
|
|
window manager hides no window to leave a workspace. The reasons are in
|
|
[DESIGN.md](DESIGN.md#why-are-komorebis-workspaces-kwins-virtual-desktops).
|
|
|
|
Showing a workspace ([`Monitor::load_focused_workspace`]) is three steps. Every window of every
|
|
workspace of the monitor is put on the desktop of its workspace, unless it is already there.
|
|
The compositor is asked to show the workspace on the monitor ([`Backend::switch_workspace`]),
|
|
which leaves the other monitors as they are. The workspace's focused window is given the focus.
|
|
The order matters: a window that is focused while it is on another desktop takes the monitor
|
|
back to that desktop.
|
|
|
|
Desktops are global in KWin: every monitor has as many as the monitor with the most
|
|
workspaces, and each has one name. At startup, and when a workspace is asked for that has no
|
|
desktop, the backend creates the missing ones over KWin's `org.kde.KWin.VirtualDesktopManager`
|
|
interface, named "Workspace 1", "Workspace 2" and so on ([`WindowManager::sync_workspace_desktops`]).
|
|
Without KWin's per-output virtual desktops every output would show the same desktop, so
|
|
[`connect`] turns them on in the user's `kwinrc`, and [`Kwinrc`] keeps the user's value and
|
|
puts it back when komorebi stops, also after a crash.
|
|
|
|
Something else can switch a monitor to another desktop: a pager, a KWin shortcut, an
|
|
application that raises a window of another desktop, which KWin answers by showing that
|
|
desktop. KWin has one signal for it, which does not say which output, so the bridge sends the
|
|
desktop of every output after it, and once a second. The table compares it with what it had,
|
|
and a monitor that shows another desktop is a `WorkspaceChange` event: the window manager
|
|
focuses that workspace and tiles it ([`WindowManager::adopt_workspace_change`]), and asks the
|
|
compositor for nothing, since KWin has already shown the windows and chosen which to focus. A
|
|
window put on another desktop by something else (the window menu's "Move to Desktop") is a
|
|
`WorkspaceMove`: the window manager moves the window to that workspace. komorebi's own switch
|
|
and move come back from KWin as the same reports; the table records them when the command is
|
|
sent ([`Table::switch_commands`], [`Table::window_workspace_commands`]), so they raise nothing.
|
|
|
|
## Stacks
|
|
|
|
A container has one tile, so when it has more than one window the others have to be out of
|
|
sight, and out of the way of the pointer and the focus: the window manager hides them
|
|
([`Window::hide`]). Minimizing them would not do. KWin reports a minimize as the user's, and
|
|
the windows would be taken for ones the user does not want tiled. What komorebi uses is the one
|
|
thing KWin has that hides a window and leaves it alone: a virtual desktop nobody looks at, the
|
|
parking desktop. The reasons are in
|
|
[DESIGN.md](DESIGN.md#why-are-the-windows-behind-the-front-one-of-a-stack-moved-to-a-parking-desktop).
|
|
|
|
A container keeps its front window in sync with what is in sight ([`Container::add_window`],
|
|
[`Container::load_focused_window`]): the window that is added or focused is shown, and every
|
|
other one is hidden. Operations that change which window is in front, or which container a
|
|
window is in, end with that call: stacking and unstacking ([`Workspace::move_window_to_container`],
|
|
[`Workspace::new_container_for_focused_window`]), a window that closes
|
|
([`Workspace::remove_window`], which brings the next one forward and focuses it), a window that
|
|
is floated, a focus change to a window that is behind ([`Workspace::focus_container_by_window`]),
|
|
and cycling the stack or focusing a window of it by its index.
|
|
|
|
A monocle hides the other containers in the same way. Turning it on hides every window of each
|
|
of them ([`Container::hide`]), turning it off restores the window in front of each
|
|
([`Container::restore`]), and cycling it hides the container it gave up. While a monocle is on,
|
|
[`Workspace::restore`] restores its window and no other.
|
|
|
|
[`Window::hide`] puts the window's id in [`HIDDEN_WINDOWS`] before it asks the backend, and
|
|
[`Window::restore`] takes it out, so that when the compositor reports something about the window
|
|
because of the hide the window manager knows who did it. A `Minimize` for a window in the set is
|
|
not the user's and changes nothing. On the way out, [`WindowManager::stop`] and the daemon's
|
|
`SIGINT` and `SIGTERM` handler restore every window in the set before the backend shuts down
|
|
([`WindowManager::restore_all_windows`]).
|
|
|
|
The KWin backend hides a window by putting it on the parking desktop. The table keeps for each
|
|
window whether it is parked and the desktop it came from ([`Table::hide_commands`],
|
|
[`Table::restore_commands`]): a hidden window is still on its workspace for every query
|
|
([`Table::window_workspace`]), a window put on another workspace while it is hidden is hidden
|
|
still and comes back there ([`Table::window_workspace_commands`]), and KWin's report that the
|
|
window moved to the parking desktop is not a `WorkspaceMove`. The parking desktop is made the
|
|
first time it is needed ([`NeedsPark`], `create_park_desktop` in [`kwin`]), by position, so
|
|
that it stays after the workspaces' desktops. The table checks that KWin still lists it before it
|
|
sends a window there, and asks for a new one when somebody removed it.
|
|
|
|
An application that raises a hidden window, or a click on it in the task manager, makes KWin
|
|
switch that monitor to the parking desktop. The table notices that a monitor shows it, sends the
|
|
monitor back to the desktop it showed before, and raises no `WorkspaceChange`; the window's own
|
|
`FocusChange` then brings it to the front of its stack.
|
|
|
|
KWin keeps its virtual desktops in `kwinrc` as soon as one is made, so a parking desktop that
|
|
komorebi does not remove stays for every later session. The backend removes it in `shutdown`,
|
|
after the restoring commands have reached the bridge. A daemon that was killed leaves it, with
|
|
windows on it: the first snapshot the next daemon gets lists a desktop with that name, which the
|
|
table treats as stale ([`Table::stale_park`]). It is never the parking desktop of the new run.
|
|
The snapshot sends the windows on it to the first workspace's desktop, and half a second later
|
|
the backend removes it (`sweep_stale_park`).
|
|
|
|
## Monitors that change
|
|
|
|
The outputs KWin has change under a running daemon: an output gets another resolution or
|
|
scale, or a panel changes the room the windows have on it. KWin has one signal for the outputs
|
|
(`workspace.screensChanged`) and none for the room a panel leaves, so the bridge sends every
|
|
output, with its geometry and work area, as a `screens` event when the signal comes, when a
|
|
dock window opens, closes or moves, and once a second, each time the outputs are not what it
|
|
last sent. The table compares them with the monitors it had ([`Table::handle`]), and a monitor
|
|
whose size or work area changed is a [`MonitorNotification::Resize`]. It is not a window
|
|
event, since it is about no window, and it goes to the monitor reconciliator
|
|
([`monitor_reconciliator`]), not to the window manager's channel of events.
|
|
|
|
The reconciliator is a thread with a channel of its own ([`listen_for_notifications`]). It takes
|
|
one notification at a time with the window manager locked, as the thread of events and the
|
|
thread of messages do, so the three never change the window manager at once. For a `Resize` it
|
|
reads the size and the work area of every monitor from the backend again
|
|
([`update_monitor_work_areas`]), tiles the workspace in front of the focused monitor in the area
|
|
it has now, and notifies the subscribers ([`NotificationEvent::Monitor`]). Another monitor's
|
|
windows keep their tiles until something tiles them again, such as `Retile`.
|
|
|
|
A monitor that is unplugged or plugged in changes the set of outputs, and the table announces
|
|
that as a [`MonitorNotification::DisplayConnectionChange`], by the ids of the monitors. KWin has
|
|
already taken the windows of the output that went to the outputs that are left, so komorebi's
|
|
tiles for them are on a screen that is gone and the windows sit untiled where they landed. The
|
|
reconciliator ([`reconcile_display_change`]) reads the monitors from the backend and compares
|
|
them with the window manager's by their serial number ids. For each monitor that went it keeps a
|
|
copy, with its workspaces and settings, in a cache that lives as long as the daemon
|
|
([`insert_in_monitor_cache`]), and has the window manager move what was on it to the monitors
|
|
that are left ([`WindowManager::evacuate_monitor`]). Each container and floating window goes to
|
|
the workspace of the same number on the monitor that now holds the middle of its window's frame,
|
|
or on the first monitor that is left when the window is on none; a stack goes as the container it
|
|
is, a monocle comes as an ordinary container, and a maximized window stays maximized, as the
|
|
maximized window of the workspace it goes to ([`Workspace::adopt_maximized_window`]). The monitor
|
|
is then removed.
|
|
A monitor that is new to the window manager is added by [`load_monitor_information`], and when
|
|
the cache has a monitor with its serial number id, the new monitor takes the cached workspaces and
|
|
settings, less the windows that have closed or that the window manager has elsewhere now, and the
|
|
cache entry is used once. Every monitor then shows the workspace in front again. The
|
|
focused monitor and workspace are focused again, since adding a monitor can take the focus. When
|
|
KWin reports no output at all, nothing is done.
|
|
|
|
A monitor that goes takes its workspace rules (the entries of [`WORKSPACE_MATCHING_RULES`] that
|
|
name its index) out of the list, and those of the workspaces a cached monitor brings back are
|
|
added again. The configuration fills the cache as well: [`StaticConfig::postload`] and
|
|
[`StaticConfig::reload`] keep a copy of each monitor that has the entry its display index
|
|
preference names, and make a monitor from the entry of each preference whose monitor is not
|
|
connected, so that the monitor takes its entry when it is plugged in.
|
|
|
|
The compositor can also move a window to another output without the set of outputs changing: a
|
|
shortcut that sends it to the next screen, the window menu, an application. The bridge reports
|
|
that from the window's `outputChanged` signal as an `output` event, with the frame the window
|
|
has by then, since the signal and the frame arrive in no fixed order, and the table makes it an
|
|
`OutputChange` event for the window manager. KWin also moves windows for a moment after outputs
|
|
come and go, so the reconciliator looks again 1.5 seconds after each notification
|
|
([`settle_later`]; a burst of notifications makes one look). Both end in
|
|
[`WindowManager::follow_windows_to_their_monitors`]: it finds every container whose focused
|
|
window, and every floating window, is on another monitor than the one the window manager has it
|
|
on, and moves it, whole, to the workspace of the same number on the monitor that holds the
|
|
middle of its frame, creating workspaces if that monitor has fewer, and tiles the monitors that
|
|
windows moved between. The window manager reads every window, not only the one in the event,
|
|
since when one moved others may have, and a move komorebi made itself is in its state already
|
|
and finds nothing. While the user drags a window nothing moves, since the end of the drag decides
|
|
where that window goes: the method returns at once while [`WindowManager::pending_move_op`] or
|
|
[`WindowManager::pending_resize_op`] holds a drag.
|
|
|
|
## Sleep, wake and lock
|
|
|
|
While the screen is locked or the machine is asleep, KWin sends its clients no frame callbacks,
|
|
so nothing komorebi asked for in that time is done, and the monitors can come back in another
|
|
order or size. None of it changes komorebi's state, but each can leave the desktop other than
|
|
komorebi left it. [`kwin::power`] watches for the two ends of it and sends a
|
|
[`MonitorNotification::Wake`] to the monitor reconciliator:
|
|
|
|
- `PrepareForSleep` of `org.freedesktop.login1.Manager` on the system bus, with `false`, 2.5
|
|
seconds after it. The match rule names `org.freedesktop.login1` as the sender, so that only
|
|
logind is believed.
|
|
- `ActiveChanged` of `org.freedesktop.ScreenSaver` on the session bus, with `false`, which KDE's
|
|
lock screen sends when it goes, 0.6 seconds after it, from any sender.
|
|
|
|
Going to sleep and locking the screen only log. A session with no lock screen or a machine with
|
|
no system bus has nothing to watch, and the daemon logs that and runs. The watchers are tasks of
|
|
the runtime of the KWin backend, which [`kwin::connect`] starts with the connection.
|
|
|
|
[`wake_up`] is what the reconciliator does for a `Wake`: it does what it does for a change of the
|
|
set of monitors ([`reconcile_display_change`]), reads the size and work area of every monitor
|
|
([`update_monitor_work_areas`]), follows the windows to the monitors they are on
|
|
([`WindowManager::follow_windows_to_their_monitors`]), and tiles every monitor
|
|
([`WindowManager::retile_all`]). A paused window manager is left alone.
|
|
|
|
The lock screen is a window of KWin's, with the class `kwin_wayland`, on every desktop and in no
|
|
task list, pager or switcher, and the bridge does not report it as an application window, so the
|
|
table never has a tile for it. The bridge also compares KWin's active window with the one it
|
|
last announced once a second, and announces the active one when they differ, since the window
|
|
that is active again after the screen is unlocked is not always announced.
|
|
|
|
## The command socket
|
|
|
|
The daemon takes messages on a Unix socket, `komorebi.sock` in [`DATA_DIR`]
|
|
(`$XDG_RUNTIME_DIR/komorebi`, or the local data directory without a runtime directory).
|
|
[`WindowManager::new`] binds it, after it removes a socket file that a daemon that did not
|
|
stop cleanly left behind, and the window manager keeps the listener. The daemon connects to
|
|
KWin before it makes the window manager, so a komorebi that cannot connect, such as a second
|
|
one in the same session, whose bus refuses it `org.komorebi.Bridge`, exits before it touches
|
|
the socket of the one that is running.
|
|
|
|
[`listen_for_commands`] starts a thread that accepts connections, and reads each connection
|
|
on a separate thread ([`read_commands_uds`]). A connection carries one [`SocketMessage`]
|
|
per line, as JSON with the variant's name in `type` and its arguments in `content`:
|
|
`{"type":"Query","content":"FocusedWorkspaceIndex"}`. Each message is handled by
|
|
[`WindowManager::process_command`] with the window manager locked, so messages are handled
|
|
one at a time. A message that waited a second for the lock is logged and dropped.
|
|
|
|
A question is answered on the same connection: the [`State`] or the monitors as JSON, or
|
|
the answer to a [`StateQuery`] as one value in plain text. The answer has no framing; it
|
|
ends when the daemon closes the connection, which it does once it has read the last line.
|
|
A client therefore shuts down its side for writing after its message, and reads until the
|
|
end ([`send_query`]). An order is answered with nothing, and a client that sends only
|
|
orders can close the connection as soon as it has written them ([`send_message`],
|
|
[`send_batch`]).
|
|
|
|
A message that fails (a query about the focused container when the workspace has none) is
|
|
logged, and the lines after it on that connection are not read. The client sees the
|
|
connection close with no answer.
|
|
|
|
`Stop` ends the daemon: [`WindowManager::stop`] removes the socket, lets the backend give up
|
|
what it set up in the compositor (the KWin backend unloads the bridge), and exits with
|
|
status 0. `SIGINT` and `SIGTERM` do the same from `main.rs`, with the exit status 130.
|
|
|
|
## Notifications
|
|
|
|
A subscriber binds a socket in [`DATA_DIR`] and sends `AddSubscriberSocket` with its name,
|
|
or `AddSubscriberSocketWithOptions` with its name and [`SubscribeOptions`]
|
|
([`subscribe`], [`subscribe_with_options`], `komorebic subscribe-socket`).
|
|
[`WindowManager::process_command`] records it in [`SUBSCRIPTION_SOCKETS`], and its options in
|
|
[`SUBSCRIPTION_SOCKET_OPTIONS`]. `RemoveSubscriberSocket` takes it out again. The
|
|
subscribers are kept in memory only, and a daemon that restarts has none.
|
|
|
|
At the end of [`WindowManager::process_event`] and of [`WindowManager::process_command`], and
|
|
after the monitor reconciliator has handled a notification, the window manager calls
|
|
[`notify_subscribers`] with a [`Notification`]: the event, the message or the change of the
|
|
monitors ([`NotificationEvent`]), and the [`State`] after it. A [`State`] made before the event
|
|
or the message is compared with the window manager after it ([`State::has_been_modified`]);
|
|
the monitors are compared down to the windows, and a window by its id only. An event about a
|
|
window komorebi does not manage, and a message that fails, return before this and are not
|
|
notified.
|
|
|
|
[`notify_subscribers`] serializes the notification once, and for each subscriber connects to
|
|
its socket, writes the JSON and closes the connection, all on the thread that handled the
|
|
event or the message, with the window manager still locked. A subscriber with
|
|
`filter_state_changes` is skipped when the state did not change, unless the notification is
|
|
about a window being shown, a subscriber being added, a change of the monitors, a theme being
|
|
set (the colors of the borders are not part of the state) or the configuration being reloaded.
|
|
A subscriber that cannot be connected to is removed, with its socket file, after the loop.
|
|
Writing to a subscriber that was connected to has no timeout, and a failed write returns an
|
|
error from the call that handled the event or the message. A subscriber that stops reading
|
|
therefore holds the window manager, and with it the thread that handles events: the backend's
|
|
events wait in the channel of [`window_manager_event_listener`], and past 20 the backend drops
|
|
them.
|
|
|
|
When the daemon stops ([`WindowManager::stop`], or `SIGINT` and `SIGTERM` in `main.rs`), it
|
|
connects to each subscriber and shuts the connection down without writing anything.
|
|
|
|
## Commands
|
|
|
|
A message that gives an order is a call to a method of the [`WindowManager`] from
|
|
[`WindowManager::process_command`]. An order that goes somewhere takes one of two kinds of
|
|
direction, both from `komorebi-layouts`:
|
|
|
|
- An [`OperationDirection`] (left, right, up, down) is a side of the focused container's tile.
|
|
The workspace asks its layout which container is on that side
|
|
([`Workspace::new_idx_for_direction`]). A layout answers from the index of the focused
|
|
container and the number of containers, not from where the windows are.
|
|
- A [`CycleDirection`] (next, previous) follows the order of the workspace's containers, and
|
|
wraps around at both ends ([`Workspace::new_idx_for_cycle_direction`]).
|
|
|
|
`FocusWindow` and `CycleFocusWindow` focus another container of the focused workspace and give
|
|
its window the focus with the backend's `focus` ([`WindowManager::focus_container_in_direction`],
|
|
[`WindowManager::focus_container_in_cycle_direction`]). They retile nothing. When there is no
|
|
container in that direction, the focus goes to the monitor in that direction
|
|
([`WindowManager::monitor_idx_in_direction`], from the bounds of the monitors), to its container
|
|
nearest the monitor the focus came from (the leftmost one going right); with no monitor there
|
|
either, the message fails and changes nothing. Whether the pointer follows
|
|
the focus is [`WindowManager::mouse_follows_focus`], which `MouseFollowsFocus` and
|
|
`ToggleMouseFollowsFocus` set.
|
|
|
|
`MoveWindow` and `CycleMoveWindow` swap the focused container with the container in that
|
|
direction ([`WindowManager::move_container_in_direction`],
|
|
[`WindowManager::move_container_in_cycle_direction`]), and tile the workspace again: the window
|
|
that moved takes the other's tile, keeps the focus, and the pointer follows it. The swap goes
|
|
through `LockableSequence`, so a lock stays with its index: the container swapped onto a locked
|
|
index is locked in its place. When there is no container in that direction, the container goes
|
|
to the monitor in that direction ([`WindowManager::move_container_to_monitor`]), and
|
|
[`WindowManager::cross_monitor_move_behaviour`] says what it does there: by default (`Swap`) the
|
|
container at that edge of the other monitor comes back in its place, `Insert` puts it in among
|
|
the containers there, and `NoOp` keeps it where it is. With no monitor in that direction, the
|
|
message fails and nothing moves. [`WindowManager::cross_boundary_behaviour`] can make both cross
|
|
to the next workspace of the same monitor instead.
|
|
|
|
`ChangeLayout` and `CycleLayout` set the focused workspace's [`Layout`], and `FlipLayout`
|
|
mirrors it along an axis, on top of the flip it has ([`Workspace::layout_flip`]); each tiles
|
|
the workspace again ([`WindowManager::change_workspace_layout_default`],
|
|
[`WindowManager::cycle_layout`], [`WindowManager::flip_layout`]). A flip mirrors the tiles,
|
|
not the order of the containers, and a direction is flipped with them, so `focus left` on a
|
|
flipped layout goes to the tile on the left. `ToggleTiling` turns the workspace's tiling off
|
|
and on ([`Workspace::tile`]): with it off, [`Workspace::update`] moves no window, the workspace
|
|
still takes the windows that open into containers, and they are tiled when it is turned on.
|
|
|
|
`LayoutRatios` and `ScrollingLayoutColumns` set the focused workspace's [`LayoutOptions`]
|
|
([`Workspace::layout_options`]): the share of the work area its first columns and rows take, and
|
|
the number of columns of the scrolling layout in view. The layout calculates the tiles from
|
|
them, and from the tiles it calculated last ([`Workspace::latest_layout`]).
|
|
|
|
The scrolling layout is a strip of columns that runs past the edges of the work area, and a column
|
|
can hold several windows ([the scrolling layout](../usage/scrolling-layout.md), and
|
|
[scrolling-next.md](scrolling-next.md) for where it comes from). Each workspace keeps a
|
|
`ScrollingSpace` of `komorebi-scrolling` ([`Workspace::scrolling`]), made on the first layout
|
|
and keyed by the id of each container; [`Workspace::update`] makes it match the containers and the
|
|
focus, and asks it where every container goes ([`Workspace::layout_scrolling`]). Its tiles are the
|
|
only ones [`Workspace::update`] does not clamp to the work area. `consume`, `expel`,
|
|
`toggle-column-stack`, `scrolling-layout-full-width`, `center-focus`, `expand` and `pan` act on
|
|
the strip, and `focus`, `move`, `resize-*` and `retile` go through it in this layout; a container
|
|
that leaves a workspace leaves what its column was given for the next one to find
|
|
(`note_scrolling_departure`). A window that is out of view rests at the edge it left by with five
|
|
pixels of it on the screen and keeps its size (`parked`), or is hidden ([`Window::hide`]) where
|
|
that edge has another monitor next to it (`rest`), because KWin shows a window on every monitor it
|
|
overlaps. A `FocusChange` tiles a scrolling workspace after the focused container has changed,
|
|
which scrolls the strip to it. A window that did not get as narrow as its column is widens the
|
|
column, at the first layout 400 ms after the windows were sent (`adopt_actual_widths`: the frame
|
|
comes back from KWin a moment after the command). A drag of an edge keeps the strip with the rest
|
|
of what it started from ([`LiveResize`]). KWin keeps part of a window it resizes on an output, so
|
|
the bridge asks again for the position of a window KWin pulled back within a second of the
|
|
command, three times at most ([`bridge.js`]'s `settle`), unless the window is maximized meanwhile.
|
|
|
|
`Promote` moves the focused container to index 0, the largest tile of every layout but the grid,
|
|
through `LockableSequence`, so the containers before it move one index on
|
|
([`WindowManager::promote_container_to_front`]). `PromoteSwap` swaps it with the container at
|
|
index 0 and records the index it came from in [`Workspace::promotion_swap_container_idx`], so
|
|
that from index 0 it swaps back ([`WindowManager::promote_container_swap`]). `PromoteFocus`
|
|
focuses the container at index 0 ([`WindowManager::promote_focus_to_front`]), and
|
|
`PromoteWindow` focuses the container in a direction and promotes it. On the grid layout they
|
|
change nothing.
|
|
|
|
`ToggleFloat` takes the window the compositor has focused out of its container into the
|
|
workspace's floating windows, and back into a container of its own at the focused index
|
|
([`WindowManager::toggle_float`]). A floating window is in the index of managed windows like
|
|
any other, so its events find it, and [`Workspace::update`] leaves it where it is. Where a
|
|
floated window goes is [`WindowManagementBehaviour::toggle_float_placement`]: by default it
|
|
stays where it is.
|
|
|
|
`StackWindow` puts the focused window and the window of the container in a direction in one
|
|
container ([`WindowManager::add_window_to_container`]): the focused window goes to the front of
|
|
that container, or, when the focused container is a stack and the other is not, the other
|
|
container's window goes into the stack. `UnstackWindow` takes the front window out of a stack
|
|
into a container of its own after it ([`WindowManager::remove_window_from_container`]), `StackAll`
|
|
puts every window of the workspace in one container ([`WindowManager::stack_all`]), and
|
|
`UnstackAll` takes them out again ([`WindowManager::unstack_all`]). Each ends with the workspace
|
|
tiled again, and the window that was in front has the focus.
|
|
|
|
`CycleStack` brings the next or the previous window of the focused container to the front, which
|
|
wraps around ([`WindowManager::cycle_container_window_in_direction`]), and `FocusStackWindow` the
|
|
window at an index ([`WindowManager::focus_container_window`]). Both show that window, hide the
|
|
one that was in front ([`Container::load_focused_window`]) and give the window the focus.
|
|
`FocusStackWindow` first focuses the monitor the pointer is on, so that a program such as a bar
|
|
that runs it for a click on its own monitor acts there. `CycleStackIndex` swaps the front window
|
|
with the next or the previous one in the container's order, and keeps it in front
|
|
([`WindowManager::cycle_container_window_index_in_direction`]): only the order of the stack
|
|
changes. A container of one window cannot be cycled, and an index it does not have cannot be
|
|
focused.
|
|
|
|
A window that opens gets the behavior [`WindowManager::window_management_behaviour`] says for
|
|
its workspace: a container of its own (`Create`, the default), or a place in the focused
|
|
container (`Append`), and a floating window with the float override on. A workspace's own
|
|
[`Workspace::window_container_behaviour`] and [`Workspace::float_override`] win over the window
|
|
manager's, and `Append` on an empty workspace is `Create`. `ToggleWindowContainerBehaviour` and
|
|
`ToggleFloatOverride` flip the window manager's, and `ToggleWorkspaceWindowContainerBehaviour` and
|
|
`ToggleWorkspaceFloatOverride` the focused workspace's, which starts from the opposite of the
|
|
window manager's value the first time.
|
|
|
|
`ToggleMonocle` takes the focused container out of the workspace's containers and makes it the
|
|
monocle container, which [`Workspace::update`] puts on the whole work area, less the padding, and
|
|
hides the windows of the other containers; turned off, their windows are restored and the
|
|
container is put back at the index it came from ([`WindowManager::toggle_monocle`],
|
|
[`Workspace::new_monocle_container`], [`Workspace::reintegrate_monocle_container`]). The container's resize adjustment is kept
|
|
meanwhile, and so is the whole of [`Workspace::resize_dimensions`], so that the tiles come back
|
|
as they were resized. While there is a monocle container, the window in front of it is the one
|
|
that has the focus: a `FocusChange` to another window gives the focus back to it, and
|
|
[`WindowManager::update_focused_workspace`] focuses it, not the focused container's window, which
|
|
is one of the tiles behind. `MoveWindow` and `CycleMoveWindow` are refused, and `ToggleFloat` is
|
|
ignored, for the focused container is not the window in front. What `FocusWindow` does is
|
|
[`WindowManager::monocle_focus_behaviour`]: with `Cycle`, it makes the next or the previous
|
|
container the monocle ([`WindowManager::cycle_monocle`]); with `NoOp`, the default, there is no
|
|
container on any side, so the focus goes to the monitor on that side, and it fails where there
|
|
is none. `CycleFocusWindow` always carries the monocle to the next or the previous container.
|
|
`MonocleFocusBehaviour` sets the behavior, and `ToggleMonocleFocusBehaviour` switches it. A focus
|
|
that crosses to a monitor whose workspace has a monocle container gives the focus to its window,
|
|
and a container moved to a workspace with one turns that monocle off first.
|
|
|
|
`ToggleMaximize` takes the focused window out of its container, and the container with it when
|
|
it was the last window in it, remembers the index, and asks the backend to maximize it: KWin's
|
|
own maximize, to the work area with the panels left alone ([`WindowManager::toggle_maximize`],
|
|
[`Workspace::new_maximized_window`]). From then on the workspace has a maximized window, in no
|
|
container, and [`Workspace::update`] asks the backend to maximize it again and moves nothing
|
|
else, so the containers stay where they were behind it. Toggled again, the window is restored
|
|
(the backend gives it back the frame it had), put in a container of its own at the index it came
|
|
from, and the workspace is tiled again ([`Workspace::reintegrate_maximized_window`]). A window in
|
|
monocle that is maximized leaves the monocle. While a window is maximized, the messages that are
|
|
about the tiles are dropped before they are handled: `MoveWindow`, `CycleMoveWindow`, the stack
|
|
messages, the resize, promote and preselect messages, `ToggleLock`, `ToggleFloat`,
|
|
`ToggleMonocle`, and the messages that send a container to another monitor or workspace
|
|
(`is_layout_command` in [`process_command`](../../../crates/komorebi-linux/src/process_command.rs)). A window that
|
|
closes while it is maximized leaves the workspace like any other. `FocusWindow` finds no
|
|
container on any side of the maximized window, so the focus goes to the monitor on that side and
|
|
fails where there is none, and a maximized window, or a monocle, on the workspace it reaches is
|
|
given the focus whole. `CycleFocusWindow` is not dropped: it gives the focus to the next
|
|
container's window and leaves the maximized window as it is, where komorebi for Windows
|
|
maximizes the window the focus cycles to
|
|
([`WindowManager::focus_container_in_cycle_direction`]). When the workspace is shown,
|
|
[`Workspace::restore`] restores the maximized window and gives it the focus ahead of the
|
|
containers' windows. Nothing hides it when the workspace is left: KWin hides the windows of a
|
|
desktop that no monitor shows.
|
|
|
|
A window the user maximizes, with its title bar, a double click or a shortcut, is the same from
|
|
then on. KWin does it without komorebi's being asked, and the bridge reports it as a `maximized`
|
|
event, which the table turns into a `MaximizeChange` ([`WindowManager::adopt_maximize_change`]).
|
|
The window leaves its container, or the floating windows, or the monocle, for the workspace's
|
|
maximized window, with the index it goes back to ([`Workspace::adopt_maximized_window`]), and the
|
|
workspace is tiled again. It is not asked to maximize: it already is. A workspace has room for one
|
|
maximized window, so the one it had is put back in its container first. When the user restores
|
|
the window, it goes back in a container of its own at that index; a window that was floating comes
|
|
back as a tile, as it does after `ToggleMaximize`. komorebi's own maximize and restore are
|
|
reported too, and find the workspace as they leave it, so they change nothing. A window that is
|
|
already maximized when komorebi starts ([`load_workspace_information`]) or when it opens (a
|
|
`Show`) is the maximized window the same way.
|
|
|
|
A window that goes fullscreen needs no message and no model of its own. The compositor covers the
|
|
monitor with it, the bridge reports a `fullscreen` event, and the table keeps
|
|
[`WindowInfo::fullscreen`] for the window. The window stays in its container, and the layout keeps
|
|
its tile, but [`Window::set_position`], which every move the window manager makes goes through,
|
|
does nothing for a fullscreen window, so a retile, a centering or a window that opens moves it
|
|
nowhere. A `FullscreenChange` back from fullscreen tiles the workspace again, because KWin gives
|
|
the window the frame it had before, which may not be its tile any more.
|
|
|
|
A workspace is on one of two layers ([`Workspace::layer`]), and the focus, move and resize
|
|
messages go to the windows of that layer. `ToggleFloat` puts the workspace on the floating layer,
|
|
and putting the window back, or a `FocusChange` to a window that is not floating, a new tiled
|
|
window or a focus message that reaches a container, puts it on the tiling layer, which is where
|
|
a workspace starts; a `FocusChange` to a floating window, and a window that a float rule floats
|
|
when it opens, put it on the floating layer. `ToggleWorkspaceLayer` switches it by hand and raises
|
|
the windows of the layer it goes to, the focused one last, and gives the focus to one of them.
|
|
On the floating layer `FocusWindow` gives the focus to the nearest floating window on that side
|
|
of the focused one, by where the compositor has them, and where there is none the focus crosses
|
|
as it does for tiles, to the monitor on that side, and fails where there is none
|
|
([`WindowManager::focus_floating_window_in_direction`]); `CycleFocusWindow` goes round the
|
|
floating windows in the order they were floated
|
|
([`WindowManager::focus_floating_window_in_cycle_direction`]); `MoveWindow` moves the focused
|
|
floating window by [`WindowManager::resize_delta`], not out of the work area of the monitor
|
|
([`WindowManager::move_floating_window_in_direction`]); the resize messages change its frame,
|
|
and nothing is tiled ([`WindowManager::resize_window`]); and `PreselectDirection` does nothing.
|
|
`ToggleMaximize` maximizes the focused floating window there, which then leaves the floating
|
|
windows. `ToggleWorkspaceLayer` hides the floating windows when it goes to the tiling layer, and
|
|
shows them when it goes to the floating layer. A window that opens while the workspace is on its floating layer floats
|
|
(`floating_layer_override`) when the floating layer behaviour says to float: the workspace's own
|
|
([`Workspace::floating_layer_behaviour`]), else its monitor's, else the window manager's
|
|
([`WindowManager::window_management_behaviour`]). All three tile by default, and nothing sets
|
|
the first two until the configuration does.
|
|
|
|
`ToggleLock` locks the focused container, or unlocks it ([`WindowManager::toggle_lock`]), and
|
|
`LockMonitorWorkspaceContainer` and `UnlockMonitorWorkspaceContainer` do the same for a
|
|
container given by its indices. A lock changes nothing until containers are inserted, removed or
|
|
swapped around it, which the workspace does through `LockableSequence`.
|
|
|
|
`Minimize` and `Close` ask the backend to minimize or close the window the compositor has
|
|
focused, and change nothing else: the window leaves the layout when the compositor reports it,
|
|
through the `Minimize` and `Destroy` events. `UnmanageFocusedWindow` and `ManageFocusedWindow`
|
|
do not change the model either: they raise an `Unmanage` or a `Manage` event for the focused
|
|
window on the channel the backend's events go through
|
|
([`WindowManager::unmanage_focused_window`], [`WindowManager::manage_focused_window`]), and
|
|
the window manager handles them in order with the compositor's. An `Unmanage` is handled like a
|
|
`Destroy` of a window that stays open: the window leaves its workspace, is centered in the work
|
|
area, and its id goes into a list of unmanaged windows that `handle_event` checks before it acts
|
|
on any other event about a window; a `Manage` takes the id out of the list and is handled like a
|
|
`Show`. A `Destroy` takes the id out of the list too.
|
|
|
|
Whether a window is managed is [`Window::should_manage`]: the window has to be alive and normal,
|
|
and then the rules decide ([`should_act`]). An ignore rule that matches keeps the window out
|
|
unless a manage rule matches it too. The float rules do not decide whether a window is managed,
|
|
but a `Show` puts a window one matches in the workspace's floating windows instead of a container
|
|
(a `Manage` does not ask them). The rules live in process-wide lists in `lib.rs`:
|
|
`IgnoreRule` and `ManageRule` add a rule that names an application by one of the four
|
|
([`ApplicationIdentifier`]), and take the windows that it names exactly out of the containers of
|
|
the workspaces in front; `SessionFloatRule` adds a float rule made of the focused window's
|
|
executable, title and class, all three to be equal, and floats it, and `ClearSessionFloatRules`
|
|
takes those rules out again. `GlobalState` answers with the ignore and the manage rules
|
|
([`GlobalState`]), and `DebugWindow` with what [`Window::should_manage`] read of a window and
|
|
which rules matched ([`RuleDebug`]). A rule decides when a window is shown, by the title it has
|
|
then; the bridge reports a title change (`caption`), which is a `TitleUpdate` event, and the
|
|
window manager does nothing with it, so a window an ignore rule matched stays unmanaged when its
|
|
title changes, as in kfw.
|
|
|
|
`TogglePause` sets [`WindowManager::is_paused`]: while it is set, `handle_event` returns at once
|
|
for every event, and [`read_commands_uds`] hands only `TogglePause`, `State`, `GlobalState` and
|
|
`Stop` to the window manager. `UnmanagedWindowOperationBehaviour` sets
|
|
[`WindowManager::unmanaged_window_operation_behaviour`]: with `NoOp`,
|
|
[`WindowManager::process_command`] refuses every message while the window the compositor has
|
|
focused is in no container of the focused workspace
|
|
([`WindowManager::handle_unmanaged_window_behaviour`]).
|
|
|
|
`WorkspacePadding`, `ContainerPadding` and their `FocusedWorkspace` forms set a workspace's own
|
|
padding, and `NamedWorkspacePadding` and `NamedWorkspaceContainerPadding` do the same for the
|
|
workspace with a name, which they look up like `NamedWorkspaceLayout` does
|
|
([`WindowManager::monitor_workspace_index_by_name`]). `AdjustWorkspacePadding` and
|
|
`AdjustContainerPadding` change the focused workspace's by a number of pixels, which needs a padding
|
|
of its own to change ([`WindowManager::adjust_workspace_padding`]). `WorkAreaOffset`,
|
|
`MonitorWorkAreaOffset` and `WorkspaceWorkAreaOffset` set the offset of the window manager, of a
|
|
monitor or of a workspace, and `ToggleWindowBasedWorkAreaOffset` sets whether the focused workspace
|
|
takes its monitor's window-based offset ([`Workspace::apply_window_based_work_area_offset`]). Each
|
|
tiles the workspace in front again, or every monitor.
|
|
|
|
`ResizeWindowEdge` and `ResizeWindowAxis` resize the focused container
|
|
([`WindowManager::resize_window`]). A resize is an adjustment of the container's tile, kept in
|
|
[`Workspace::resize_dimensions`], one per container, which the layout applies when it calculates
|
|
the tiles; before every tiling, the workspace clears the parts of each adjustment that its layout
|
|
cannot apply, such as the top and the bottom of a column. The adjustment is the one
|
|
`komorebi-layouts` gives for the edge and [`WindowManager::resize_delta`], cut down until no tile
|
|
would be smaller than a tenth of the work area, or than it is already. The adjustments are kept
|
|
until a `Retile`, which drops them; `RetileWithResizeDimensions` tiles with them, and `Save`,
|
|
`Load`, `QuickSave` and `QuickLoad` write them to a file as JSON and read them back.
|
|
|
|
`FocusWorkspaceNumber` shows another workspace of the focused monitor
|
|
([`WindowManager::focus_workspace`]). `MoveContainerToWorkspaceNumber` takes the focused
|
|
container (or the focused window, when it is a floating one) to the back of another workspace
|
|
of the monitor and shows that workspace, and `SendContainerToWorkspaceNumber` does the same
|
|
and leaves the workspace in front showing ([`WindowManager::move_container_to_workspace`]). A
|
|
workspace that does not exist yet is created, with the ones before it, and gets a desktop.
|
|
|
|
The other workspace commands are made of the same two operations. `CycleFocusWorkspace`,
|
|
`CycleMoveContainerToWorkspace` and `CycleSendContainerToWorkspace` take the next or the previous
|
|
workspace, which wraps around, and `CycleFocusEmptyWorkspace` the next or the previous with no
|
|
window. A monitor remembers the workspace that was in front before ([`Monitor::focus_workspace`]
|
|
records it), which `FocusLastWorkspace`, `MoveContainerToLastWorkspace` and
|
|
`SendContainerToLastWorkspace` take. `FocusWorkspaceNumbers` shows a workspace on every monitor.
|
|
`WorkspaceName` names a workspace, `EnsureNamedWorkspaces` gives a monitor named workspaces, and
|
|
`FocusNamedWorkspace`, `NamedWorkspaceLayout` and `NamedWorkspaceTiling` look a workspace up by
|
|
its name on every monitor ([`WindowManager::monitor_workspace_index_by_name`]); a name no
|
|
workspace has changes nothing. `FocusNamedWorkspace` focuses the monitor that has the workspace
|
|
([`WindowManager::focus_monitor`]). `WorkspaceLayout` and `WorkspaceTiling` set the layout and the
|
|
tiling of a workspace by index. `NewWorkspace` adds a workspace and shows it, `EnsureWorkspaces`
|
|
gives a monitor workspaces up to a number, and `CloseWorkspace` removes the workspace in front
|
|
when it has no window and no name and is not the monitor's last. A name is komorebi's: the
|
|
compositor's desktops keep theirs.
|
|
|
|
A window the compositor reports shown or focused on a monitor that is not the focused one
|
|
focuses that monitor ([`WindowManager::monitor_idx_from_window`]). The monitor commands work on
|
|
the ring of monitors: `FocusMonitorNumber`, `CycleFocusMonitor` and `FocusMonitorAtCursor` focus
|
|
one ([`WindowManager::focus_monitor`]), and `FocusMonitorWorkspaceNumber` a workspace of it;
|
|
`MoveContainerToMonitorNumber`, `SendContainerToMonitorNumber`, their cycle and monitor-workspace
|
|
forms, and `MoveContainerToNamedWorkspace` and `SendContainerToNamedWorkspace` take the focused
|
|
container to another monitor, put its windows on the desktop of the workspace they go to, and tile
|
|
both monitors ([`WindowManager::move_container_to_monitor`]); `MoveWorkspaceToMonitorNumber` and
|
|
`CycleMoveWorkspaceToMonitor` take the workspace in front to another monitor, and
|
|
`SwapWorkspacesToMonitorNumber` exchanges the workspaces of two monitors.
|
|
`DisplayIndexPreference` gives a monitor, by its serial number id, the index the window manager
|
|
puts it at when it loads the monitors, with placeholders before it while the monitors in between
|
|
are not connected. `EagerFocus` focuses the first window of an executable, searching every
|
|
workspace of every monitor, and shows its workspace.
|
|
|
|
`PreselectDirection` reserves the index on one side of the focused container for the next
|
|
window ([`WindowManager::preselect_container_in_direction`]). The workspace inserts an empty
|
|
container there ([`Container::preselect`]), which [`Workspace::update`] keeps and the layout
|
|
gives a tile, and [`Workspace::new_container_for_window`] puts the next window in it.
|
|
`CancelPreselect` removes it. A workspace reserves one place at a time, and the grid layout
|
|
none.
|
|
|
|
A new window's container goes after the focused container, unless the workspace has placement
|
|
rules ([`Workspace::initial_window_placement_rules`]), which the entry of the workspace in
|
|
`komorebi.json` gives it ([`Workspace::load_static_config`]). A rule is a place for every window,
|
|
or a map from places to conditions, which are the conditions of the ignore and manage rules and are
|
|
read by [`should_act`] and [`should_act_individual`]; the first place in key order whose condition
|
|
matches the window is the window's. A place is a strategy ([`WindowPlacement`]: the primary tile,
|
|
the secondary, before the focused container, after it, the last place) or a number counting from
|
|
1, and the primary and the secondary tile are indexes 0 and 1 of every layout
|
|
([`Workspace::new_container_for_window`]). A place that does not exist, such as the secondary tile
|
|
of a workspace with one container or a number past the end, is the default place. A reserved place
|
|
(`PreselectDirection`) is used before the rules are asked, and the container is inserted through
|
|
`LockableSequence`, so that a rule that asks for the index of a locked container gets the index
|
|
after it. The rules apply wherever a window gets a container of its own: a window that opens, one
|
|
that comes back from a minimize, one that `ManageFocusedWindow` takes in, one that comes out of a
|
|
stack, and one that a workspace rule or the compositor moves to the workspace.
|
|
|
|
## Dragging
|
|
|
|
The user can move a window with the pointer, and resize it by dragging an edge. KWin reports the
|
|
start and the end of a drag (`interactiveMoveResizeStarted` and `interactiveMoveResizeFinished`),
|
|
the bridge sends both with the window's frame, and the end with where the pointer is, and the
|
|
table turns a move into a `MoveStart` and a `MoveEnd`, and a resize into a `ResizeStart` and a
|
|
`ResizeEnd` ([`Table::handle`]). The bridge also stops asking KWin again for the position komorebi
|
|
gave the window (`settle`): the window is the user's now.
|
|
|
|
`MoveStart` records the monitor, the workspace and the window
|
|
([`WindowManager::pending_move_op`]). `MoveEnd` takes the record, and when it names the same
|
|
window, looks at the monitor under the pointer. On another monitor than the one the move began
|
|
on, the window's container goes there and takes the place of the container whose tile the
|
|
pointer is on ([`WindowManager::transfer_window`]); a floating window joins the floating
|
|
windows there, a monocle container is put back among the containers first, and a maximized
|
|
window stays where it is. Both monitors are tiled again.
|
|
|
|
On the same monitor, `MoveEnd` compares the frame of the focused container's window with the
|
|
tile the layout last gave it ([`Workspace::latest_layout`]). A frame as wide and as tall as its
|
|
tile makes the drag a move. With the float override on, the window is then made a floating one
|
|
where it was let go. Otherwise the container under the pointer
|
|
([`Workspace::container_idx_from_current_point`], through the backend's `cursor_pos`) is the
|
|
target: the focused container swaps with it, or, with the window container behavior `Append`,
|
|
the window joins it ([`Workspace::move_window_to_container`]). The workspace is tiled again,
|
|
which puts a window let go where there is no tile back on its own. A frame of another size
|
|
changes nothing, and neither does a floating window, which is in no container
|
|
([`Workspace::contains_managed_window`]). The swap goes through `LockableSequence`, as
|
|
`MoveWindow`'s does.
|
|
|
|
A resize is measured from the frame the window had when the user took hold of it. The table keeps
|
|
that frame at the start ([`Table::drag_origin`], the backend's `drag_origin`), because the
|
|
`geometry` events of the drag change the window's frame before the window manager gets to the start;
|
|
a backend that keeps none, as the fake, gives the frame the window has when `ResizeStart` is
|
|
handled. `ResizeStart` records the window and that frame ([`WindowManager::pending_resize_op`]). At
|
|
the end the edges are where the user took them, which is not where the window's frame is: KWin
|
|
changes the frame once the client has drawn at the new size, and goes on reporting the client's
|
|
frames after the end of the drag, which can be before the window manager, busy with an earlier
|
|
event, gets to the end. The bridge therefore keeps the frame the last step of the drag asked for
|
|
(`interactiveMoveResizeStepped`) and sends it with the end as `requested`, and the table keeps it
|
|
apart from the window's frame ([`Table::drag_end`], the backend's `drag_end`), where a frame event
|
|
cannot reach it. `ResizeEnd` compares each edge of that frame, or of the window's frame when there
|
|
is none (a drag with no step), with the same edge of the frame the drag began with, focuses the
|
|
container of the dragged window, and calls [`WindowManager::resize_window`] for each edge that
|
|
moved, with twice the distance, since the layout moves a shared edge by half of what it is given. If
|
|
the window is in a container of a workspace that tiles, and the frame changed, the workspace is
|
|
tiled again, which also puts back a window whose edge had no neighbor to take room from.
|
|
[drag-resize.md](../drag-resize.md) has the rules and the reasons for each.
|
|
|
|
The neighbors follow the edge while it is dragged, and the animation effect, which does not
|
|
animate a resize by the user, leaves them to move at once. At every step of a resize KWin reports the
|
|
frame the pointer asks for (`interactiveMoveResizeStepped`); the bridge sends one step every 16 ms
|
|
at most, the newest, from a single-shot timer that sends the last one of a burst, and the table
|
|
makes the frame the window's and raises a `ResizeStep`. [`send_notification`] keeps at most one
|
|
`ResizeStep` waiting in the channel, and drops the others: the window manager reads where the
|
|
edge is when it gets to the step. `ResizeStart` keeps the workspace's
|
|
[`Workspace::resize_dimensions`] and [`Workspace::latest_layout`] as they are when the user takes
|
|
hold ([`LiveResize`], in [`WindowManager::live_resize`]), and puts the window in the list of
|
|
windows being resized ([`live_resize_begins`]), which [`Window::set_position`] leaves alone. A
|
|
step puts the kept layout back, applies the drag so far as `ResizeEnd` does, and tiles the
|
|
workspace; it makes no notification. `ResizeEnd` takes the window out of the list, puts the kept
|
|
layout back if a step moved the neighbors, and does the whole drag on it, so a drag ends the same
|
|
with steps as without, and one that is taken back puts the neighbors back. A window that opens or
|
|
closes part way through a drag changes the number of tiles: the kept layout is dropped, and the
|
|
steps after it change nothing until the end.
|
|
|
|
## Configuration
|
|
|
|
The daemon is configured by `komorebi.json`, which [`StaticConfig`] reads. `main.rs` takes the
|
|
file `--config` names, or else `komorebi.json` in [`HOME_DIR`] (`$KOMOREBI_CONFIG_HOME`, or
|
|
`~/.config/komorebi`) if there is one; without a file, the daemon runs on its defaults. Loading
|
|
has three steps, because the monitors are known only after the backend is asked:
|
|
|
|
1. [`StaticConfig::preload`] reads the file, applies the settings that are process-wide (the
|
|
default paddings, [`LAYOUT_DEFAULTS`], the indices of the monitors, the settings of the borders
|
|
in [`border_manager`], and the rules in [`IGNORE_IDENTIFIERS`], [`MANAGE_IDENTIFIERS`] and
|
|
[`FLOATING_APPLICATIONS`]) and makes the [`WindowManager`], with the settings that are its own.
|
|
It starts a watcher on the file.
|
|
2. [`WindowManager::init`] loads the monitors and the windows from the backend, in the order the
|
|
indices of the monitors give.
|
|
3. [`StaticConfig::postload`] gives each monitor the entry of the file that is its own, and each
|
|
workspace of the monitor the entry of its place ([`Workspace::load_static_config`]). A monitor
|
|
takes the entry at the index `display_index_preferences` gives its serial number id, or else
|
|
the first entry that no preference names and no monitor has taken. `main.rs` then asks the
|
|
backend for a desktop for each workspace ([`WindowManager::sync_workspace_desktops`]), and
|
|
tiles.
|
|
|
|
When the file changes, the watcher (a [`hotwatch`] watcher, which waits two seconds for a file
|
|
to settle) writes `ReloadStaticConfiguration` to the daemon's own socket, and the message is
|
|
handled like any other, with the window manager locked: [`StaticConfig::reload`] reads the file
|
|
first and applies it only when it parsed, so a file that is half written changes nothing. It does
|
|
for the window manager that is running what the three steps do for one that starts, and tiles
|
|
every monitor again.
|
|
|
|
`app_specific_configuration_path` names files of rules for particular applications, which
|
|
[`StaticConfig`] reads after the rules of its own (`apply_globals`): the `ignore`, `manage` and
|
|
`floating` rules of each application in each file are appended to the same lists, once each (a
|
|
rule that is in the list already is not added again). A path with no file is skipped with a
|
|
warning, since one `komorebi.json` may name a file that only another platform has; a file that is
|
|
there and is not an applications.json is an error.
|
|
|
|
Three kinds of key reach the model. A **setting** of the window manager is a field of it
|
|
([`WindowManager::resize_delta`] and the others). A **default** is a process-wide static that a
|
|
monitor or a workspace falls back to when it has no value of its own: the paddings, the layout
|
|
defaults. The settings of the borders are statics too, which the file sets and
|
|
[`border_manager::current_look`] and the layout read. A **rule** is an entry of one of the
|
|
process-wide lists. A workspace's own `layout_rules`, `work_area_offset_rules` and
|
|
`window_container_behaviour_rules` are fields of the [`Workspace`], which [`Workspace::update`]
|
|
reads each time it tiles, and so are its `initial_window_placement_rules`, which
|
|
[`Workspace::new_container_for_window`] reads when a window gets a container.
|
|
|
|
A workspace's `workspace_rules` and `initial_workspace_rules` are not fields of the window manager
|
|
but entries of [`WORKSPACE_MATCHING_RULES`], which [`StaticConfig::postload`] and
|
|
[`StaticConfig::reload`] empty and fill from the file, and the messages `WorkspaceRule`,
|
|
`InitialWorkspaceRule` and their named forms add to. [`WindowManager::enforce_workspace_rules`] reads
|
|
them: for each window in view (the top of each container, and the floating windows) that a rule
|
|
matches, on a workspace other than the rule's, it makes an operation, takes the window out of its
|
|
workspace (and hides it when that is the focused one), puts it in the target (making the workspace
|
|
if it is not there, and taking a floating window to the target's monitor), tiles the focused
|
|
workspace again if the window left it, and has each target monitor put its windows on the desktops
|
|
of their workspaces
|
|
([`Monitor::assign_windows_to_workspaces`]). `handle_event` calls it before every event about a
|
|
window it manages, and `postload`, `reload` and `EnforceWorkspaceRules` call it too. An `initial_only`
|
|
rule moves a window once: the window id goes into [`WindowManager::already_moved_window_handles`],
|
|
which `EnforceWorkspaceRules` empties and a destroy removes the id from.
|
|
|
|
`ReplaceConfiguration` is the third way in: [`WindowManager::process_command`] checks that the file
|
|
reads as a [`StaticConfig`], pauses the old window manager and brings back the windows it hid
|
|
([`WindowManager::restore_all_windows`]), makes a new window manager from the file
|
|
([`StaticConfig::preload`], [`WindowManager::init`], [`StaticConfig::reload`]) over the same command
|
|
socket, and puts it where the old one was, so the daemon starts over from the file, with the
|
|
windows the backend has, all of them in view, and a watcher on the new file. A file that does not read is logged and the old window manager stays.
|
|
`komorebic quickstart` writes the example file of the docs, `komorebic/applications.json` and the
|
|
sample `skhdrc` of the docs into the config home, compiled into the binary, and asks before it
|
|
writes over a file;
|
|
`komorebic enable-autostart` writes the XDG autostart entry that has Plasma start the daemon at login.
|
|
|
|
The same types describe the file as a JSON Schema, which `schemars` derives: `komorebic
|
|
static-config-schema` prints the schema of [`StaticConfig`], `application-specific-configuration-schema`
|
|
that of an applications file, and `notification-schema` and `socket-schema` those of a
|
|
[`Notification`] and of a [`SocketMessage`]; the daemon answers the same four questions on its
|
|
socket (the fourth is the schema of a list of applications files, where `komorebic` prints the
|
|
schema of one).
|
|
The schemas of the first two are committed (`schemas/linux/schema.json`, `schemas/linux/schema.asc.json`, written by `just
|
|
jsonschema`), and so are the options the module's `config` is typed with
|
|
(`nix/linux/komorebi-options.nix`, which `schemars-nixgen` makes from `schemas/linux/schema.json` and `just nixgen`
|
|
gives the hash of the schema as its first line). The check `schema-fresh` compares the schemas with
|
|
what the Rust types print and the hash with the schema, so a change to a key that leaves one of
|
|
them behind fails.
|
|
|
|
One `komorebi.json` is meant for every platform komorebi runs on, so [`StaticConfig`] has no
|
|
`deny_unknown_fields`: a key it does not read is dropped, and the keys of other platforms that
|
|
mean nothing on KDE Plasma are fields that nothing reads. `StaticConfig::from(&WindowManager)`
|
|
goes the other way, for `GenerateStaticConfig`.
|
|
|
|
Because a key that is not read is dropped without a word, `komorebic check` is how a typo is
|
|
found. The checks are in [`check`], and they return [`Finding`]s (a [`Level`], a message, and a hint)
|
|
that `komorebic` gathers in a [`Report`]: [`check_configuration`] reads the file as JSON, then as a
|
|
[`StaticConfig`], as the daemon does, and an error from either shows the line serde stopped at,
|
|
with the two lines before it ([`snippet`]). [`check_keys`] then compares the keys of the file
|
|
with [`known_keys`], which are read from the schema of [`StaticConfig`] at the root, in a monitor
|
|
and in a workspace: a key that is in the lists of the other platforms' keys is a note, and any
|
|
other that is not in the schema is a warning with the nearest key (the edit distance, at most
|
|
three). The settings that mean something are looked at too: an application specific
|
|
configuration or a wallpaper that is not there, a wallpaper no theme can be made from, a workspace
|
|
that is only a name, which is not tiled, and a workspace that appends windows to a container and
|
|
has placement rules, which are not used then (the workspace's `window_container_behaviour`, or the
|
|
file's when it has none). [`check_configuration`] also returns the [`Wants`] of the file: whether
|
|
it draws borders, which animation style it asks for and whether it has wallpapers. The key binding
|
|
file has a section of its own, which `komorebic` makes (see [Key bindings](#key-bindings)).
|
|
`komorebic` prints [`Report::render`] and exits with status 1 if [`Report::has_errors`].
|
|
|
|
[`check_environment`] looks at the desktop, with D-Bus calls of its own on the session bus (they
|
|
are in the table of [PLATFORM.md](PLATFORM.md#d-bus)), and `komorebic check --configuration-only`
|
|
leaves it out. It looks in this order at the session (`XDG_CURRENT_DESKTOP` and
|
|
`XDG_SESSION_TYPE`, each a warning when it is not what komorebi is made for), the session bus, KWin
|
|
on it (`NameHasOwner`), KWin's version (the line `KWin version:` of `supportInformation`), the daemon
|
|
(a connection to `komorebi.sock`) and its script (`isScriptLoaded`), the two effects, the Plasma
|
|
shell when the file has wallpapers (`NameHasOwner` of `org.kde.plasmashell`), and last at what
|
|
starts komorebi at login. A bus that cannot be connected to or does not answer, and no KWin on it,
|
|
are errors, and the looking stops there: everything after depends on them. Every call has two
|
|
seconds to answer, so a bus that never does cannot hold the check.
|
|
|
|
The two effects are not part of KWin, and only komorebi's builds of them do everything komorebi
|
|
asks: a color for each kind of window (KDE Rounded Corners) and every `animation.style` (Geometry
|
|
Change). The stock builds have a color for the active window and one for the others, and one curve,
|
|
an exponential ease out. The check tells a build by what it contains: the plugin file of the border
|
|
effect has the name of the interface `org.komorebi.BorderColours1` (or its object is on the bus),
|
|
and the script of the animation effect reads a `Curve` setting. It looks for them where KWin looks,
|
|
which it reads from KWin's own environment (`/proc/PID/environ`, PID being the owner of
|
|
`org.kde.KWin`), because the shell the command is typed in has other directories than a session.
|
|
What the file asks of the effects is its [`Wants`]: a stock effect is a warning only when the file
|
|
asks for what it cannot do.
|
|
|
|
| Rule | Enforced by | Proof | Escape hatch |
|
|
|---|---|---|---|
|
|
| A key komorebi does not read does not stop a configuration from loading | serde: [`StaticConfig`] ignores unknown keys, and a key of another platform is a field nothing reads | `the_keys_that_do_nothing_on_kde_are_accepted_and_ignored`, `fixtures_load_round_trip_and_only_lose_windows_only_keys` | none |
|
|
| The keys the parity test ignores are keys `StaticConfig` does not read | test: the ignore lists are compared with the schema of [`StaticConfig`] | `the_lists_name_no_key_that_is_read` | none |
|
|
| The example in the docs is the example file, which loads | test | `the_example_in_the_docs_is_the_example_file`, `fixtures_load_round_trip_and_only_lose_windows_only_keys` | none |
|
|
| A path to an applications file that is not there is skipped, and one that is there and is broken is an error | framework path: `handle_asc_file` returns early when the path does not exist, and parses the file otherwise | `a_missing_application_specific_configuration_is_skipped_not_fatal`, `an_application_specific_configuration_that_exists_but_is_broken_is_still_an_error`, `a_file_that_is_not_there_is_skipped_and_one_that_is_broken_is_an_error` | none |
|
|
| A file that is not JSON, or has a value komorebi does not read, is an error that shows the line | framework path: [`check_configuration`] reads the file as JSON, then as a [`StaticConfig`], and an error from either goes through [`snippet`] | `a_file_that_is_not_json_is_an_error_with_the_line`, `a_value_komorebi_does_not_know_is_an_error_with_the_line`, `a_file_that_is_not_json_fails_and_shows_the_line` | none |
|
|
| A key for another platform is a note, and a key that no platform has is a warning with the nearest key | framework path: [`check_keys`] sorts the keys of the file by [`known_keys`] and the lists of the other platforms' keys | `keys_for_another_platform_are_told_apart_from_mistakes`, `a_typo_is_a_warning_with_the_word_that_was_meant_and_does_not_fail` | none |
|
|
| The keys the check knows are the ones komorebi reads, at every level | framework path: [`known_keys`] reads them from the schema of [`StaticConfig`] | `the_keys_come_from_the_schema_at_every_level`, `a_whole_example_configuration_has_nothing_the_check_does_not_know` | built without the `schemars` feature, the check does not look at keys |
|
|
| Placement rules that are not used while windows are appended are a note, and a workspace that creates containers is not told so whatever the file says | framework path: [`check_configuration`] reads the workspace's `window_container_behaviour` before the file's | `placement_rules_that_are_not_used_while_windows_are_appended_are_noted` | none |
|
|
| A wallpaper that is not a file is an error, and a picture no theme can be made from is a warning | framework path: [`check_configuration`] looks at the wallpaper of each monitor and workspace once, and asks the theme generator for a palette when the wallpaper does not turn it off | `a_wallpaper_that_is_not_there_and_a_rules_file_that_is_not_there_are_found`, the `vm-check` VM suite for the warning | none |
|
|
| No session bus, or no KWin on it, is an error, and the desktop is looked at no further | framework path: [`check_environment`] returns at the first finding the rest depends on | `without_a_session_bus_the_desktop_is_an_error`, `vm-check` | none |
|
|
| An effect is komorebi's build because of what it contains, not because of its name | framework path: [`check_environment`] looks for the colors interface in the border effect's plugin file (or its object on the bus) and for a `Curve` setting in the animation effect's script | `the_border_effect_is_komorebis_when_it_has_the_colours_interface`, `the_effects_are_told_apart_by_what_they_contain` | none |
|
|
| Effects are looked for where KWin looks, not where the shell that runs the command would | framework path: [`check_environment`] reads KWin's environment from `/proc` | `vm-check` | none |
|
|
| A daemon that is running without its script in KWin is an error | framework path: [`check_environment`] compares the connection to the daemon's socket with KWin's answer to `isScriptLoaded` | `vm-check` | none |
|
|
| No call of the desktop check delays it by more than two seconds | convention: every call goes through one timeout (`within`) | `a_session_bus_that_never_answers_is_given_up_on` | none |
|
|
| `--configuration-only` leaves the desktop out | framework path: the section is pushed only without it | `configuration_only_leaves_the_desktop_out` | none |
|
|
| `komorebic check` exits with status 1 if and only if a finding is an error | framework path: `komorebic` exits through [`Report::has_errors`] | `a_good_configuration_passes`, `a_typo_is_a_warning_with_the_word_that_was_meant_and_does_not_fail`, `a_file_that_is_not_json_fails_and_shows_the_line` | none |
|
|
| The rules of an applications file and of the configuration add up | framework path: both are appended to the process-wide lists | `the_rules_of_the_configuration_and_of_the_file_add_up`, `every_file_of_a_list_is_read` | none |
|
|
| `schemas/linux/schema.json` and `schemas/linux/schema.asc.json` are what the Rust types print | Nix check `schema-fresh`, and a test that compares them with `komorebic`'s output | `schema-fresh`, `the_schema_files_of_the_repository_are_what_the_subcommands_print` | `just jsonschema` |
|
|
| The options the module is typed with are made from the committed schema | the hash of `schemas/linux/schema.json` in the first line of `nix/linux/komorebi-options.nix`, compared by `schema-fresh` | `schema-fresh` | `just nixgen` |
|
|
| The example, in Nix and in JSON, follows the schema, and says the same in both | Nix check `nix-options-validation`: the options evaluated against the Nix example, both examples checked with `check-jsonschema`, and one contained in the other | `nix-options-validation` | none |
|
|
| A window a workspace rule names ends on its workspace, and on that workspace's desktop once the workspace is shown | framework path: [`WindowManager::enforce_workspace_rules`] moves it in the model, and has the target monitor put it on the desktop | `a_rule_sends_the_window_of_an_application_to_its_workspace`, `a_window_a_rule_moves_between_workspaces_that_are_out_of_view_is_put_on_the_new_desktop`, `vm-workspace-rules` | none |
|
|
| A window a workspace rule takes off the workspace in front is out of sight until its workspace is shown | framework path: [`WindowManager::enforce_workspace_rules`] hides it, and showing the workspace restores it | `a_rule_sends_the_window_of_an_application_to_its_workspace`, `vm-workspace-rules` | none |
|
|
| An initial rule sends a window once, a permanent one every time | framework path: the window ids an initial rule has sent are in [`WindowManager::already_moved_window_handles`] until the window closes | `an_initial_rule_moves_a_window_once_and_a_permanent_one_every_time`, `vm-workspace-rules` | `komorebic enforce-workspace-rules` forgets them |
|
|
| The rules of the configuration are the file's, and the windows that are open follow them | framework path: [`StaticConfig::postload`] and [`StaticConfig::reload`] empty the list, fill it from the file and enforce it | `the_rules_of_komorebi_json_send_the_windows_that_are_open_and_come_out_in_the_generated_file`, `vm-workspace-rules` | none |
|
|
| Replacing the configuration with a file that does not read leaves the window manager as it was | framework path: the arm reads the file as a [`StaticConfig`] before it makes anything | `replacing_the_configuration_makes_the_window_manager_again_from_the_new_file` | none |
|
|
| Replacing the configuration leaves no window hidden | framework path: the arm brings back the windows the old window manager hid ([`WindowManager::restore_all_windows`]) before the new one reads the desktop | `replacing_the_configuration_makes_the_window_manager_again_from_the_new_file` | none |
|
|
| `quickstart` never writes over a file without being told to | framework path: every file goes through `write_file_with_prompt`, which keeps the file unless the answer is `y` or `yes` | `quickstart_writes_the_example_files_and_asks_before_it_overwrites_one` | none |
|
|
| The entry `enable-autostart` writes is one the session runs | VM test: systemd's autostart generator makes a unit of it, which starts a daemon | `autostart_writes_an_xdg_entry_that_starts_komorebi_and_takes_it_away_again`, `vm-module` | none |
|
|
| A file that cannot be read changes nothing | framework path: [`StaticConfig::reload`] reads and parses the file before it applies anything | `a_configuration_that_cannot_be_read_changes_nothing`, `the_daemon_follows_the_file_it_is_given_when_the_file_changes` | none |
|
|
| The daemon applies a change to its file, written in place or replaced by a rename | framework path: the watcher sends `ReloadStaticConfiguration` to the daemon's own socket | `the_daemon_follows_the_file_it_is_given_when_the_file_changes`, `vm-module` | none: the file has to be the one the daemon was started with |
|
|
| A monitor takes the entry its display index preference gives it, else the first nobody has | framework path: [`StaticConfig::postload`] and [`StaticConfig::reload`] choose the entry by serial number id first | `a_monitor_takes_the_configuration_its_display_index_preference_gives_it` | none |
|
|
| A workspace that names no layout and does not say it tiles moves no window | framework path: [`Workspace::load_static_config`] sets [`Workspace::tile`] | `a_workspace_with_no_layout_does_not_tile`, `a_workspace_tiles_when_it_has_a_layout_or_is_told_to` | `"tile": true` |
|
|
| A workspace's own layout options replace the defaults of its layout, whole | framework path: [`Workspace::update`] takes the workspace's options and rules when it has either, and else those of `layout_defaults` | `layout_options_of_a_workspace_replace_the_defaults_of_its_layout` | none |
|
|
| The highest of the rules a count of containers reaches decides the layout, the offset and the container behavior | framework path: [`Workspace::update`] reads the rules, kept in the order of their thresholds | `layout_rules_change_the_layout_with_the_number_of_windows`, `work_area_offset_rules_change_the_offset_with_the_number_of_windows`, `window_container_behaviour_rules_change_the_behaviour_with_the_number_of_containers` | none |
|
|
| A workspace's own container behavior and float override come before the one for every workspace, and its floating layer behavior before its monitor's, which comes before the one for every workspace | framework path: [`WindowManager::window_management_behaviour`] reads the workspace, then (for the floating layer) the monitor, then the window manager | `the_behaviour_of_a_workspace_comes_before_the_one_for_every_workspace`, `the_float_override_of_a_workspace_comes_before_the_one_for_every_workspace`, `the_floating_layer_behaviour_of_a_workspace_comes_before_a_monitors_and_that_before_the_default` | none |
|
|
| A workspace places the windows that open by the `initial_window_placement_rules` of its entry in `komorebi.json` | framework path: [`Workspace::load_static_config`] sets [`Workspace::initial_window_placement_rules`] from the entry, and [`Workspace::new_container_for_window`] reads it | `a_workspace_in_komorebi_json_has_them`, `the_rules_come_from_the_configuration_of_the_workspace`, `vm-placement` | none |
|
|
| The schema, and the module's typed `config`, take a place in each form the daemon reads: a name, a number, or text that is a number | test: the schema of [`PlacementTarget`] is written by hand, to say what its `Serialize` and `Deserialize` do, and a file with a place of each form is checked against `schemas/linux/schema.json` | `a_place_is_described_as_a_name_a_number_or_text_that_is_a_number`, `nix-options-validation`, `nixos-module-eval` ("the place a workspace gives the windows that open is a typed option in each form it is read") | none |
|
|
| The configuration the daemon generates loads and says the same | test | `a_configuration_comes_out_again_as_the_settings_of_the_window_manager` | none |
|
|
| The daemon reads the file it is given or the one in the config home, not both | startup order: `main.rs` takes `--config` before the config home | `the_daemon_follows_the_file_it_is_given_when_the_file_changes`, `the_daemon_reads_komorebi_json_from_the_config_home` | none |
|
|
|
|
## The look
|
|
|
|
komorebi decides what the windows look like and the compositor draws it, since only the
|
|
compositor has the frames, the corners and the shadows. A border is the outer outline of a window,
|
|
drawn outside it, in the gap the layout leaves between tiles, and an animation is a window
|
|
sliding to its new place instead of jumping there. The window manager describes a
|
|
[`Look`] and asks the backend for it ([`Backend::apply_look`]); the backend sees that it is drawn
|
|
and answers whether borders are being drawn. The reasons are in
|
|
[DESIGN.md](DESIGN.md#why-does-the-compositor-draw-the-borders-and-not-komorebi).
|
|
|
|
[`border_manager`] holds the settings of the border, as statics that the messages `Border`,
|
|
`BorderWidth` and `BorderOffset` set: whether borders are on, their width, the offset, the radius
|
|
of the corners and the two colors. [`animation`] holds the settings of the animation, which
|
|
`Animation`, `AnimationDuration`, `AnimationStyle` and `AnimationFps` set: whether windows are
|
|
animated, for how long and with what easing, for every kind of animation or for one
|
|
([`AnimationPrefix`]; movement is the only kind). The
|
|
settings of one kind win over the global ones, and a global setting clears the ones of the kinds.
|
|
The commands that change a stack turn the animation off while they run and put it back, as
|
|
komorebi does on the other platforms; the file's `animation` key sets the same settings.
|
|
[`border_manager::current_look`] makes the [`Look`] of them all, and
|
|
[`border_manager::send_notification`] asks the backend for it and keeps the answer in
|
|
[`BORDER_DRAWN`]. It is called before the first layout, after every event and every message, and
|
|
after every retile. A backend remembers the look it applied last and does nothing when it is asked
|
|
for the same again, so nothing reaches KWin unless a setting changed.
|
|
|
|
What a window is to the layout decides the color of its border: the focused window of the focused
|
|
container on the focused monitor is `Single`, or `Stack` when its container has more than one
|
|
window, the focused window of every other container is `Unfocused` or, when the container is
|
|
locked, `UnfocusedLocked`, and a floating window is `Floating` while it has the focus and
|
|
`Unfocused` otherwise ([`border_manager::window_kinds`]). After every event and every message
|
|
[`border_manager::update_colours`] tells the backend the color of each window's border
|
|
([`Backend::set_border_colours`]), or that none has one when borders are off, and the backend sends
|
|
nothing it sent last time. `BorderColour` sets the color of one kind.
|
|
|
|
A drawn border takes room. While [`BORDER_DRAWN`] is set, [`Monitor::update_workspace_globals`]
|
|
gives the workspace the width and the offset of the border, and [`Workspace::update`] takes both
|
|
off every side of every tile, so the border falls in the gap and not on the window next to it.
|
|
Without a border, because they are off or the compositor has no effect to draw one, the room is
|
|
given back the next time the workspace is tiled.
|
|
|
|
The KWin backend draws the border with the effect KDE Rounded Corners
|
|
([`kwin::effects`](../../../crates/komorebi-linux/src/kwin/effects.rs)). `apply_look` writes the effect's settings
|
|
into the user's `kwinrc`, group `[Round-Corners]`, through [`Kwinrc`]; loads the effect with
|
|
`loadEffect` unless it is loaded; and always asks it to read its settings again
|
|
(`reconfigureEffect`), since KWin hands an effect the copy of `kwinrc` it read when it started. A
|
|
look without a border unloads the effect, if komorebi loaded it, and puts the user's settings
|
|
back. `shutdown` does the same. An effect KWin did not find when it started is reported once and
|
|
komorebi goes on without it, with `apply_look` answering false. The animation is Geometry Change:
|
|
its `Duration` and `Curve` are the look's, and cross-fading is off. The curve is a Qt easing
|
|
curve, the name of the style without its "Ease" ([`QtCurve::qt_curve`]); the stock effect
|
|
ignores `Curve` and eases out exponentially, and komorebi's build of it reads it. KWin animates
|
|
what is painted, so the
|
|
frame the table reads is the final one at once and nothing waits for an animation to end. The
|
|
stock border effect has one color for
|
|
the active window and one for the others; komorebi's build of it takes a color for each window,
|
|
over D-Bus, by the UUID KWin knows the window by. `set_border_colours` answers false, and logs it
|
|
once, when the effect is the stock one: every border is then the focused or the unfocused color of
|
|
the look.
|
|
[PLATFORM.md](PLATFORM.md#kwin-effects) has the effect, its settings and what was measured.
|
|
|
|
## Themes
|
|
|
|
A theme gives the borders their colors. The `Theme` message carries a [`KomorebiTheme`]: a
|
|
Catppuccin theme, a Base16 theme or a Base16 palette, each with the color it gives to each of the
|
|
six kinds of border, and a default for the ones it does not name.
|
|
[`theme_manager::send_notification`] puts the six colors into the statics of [`border_manager`],
|
|
keeps the theme as [`theme_manager::CURRENT_THEME`] and asks the backend for the look again, on
|
|
the thread that handled the message; [`border_manager::update_colours`] hands them to the border
|
|
effect with the next event or message. The palette of a picture is made by
|
|
`komorebi-themes` ([`komorebi_themes::generate_base16_palette`]), which takes seconds, so
|
|
[`theme_manager::wallpaper_palette`] keeps each palette it makes in `komorebi` in the user's cache
|
|
directory, in a file named by [`theme_manager::palette_file`] for the picture, a hash of its path,
|
|
size and time of last change, and the variant: a picture that was painted over, or one of the same
|
|
name in another folder, has a palette of its own. A picture it cannot make a palette from (it
|
|
cannot be read, or has fewer than eight colors) has none, and the borders keep their colors.
|
|
[DESIGN.md](DESIGN.md#why-is-a-palette-kept-by-the-hash-of-its-picture) has the reason.
|
|
|
|
## Wallpapers
|
|
|
|
A [`Monitor`] has a [`Wallpaper`] and so can each of its workspaces: the `wallpaper` of an entry of
|
|
`monitors` and of `workspaces` in `komorebi.json`, which [`StaticConfig::postload`] and
|
|
[`StaticConfig::reload`] give them. [`Workspace::apply_wallpaper`] asks the backend to show the workspace's own wallpaper
|
|
or else its monitor's ([`Backend::set_wallpaper`]), and, unless the wallpaper says
|
|
`generate_theme: false`, makes a theme of the palette of the picture
|
|
([`theme_manager::wallpaper_palette`]), with the variant and the colors of its `theme_options`, and
|
|
hands it to [`theme_manager::send_notification`]: the colors are the borders' before the
|
|
workspace is shown. It is called wherever a workspace comes into view. [`Workspace::restore`], which
|
|
[`Monitor::load_focused_workspace`] calls for komorebi's own switches, calls it;
|
|
[`WindowManager::adopt_workspace_change`] calls it for a switch something else made, and
|
|
[`WindowManager::retile_all`] for the workspace in front at the start, and
|
|
[`WindowManager::apply_wallpaper_for_monitor_workspace`], which [`StaticConfig::reload`] calls for the
|
|
workspace each monitor shows, has a reloaded file's wallpapers shown at once. A monitor and a
|
|
workspace without a wallpaper are not asked about.
|
|
|
|
Plasma has one wallpaper for each screen, which the shell, a process of its own, keeps. The KWin
|
|
backend does not set it from the thread of the window manager: [`Backend::set_wallpaper`] notes the
|
|
wish for the output (by name, which is how the shell tells screens apart) and starts a task on the
|
|
runtime, which asks the shell what the screen shows and sets the picture only if it differs, with
|
|
the plugin that shows a picture, filling the screen ([`kwin::wallpaper`]). A shell that is not
|
|
there yet is tried again for about half a minute, with longer waits, and the failure is said once.
|
|
[PLATFORM.md](PLATFORM.md#the-plasma-shell) has what the shell is asked.
|
|
|
|
## Transparency
|
|
|
|
The opacity of a window is a property KWin has, so komorebi sets it ([`Backend::set_opacity`],
|
|
which the KWin backend sends the bridge as an `opacity` command: `alpha / 255`) and the compositor
|
|
draws the window so. [`transparency_manager`] holds the settings, as statics that `Transparency`,
|
|
`ToggleTransparency` and `TransparencyAlpha` set and the `transparency` and `transparency_alpha`
|
|
keys of the file give a value, and the rules for the ignored windows (`TRANSPARENCY_BLACKLIST`, set
|
|
by the `transparency_ignore_rules` of the file). [`transparency_manager::apply`] is called
|
|
before the first layout and after every event and every message: it works out
|
|
[`transparency_manager::transparent_windows`], on the workspace each monitor shows (the window in
|
|
front in every container, unless the container has the focus on the focused monitor; a monocled
|
|
window only on a monitor that is not the focused one; none on a workspace that does not tile; none
|
|
that a rule ignores), and sends the backend only what differs
|
|
from what the window manager kept in [`WindowManager::opacity_applied`]. A window that should not
|
|
be see-through any more is made opaque, and with transparency off every window is. When komorebi
|
|
stops, [`transparency_manager::restore`] makes every window it changed opaque again. The border
|
|
effect draws a window from a texture that has its opacity in it, and the build komorebi installs
|
|
draws it again when the opacity changes ([PLATFORM.md](PLATFORM.md#the-patches)).
|
|
|
|
## Focus follows mouse
|
|
|
|
The window under the pointer has the focus when the compositor's focus policy says so, and the
|
|
compositor does it. `FocusFollowsMouse` and `ToggleFocusFollowsMouse` set
|
|
[`WindowManager::focus_follows_mouse`] (the implementation that was asked for, or none) and ask the
|
|
backend ([`Backend::set_focus_follows_mouse`]); whichever of the two implementations is named, on
|
|
KDE it is the compositor's own policy. The KWin backend writes `FocusPolicy=1` and
|
|
`DelayFocusInterval=0` into the `[Windows]` group of the user's `kwinrc` through [`Kwinrc`], which
|
|
records the first value of each key, and asks KWin to reload; turning it off puts those two keys
|
|
back ([`Kwinrc::restore_keys`]) and leaves `PerOutputVirtualDesktops`, which is in the same group,
|
|
as it is. `shutdown` puts back everything. The focus the compositor gives is an event like the
|
|
others: the window manager follows it, and [`State`] says whether focus follows mouse is on.
|
|
[DESIGN.md](DESIGN.md#why-is-focus-follows-mouse-kwins-own-focus-policy) has the reasons.
|
|
|
|
## The NixOS module
|
|
|
|
The flake exports one module under two names, `nixosModules.default` and
|
|
`nixosModules.komorebi`: [`nixos-module.nix`] and the flake's overlay, which builds
|
|
`komorebi-full` (both programs) with the flake's nixpkgs and Rust toolchain. Its options are
|
|
`enable`, `package` (by default `pkgs.komorebi-full`, from the overlay), `logLevel`,
|
|
`configFile`, `config`, `applicationSpecificConfiguration`, `effects.borders.enable` and
|
|
`effects.borders.package` (by default `pkgs.kde-rounded-corners-komorebi`, from the overlay),
|
|
`effects.animation.enable` and `effects.animation.package`
|
|
(`pkgs.kwin-effect-geometry-change-komorebi`), `keybindings` (`format`, `text` or `file`, `mode`,
|
|
`chordTimeout`, `layout`, `strict`, `appAliases`), `kwin.untestedIsError`, and `execStart`, which is
|
|
read-only: the module sets it to `bin/komorebi` of the package with `--log-level`, `--config` with a
|
|
`komorebi.json` when `configFile` or `config` is set, and `--keybindings` and the options of the
|
|
engine when there are key bindings. `config` is typed from
|
|
`nix/linux/komorebi-options.nix`, the options `schemars-nixgen` makes from the schema of `komorebi.json`;
|
|
the module writes it as JSON, with no null in it, and `configFile` and the written file both go
|
|
through `check-jsonschema` against `schemas/linux/schema.json` when the system is built (comment lines taken out
|
|
first, since the checker reads plain JSON). `applicationSpecificConfiguration.sources` are files
|
|
fetched from GitHub that become `app_specific_configuration_path`; the module has none by default.
|
|
`internal.configAttrs` is the configuration as the attribute set the options make, for the tests.
|
|
With `services.komorebi.enable`, the module:
|
|
|
|
- installs the package in the system profile, and the effect that draws the borders unless
|
|
`effects.borders.enable` is off and the one that animates windows unless
|
|
`effects.animation.enable` is off;
|
|
- defines the user unit `komorebi.service`, which every user of the machine has. Plasma's
|
|
`plasma-workspace.target` wants it, so a Plasma session starts it at login, and it is ordered
|
|
after `plasma-core.target`, which Plasma orders after KWin's service, `plasma-kwin_wayland`;
|
|
- runs `execStart` in it, with `Restart=on-failure`, the exit status of a `SIGTERM` (130)
|
|
counted as success, and a `PATH` of `/run/wrappers/bin`, `/etc/profiles/per-user/%u/bin` and
|
|
`/run/current-system/sw/bin` instead of the store paths NixOS gives a unit;
|
|
- runs `komorebic hotkeys release` after the daemon has stopped (`ExecStopPost`), which
|
|
unregisters the keys a daemon that was killed left in kglobalaccel;
|
|
- asserts that Plasma 6 is enabled;
|
|
- with key bindings, asserts that exactly one of `text` and `file` is set, that `format` is,
|
|
and that an skhdrc has no `.load`; runs `komorebic hotkeys compile` on them at build time in
|
|
the package `komorebi-keybindings` (`--strict` with `strict`), whose
|
|
`share/kglobalaccel/*.desktop` (the static shortcuts) it installs in the system profile, with
|
|
the diagnostics in its `diagnostics.txt` and in the build log; and passes the file (a store
|
|
path), its format and mode, the chord timeout and the aliases to the daemon in `execStart`,
|
|
so that the daemon plans the file as the build did and runs the rest;
|
|
- checks the system's KWin, `pkgs.kdePackages.kwin.version`, against
|
|
[`supported-kwin.nix`]: it asserts that it is `min` or newer, and warns when it is newer than
|
|
`testedMax`, or asserts that it is not when `kwin.untestedIsError` is set.
|
|
|
|
The VM tests run on machines built with it ([vm-tests.md](vm-tests.md)). Their headless node
|
|
runs the daemon in test sessions, with a template unit, `komorebi@NAME`, that runs the module's
|
|
`execStart` on the session's private bus. The session node logs in to Plasma, which starts the
|
|
module's unit. The Plasma guests to develop in ([utm.md](utm.md)) install komorebi with the module
|
|
too.
|
|
|
|
| Rule | Enforced by | Proof | Escape hatch |
|
|
|---|---|---|---|
|
|
| komorebi is enabled only on a machine with Plasma 6 | evaluation error: an assertion of the module | `nixos-module-eval` ("without Plasma 6, an assertion fails") | none |
|
|
| komorebi is enabled only with a KWin it supports, `min` of [`supported-kwin.nix`] or newer | evaluation error: an assertion of the module | `nixos-module-eval` ("a KWin older than min fails an assertion", "with a KWin older than min, the system does not evaluate") | none |
|
|
| A KWin newer than any every VM suite has passed against is reported | a warning of the module, at evaluation; an assertion with `kwin.untestedIsError` | `nixos-module-eval` ("a KWin newer than testedMax is a warning", "with untestedIsError, a KWin newer than testedMax fails an assertion") | `kwin.untestedIsError = false`, the default |
|
|
| `testedMax` is a KWin every VM suite has passed against | convention: a commit raises it after a run of every suite, and says which | none | none |
|
|
| The log level is one the daemon accepts | evaluation error: `logLevel` is an enum of the daemon's `--log-level` values | `nixos-module-eval` ("a log level the daemon does not have does not evaluate") | none |
|
|
| The unit runs `execStart`, and nothing but the module sets it | evaluation error: the option is read-only | `nixos-module-eval` ("execStart cannot be set", "the unit runs execStart") | a unit of your own |
|
|
| The daemon is configured by `configFile` when it is set, else by `config` | framework path: `execStart` passes the checked file as `--config`, which comes before the config home | `nixos-module-eval` ("configFile reaches execStart and the unit as --config", "config reaches execStart ..."), `vm-module` | none: with neither the daemon reads the file of the config home |
|
|
| A configuration with a layout or a value komorebi does not have does not build | evaluation error: `config` is typed from the schema; and `check-jsonschema` fails the build for a file that does not follow it | `nixos-module-eval` ("a layout komorebi does not have does not evaluate"), `nix-options-validation` | `configFile` with a file the schema accepts |
|
|
| The sources of application specific configuration reach the daemon only while `applicationSpecificConfiguration.enable` is set | framework path: the module adds `app_specific_configuration_path` to the file it writes only when `enable` is set and there are sources | `nixos-module-eval` ("with applicationSpecificConfiguration.enable = false, the sources are left out of config", "with the sources turned off and no config, there is nothing to write") | none |
|
|
| `komorebi` and `komorebic` are on every user's `PATH` | the module installs the package in the system profile | `nixos-module-eval`; every VM suite runs `komorebic` | none |
|
|
| The border effect is installed with komorebi, and only by the module | the module installs `effects.borders.package` in the system profile when `effects.borders.enable` is set | `nixos-module-eval` ("the border effect is installed in the system profile", "with the border effect off, komorebi is installed and the effect is not"), `vm-look` | `effects.borders.enable = false` |
|
|
| The border effect the module installs is built for the KWin of the system and is not loaded until komorebi asks | the package: `kde-rounded-corners-komorebi` links the `libkwin` of the nixpkgs' KWin and says `EnabledByDefault` false in its metadata | the `kde-rounded-corners-komorebi-install` check | `effects.borders.package` |
|
|
| The animation effect is installed with komorebi, and not loaded until komorebi asks | the module installs `effects.animation.package` when `effects.animation.enable` is set, and the package's metadata says `EnabledByDefault` false | `nixos-module-eval` ("the animation effect is installed in the system profile", "with the animation effect off, ..."), the `kwin-effect-geometry-change-komorebi-install` check, `vm-look` | `effects.animation.enable = false` |
|
|
| Every Plasma session starts komorebi, once KWin's service has started | systemd: the user unit is wanted by `plasma-workspace.target`, after `plasma-core.target` | `nixos-module-eval`, `vm-module` | `systemctl --user mask komorebi` |
|
|
| A daemon that fails or is killed is started again | systemd: `Restart=on-failure` | `vm-module` (`SIGKILL`) | none |
|
|
| The keys of a daemon that was killed are given back | systemd: `ExecStopPost` runs `komorebic hotkeys release` | `nixos-module-eval` ("the unit releases the keys of the daemon when it stops"), `vm-restart-survival` | none |
|
|
| A daemon that is told to stop stays stopped, and the unit is not failed | systemd: the exit statuses of `Stop` (0) and of `SIGTERM` (130) are successes | `vm-module` | none |
|
|
| The daemon has the `PATH` of a terminal in the session | the unit's environment, with NixOS's default `PATH` turned off | `nixos-module-eval`, `vm-module` | none |
|
|
| The static shortcuts the module installs and the bindings its daemon runs are one plan of one file | framework path: the build and `execStart` get the same store file, format and mode, and `plan` is deterministic | `nixos-module-eval` (the key binding scenarios), `vm-keys-skhd`, `vm-keys-whkd`, `vm-keys-sxhkd` | none |
|
|
| Key bindings have one source, a format, and no skhd `.load` | evaluation error: assertions of the module | `nixos-module-eval` ("text and file together", "without a format", "skhd's .load") | none |
|
|
| `strict` fails the build on a warning or an error | `komorebic hotkeys compile --strict` exits non-zero before it writes anything, and the module's build of the key bindings passes `--strict` only when `strict` is set and fails when `komorebic` does | `strict_refuses_a_file_with_a_warning_and_writes_nothing`, `nixos-module-eval` ("with strict, the build of the key bindings runs komorebic hotkeys compile --strict", "without strict ... no --strict") | none |
|
|
| What `hotkeys compile` prints for every file of the corpus is what was reviewed | test: a diff against `nix/linux/tests/keybindings.expected` | `keybindings-fixtures` | writing the file again |
|
|
| The VM suites run komorebi as the module installs it | framework path: the test nodes import the module, and `komorebi@NAME` runs its `execStart` | every VM suite | a suite's `node` can set `services.komorebi.enable = false` (`vm-render-smoke` does) |
|
|
|
|
## The parts
|
|
|
|
- [`backend`](../../../crates/komorebi-linux/src/backend/mod.rs): the trait, the two data types, and the
|
|
process-wide handle ([`set_backend`], [`backend`], [`with_backend`]).
|
|
- [`backend::fake`](../../../crates/komorebi-linux/src/backend/fake.rs): the fake.
|
|
- [`ring`](../../../crates/komorebi-collections/src/ring.rs): the [`Ring`], and the macro that gives every type
|
|
holding one the same accessors (`containers()`, `focused_container_mut()`, ...).
|
|
- [`monitor`](../../../crates/komorebi-linux/src/monitor.rs), [`workspace`](../../../crates/komorebi-linux/src/workspace.rs),
|
|
[`container`](../../../crates/komorebi-linux/src/container.rs), [`window`](../../../crates/komorebi-linux/src/window.rs):
|
|
the four levels of the tree; `window` also has the rule engine ([`should_act`]).
|
|
- [`lockable_sequence`](../../../crates/komorebi-collections/src/lockable_sequence.rs): inserting, removing and
|
|
swapping in a list where some elements are locked in place, which is how a workspace
|
|
changes its containers.
|
|
- [`window_manager`](../../../crates/komorebi-linux/src/window_manager.rs): the [`WindowManager`] and the
|
|
operations on it (tile the workspace in front, tile every monitor, take a minimized
|
|
window out, move the focus, move the focused container, change the layout, resize or
|
|
promote the focused container, float a window, switch the layer, put a container in
|
|
monocle, maximize a window, lock a container, set the padding of a workspace, manage and
|
|
unmanage a window, stack and unstack windows and choose the one in front, show a
|
|
workspace, move a container to another workspace, follow a workspace switch made by
|
|
something else, name a workspace and find it by its name, set the layout of a workspace
|
|
that is not in front, focus another monitor, and move containers and workspaces between
|
|
monitors).
|
|
- [`desktop`](../../../crates/komorebi-linux/src/desktop.rs): loads the monitors and the open windows from
|
|
the backend, and reads the sizes of the monitors again.
|
|
- [`window_manager_event`](../../../crates/komorebi-linux/src/window_manager_event.rs) and
|
|
[`process_event`](../../../crates/komorebi-linux/src/process_event.rs): the events, and what the window
|
|
manager does with each.
|
|
- [`window_manager_event_listener`](../../../crates/komorebi-linux/src/window_manager_event_listener.rs): the
|
|
channel that carries events from the backend to the window manager, and
|
|
[`listen_for_events`], the thread that takes them off it.
|
|
- [`monitor_reconciliator`](../../../crates/komorebi-linux/src/monitor_reconciliator.rs): the channel the KWin
|
|
backend sends [`MonitorNotification`]s on, [`listen_for_notifications`], the thread that
|
|
takes them, and [`reconcile_display_change`], which brings the window manager's monitors in
|
|
line with the outputs, and [`wake_up`], which puts the desktop in order after a wake;
|
|
[`reconcile`](../../../crates/komorebi-linux/src/reconcile.rs): moves what was on a monitor that went to the
|
|
monitors that are left, and every container whose window is on another monitor to that
|
|
monitor.
|
|
- [`kwin`](../../../crates/komorebi-linux/src/kwin/mod.rs): the KWin backend, its D-Bus service and the held
|
|
call; [`kwin::state`](../../../crates/komorebi-linux/src/kwin/state.rs), the table;
|
|
[`kwin::power`](../../../crates/komorebi-linux/src/kwin/power.rs), the watchers of logind's sleep signal and the
|
|
lock screen's;
|
|
[`kwin::kwinrc`](../../../crates/komorebi-linux/src/kwin/kwinrc.rs), the editor of the user's `kwinrc`, which
|
|
keeps the values it changes and puts them back; the parking desktop is made, swept and removed
|
|
in `kwin` and tracked in `kwin::state`; and
|
|
[`bridge.js`](../../../crates/komorebi-linux/src/kwin/bridge.js), the bridge.
|
|
- [`static_config`](../../../crates/komorebi-linux/src/static_config.rs): `komorebi.json`, [`StaticConfig`], and
|
|
how it is loaded, reloaded and generated.
|
|
- [`border_manager`](../../../crates/komorebi-linux/src/border_manager/mod.rs): the border settings, and the
|
|
[`Look`] they make, which it asks the backend for.
|
|
- [`animation`](../../../crates/komorebi-linux/src/animation/mod.rs): the animation settings, which the look has.
|
|
- [`theme_manager`](../../../crates/komorebi-linux/src/theme_manager.rs): a theme's colors for the borders, and the
|
|
palettes made from pictures.
|
|
- [`kwin::wallpaper`](../../../crates/komorebi-linux/src/kwin/wallpaper.rs): the Plasma shell's side of a
|
|
wallpaper: the screen of an output, what it shows, and the picture set.
|
|
- [`transparency_manager`](../../../crates/komorebi-linux/src/transparency_manager.rs): which windows are
|
|
see-through, and the opacity they are given.
|
|
- [`kwin::effects`](../../../crates/komorebi-linux/src/kwin/effects.rs): the KWin effect that draws the border,
|
|
its settings and the state komorebi keeps of it.
|
|
- [`check`](../../../crates/komorebi-linux/src/check.rs): the checks behind `komorebic check` (the configuration,
|
|
and the desktop it runs on), the [`Finding`]s they return and the [`Report`] `komorebic` gathers
|
|
them in.
|
|
- [`process_command`](../../../crates/komorebi-linux/src/process_command.rs): the thread that listens on the
|
|
socket, and what the window manager does with each message.
|
|
- [`state`](../../../crates/komorebi-linux/src/state.rs): the [`State`] a `State` message is answered with,
|
|
and whether an event or a message changed it.
|
|
- [`lib.rs`](../../../crates/komorebi-linux/src/lib.rs): the subscribers, the [`Notification`], and
|
|
[`notify_subscribers`].
|
|
- [`komorebi-client`](../../../crates/komorebi-client/src/lib.rs): a library that writes messages on
|
|
the socket and reads the answers, and subscribes to notifications, for programs that talk
|
|
to the daemon.
|
|
- [`komorebic`](../../../komorebic/src/linux/mod.rs): the command line. A subcommand that asks
|
|
something of the daemon is one message, sent with `komorebi-client`; `start` runs the
|
|
daemon, `log` follows its log file in [`LOG_DIR`], and the hidden `docgen` writes
|
|
`docs/linux/cli`.
|
|
- [`daemon.rs`](../../../crates/komorebi-linux/src/daemon.rs): the daemon. It connects to KWin, or installs the
|
|
fake with `--fake-backend`, makes the window manager from `komorebi.json` when there is one,
|
|
which binds the socket, loads the desktop, gives the monitors their configuration, asks for the
|
|
borders and tiles, starts the threads that listen on the socket, handle events and take the
|
|
notifications of the monitors, and waits to be stopped.
|
|
- [`nixos-module.nix`]: the NixOS module, which installs komorebi and runs the daemon as a
|
|
systemd user unit of every Plasma session.
|
|
|
|
The handle is a process-wide singleton so that a window can be a small `Copy` value
|
|
that carries only its id, and ask the backend for what it needs. A test replaces it for
|
|
its own thread with [`with_backend`], which puts the previous one back when the test is
|
|
done, also if it panics.
|
|
|
|
## Traits and roles
|
|
|
|
**[`Backend`] is the compositor.** It is implemented once for each compositor and once for
|
|
the fake. The window manager is its only caller, through [`backend`]. The contract:
|
|
|
|
- Queries (`windows`, `window`, `foreground_window`, `cursor_pos`) answer from the
|
|
backend's own table and never wait for the compositor, except the KWin backend's
|
|
`cursor_pos`, which waits up to 250 ms for the bridge. `monitors` may fail.
|
|
- A command returns once the compositor has accepted it, not once the client has acted on
|
|
it. The frame a window ends up with is what `window_rect` says afterwards, because a
|
|
client may refuse a size.
|
|
- A command about a window that does not exist, or is no longer alive, is an error. It
|
|
never panics. The KWin backend knows a closed window for a minute after it closed, and
|
|
sends its commands to the bridge, which reports that it could not carry them out.
|
|
- Workspace `i` is the compositor's desktop `i`, and each monitor shows one
|
|
(`switch_workspace`, `set_window_workspace`, `window_workspace`, `current_workspaces`). A
|
|
backend asked for a workspace that has no desktop makes the desktops up to it
|
|
(`ensure_workspaces`). The compositor shows and hides the windows of the desktops, and a
|
|
backend reports a switch or a move it did not make as an event.
|
|
- A hidden window (`hide`) is out of sight, alive, not minimized, and on the workspace it was
|
|
on until `restore`: `window_workspace` still names that workspace, and
|
|
`set_window_workspace` changes where it comes back to, not where it is hidden. Only the back of
|
|
a stack and the containers behind a monocle are hidden: nothing is hidden to leave a
|
|
workspace.
|
|
- The look is a request (`apply_look`): a backend asks the compositor to draw it and answers
|
|
whether borders are being drawn, and one that draws nothing answers false, which is the default.
|
|
It does nothing when it is asked for the look it applied last.
|
|
- `shutdown`, `is_window`, `window_rect` and `monitor_from_point` have defaults, written in
|
|
terms of the queries. An implementor should not need to override them, except `shutdown`
|
|
when it created something in the compositor that has to be given up. `drag_origin` and
|
|
`drag_end` have defaults that keep nothing; a backend whose frames change during a drag and
|
|
after it, as KWin's do, overrides them.
|
|
|
|
## What is guaranteed, and by what
|
|
|
|
| Rule | Enforced by | Proof | Escape hatch |
|
|
|---|---|---|---|
|
|
| A backend implements every query and command but `maximize` | type system: the methods have no defaults, except `maximize`, which does nothing until a backend implements it | `compile_fail,E0046` on [`Backend`] | none |
|
|
| A backend can be shared between threads | type system: `Backend: Send + Sync + 'static` | `compile_fail,E0277` on [`Backend`] | none |
|
|
| The window manager asks for a backend that was never installed | runtime error: [`backend`] panics, naming [`set_backend`] and [`with_backend`] | `# Panics` on [`backend`] | none |
|
|
| A test's backend does not outlive the test | framework path: [`with_backend`] restores the previous one on drop | `with_backend_puts_the_previous_backend_back_when_the_closure_panics` | none |
|
|
| A command about a missing window is an error, not a panic | test | `a_command_about_a_window_nobody_opened_is_an_error`, `a_command_about_a_closed_window_is_an_error` and `hiding_or_restoring_a_window_nobody_opened_is_an_error` against the fake, `a_command_about_a_window_the_table_does_not_know_is_an_error_and_sends_nothing` against the KWin backend | none |
|
|
| A locked container keeps its index | framework path: a [`Workspace`] inserts, removes and swaps its containers through `LockableSequence` | `test_locked_containers_with_new_window`, `test_locked_containers_remove_window`, `a_window_that_opens_does_not_push_a_locked_container_from_its_index` | `containers_mut()` is the `VecDeque` itself |
|
|
| A container in monocle goes back to the index it came from, and the tiles come back as they were resized | framework path: [`Workspace::new_monocle_container`] keeps the index and [`Workspace::reintegrate_monocle_container`] inserts at it, and [`Workspace::update`] does not shorten the resize adjustments while there is a monocle container | `test_toggle_monocle`, `a_monocle_goes_back_to_its_index_even_when_a_window_opens_meanwhile`, `a_tile_that_was_resized_is_the_size_it_was_after_a_monocle` | none |
|
|
| The window in monocle keeps the focus | framework path: `handle_event` gives the focus back to the monocle window on a `FocusChange`, and [`WindowManager::update_focused_workspace`] focuses it | `a_window_that_takes_the_focus_from_the_monocle_is_made_to_give_it_back` | none |
|
|
| What is about the tiles does nothing while a window is in monocle | framework path: [`WindowManager::move_container_in_direction`] and [`WindowManager::move_container_in_cycle_direction`] bail, and [`WindowManager::toggle_float`] returns, when the window in front is in monocle | `what_is_about_the_tiles_is_refused_or_ignored_while_a_monocle_is_on` | none |
|
|
| A focus that crosses to a monitor in monocle gives its monocle window the focus, and a container moved or sent to a workspace in monocle turns the monocle off | framework path: the `None` arm of [`WindowManager::focus_container_in_direction`], [`WindowManager::move_container_in_direction`], [`WindowManager::move_container_to_monitor`] and [`Monitor::move_container_to_workspace`] | `focusing_towards_a_monitor_in_monocle_gives_the_focus_to_its_monocle_window`, `a_container_moved_to_a_monitor_in_monocle_turns_the_monocle_off_there`, `a_container_sent_to_a_workspace_in_monocle_turns_the_monocle_off_there` | none |
|
|
| A window in monocle that closes takes the monocle with it | framework path: [`Workspace::remove_window`] looks in the monocle container, which the index of managed windows lists | `closing_the_window_in_monocle_tiles_the_others_again` | none |
|
|
| In a real KWin, the window in monocle fills the work area and goes back to its tile | VM test | `vm-core-e2e` ("in monocle the focused window fills the work area", "toggling back puts it on its tile") | none |
|
|
| A clean stop, and the next start after a crash, bring back the windows a monocle hid | VM test | `vm-core-e2e` ("and every window is on a workspace's desktop", "after a crash and a restart a monocle still hides the other window") | none |
|
|
| A maximized window goes back in a container of its own at the index it came from, and the tiles are where they were | framework path: [`Workspace::new_maximized_window`] keeps the index and [`Workspace::reintegrate_maximized_window`] inserts at it, after asking the backend to restore the window, and nothing else moves in between | `toggle_maximize_takes_the_window_out_of_the_layout_and_back`, `maximizing_works_in_every_layout`, `a_window_goes_back_in_a_container_of_its_own_at_the_index_it_came_from` | none |
|
|
| What is about the tiles does nothing while a window is maximized | framework path: `is_layout_command` in [`WindowManager::process_command`] drops the message before it is handled | `while_a_window_is_maximized_what_is_about_the_tiles_does_nothing` | none |
|
|
| A window the user maximizes leaves the layout as `toggle-maximize` makes a window leave it, and comes back to its tile when the user restores it | framework path: `handle_event` hands a `MaximizeChange` to [`WindowManager::adopt_maximize_change`], which uses [`Workspace::adopt_maximized_window`] and [`Workspace::reintegrate_maximized_window`] | `a_window_the_user_maximizes_leaves_the_layout_like_toggle_maximize_makes_it`, `the_user_restoring_the_window_with_its_title_bar_puts_it_back_in_its_tile`, `a_floating_window_the_user_maximizes_leaves_the_floating_windows_and_comes_back_as_a_tile`, `the_window_in_the_monocle_that_the_user_maximizes_leaves_the_monocle`, `a_second_window_the_user_maximizes_puts_the_first_back_in_its_tile` | none |
|
|
| What komorebi asked for, reported back by the compositor, changes nothing | framework path: [`WindowManager::adopt_maximize_change`] returns when the window already is, or is not, the workspace's maximized window | `komorebis_own_maximize_echoing_back_changes_nothing`, `toggle_maximize_restores_a_window_the_user_maximized`, `restoring_a_window_that_is_not_maximized_changes_nothing` | none |
|
|
| A window that is maximized when komorebi starts or when it opens is the maximized window, and is not asked to leave that state | framework path: [`load_workspace_information`] and the `Show` arm of `handle_event` read [`WindowInfo::maximized`] | `a_window_that_is_maximized_when_komorebi_starts_is_the_maximized_one`, `a_window_that_opens_maximized_is_the_maximized_one` | none |
|
|
| A change to a window komorebi does not have is ignored | framework path: [`WindowManager::adopt_maximize_change`] looks the window up in the index of managed windows first | `a_change_for_a_window_komorebi_has_not_taken_in_is_ignored` | none |
|
|
| The bridge reports a window as maximized only when KWin has it maximized both ways | test: `isMaximized` in [`bridge.js`] reads KWin's `maximizeMode`, which is 3 for both ways | `bridge.test.js` ("a window maximized one way only is not maximized", "a window the user maximizes or restores is reported, and one maximized one way is not") | none |
|
|
| In a real KWin, a window the user maximizes is komorebi's maximized window, the layout does nothing meanwhile, and the window goes back to its tile | VM test | `vm-user-maximize` | none |
|
|
| A fullscreen window is never moved | framework path: [`Window::set_position`] returns before the backend when [`WindowInfo::fullscreen`] is set, and every tile, centering and float of the window manager goes through it | `a_fullscreen_window_is_left_covering_the_monitor_and_goes_back_to_its_tile_afterwards`, `a_fullscreen_window_is_not_moved_and_is_again_when_it_is_not_fullscreen` | none |
|
|
| A window that leaves fullscreen is put in the tile the layout has for it | framework path: `handle_event` tiles the workspace in front again on a `FullscreenChange` back from fullscreen | `a_fullscreen_window_is_left_covering_the_monitor_and_goes_back_to_its_tile_afterwards` | none |
|
|
| The bridge reports a fullscreen change with what the window is now | test: the bridge reads `fullScreen` when KWin raises `fullScreenChanged`, and the table keeps it | `bridge.test.js` ("a window that goes fullscreen or comes back from it is reported as it is now", and the session), `going_fullscreen_and_coming_back_are_events_and_are_remembered` | none |
|
|
| In a real KWin, a fullscreen window covers the monitor, a window that opens does not take it out of fullscreen, and it goes back to a tile that overlaps no other | VM test | `vm-features` (section E) | none |
|
|
| Without placement rules a window goes after the focused container | framework path: `resolve_placement_index` returns that index when the workspace has no rules, or none matches | `without_rules_a_window_goes_after_the_focused_one` | none |
|
|
| A place that does not exist is the default place, never an error | framework path: `resolve_placement_index` falls back to the default for a number past the end and for the secondary tile of one container | `a_place_that_is_not_there_is_the_default` | none |
|
|
| The first rule in key order decides | framework path: the rules are a `BTreeMap` ordered by the text of the place | `the_first_rule_in_key_order_wins`, `a_place_for_the_windows_that_match_a_rule` | none |
|
|
| A rule of several conditions needs all of them | framework path: a composite [`MatchingRule`] is read by [`should_act`], which needs every part | `a_rule_of_several_conditions_needs_them_all` | none |
|
|
| A setting that is not a place is refused | type system: `PlacementTarget` reads a strategy name or a number, and nothing else; checked when the setting is read | `a_place_that_is_not_one_is_refused` | none |
|
|
| A reserved place wins over the rules | framework path: [`Workspace::new_container_for_window`] checks `preselected_container_idx` before it asks for a place | `a_place_that_was_preselected_wins_over_the_rules` | none |
|
|
| A locked container keeps its index when a rule asks for it | framework path: the container is inserted through `LockableSequence` | `a_rule_that_asks_for_the_place_of_a_locked_container_gets_the_place_after_it` | none |
|
|
| In a real KWin, windows go where the rules say, by program, app id and title | VM test | `vm-placement` | none |
|
|
| A window stays maximized through a retile | framework path: [`Workspace::update`] asks the backend to maximize it again, and moves no container | `a_retile_while_a_window_is_maximized_asks_the_compositor_to_maximize_it_again` | none |
|
|
| A maximized window that closes takes the maximize with it | framework path: [`Workspace::remove_window`] looks at the maximized window first, which the index of managed windows lists | `a_maximized_window_that_closes_leaves_the_layout_whole` | none |
|
|
| In a real KWin, the maximized window fills the work area, is on top, and goes back to its tile | VM test | `vm-features` ("the focused window fills the work area", "and is the one on top", "toggled back, it is in its tile again") | none |
|
|
| The focus, move and resize messages act on the windows of the layer a workspace is on | framework path: [`WindowManager::process_command`] matches on [`Workspace::layer`] for `FocusWindow`, `CycleFocusWindow` and `MoveWindow`, [`WindowManager::resize_window`] for the resize messages, and `PreselectDirection` checks it | `focusing_in_a_direction_on_the_floating_layer_goes_to_the_nearest_floating_window`, `moving_on_the_floating_layer_moves_the_floating_window_by_the_resize_delta`, `resizing_on_the_floating_layer_changes_the_frame_of_the_floating_window`, `a_direction_to_preselect_is_ignored_on_the_floating_layer`, `focusing_a_floating_window_towards_a_monitor_crosses_to_it_and_to_its_monocle_window` | none |
|
|
| The layer follows the window that has the focus | framework path: `handle_event` sets [`Workspace::layer`] on a `FocusChange`, a `Show` and [`WindowManager::toggle_float`], and [`Monitor::move_container_to_workspace`] on the workspace a window is moved to | `floating_a_window_puts_the_workspace_on_the_floating_layer_and_focusing_a_tile_takes_it_back`, `a_window_sent_to_another_workspace_puts_that_workspace_on_the_layer_it_arrived_on` | none |
|
|
| A window that opens on the floating layer floats only when its floating layer behaviour says so | framework path: [`WindowManager::window_management_behaviour`] takes the workspace's behaviour, else its monitor's, else its own, and `handle_event` floats the window on `floating_layer_override` | `the_floating_layer_behaviour_of_the_workspace_wins_over_the_monitors_and_the_window_managers`, `a_window_that_opens_on_the_floating_layer_floats_when_the_behaviour_says_so` | none |
|
|
| The windows of the layer a workspace is not on are out of sight | framework path: `ToggleWorkspaceLayer` hides the floating windows when it goes to the tiling layer and restores them when it goes to the floating one | `going_back_to_the_tiles_hides_the_floating_windows_and_going_to_them_shows_them_again` | none |
|
|
| Cycling the focus on the floating layer visits every floating window | test | `cycling_the_focus_on_the_floating_layer_goes_round_the_floating_windows_in_order` | none |
|
|
| A floating window is never moved out of the work area by `move` | framework path: [`WindowManager::move_floating_window_in_direction`] keeps the frame inside the focused monitor's work area | `moving_on_the_floating_layer_moves_the_floating_window_by_the_resize_delta` | none |
|
|
| A window komorebi does not manage is never moved | framework path: `handle_event` asks [`Window::should_manage`] before it acts on any event but a destroy | `dialogs_and_other_non_normal_windows_are_left_alone`, `an_ignore_rule_keeps_a_window_unmanaged` | `ManageRule` |
|
|
| A window the user unmanaged stays out of the layout until it is managed again | framework path: `handle_event` checks the list of unmanaged windows before it acts on any event but a removal | `unmanage_takes_the_focused_window_out_until_manage_takes_it_back` | none |
|
|
| A paused window manager moves no window | framework path: `handle_event` returns before anything else, and [`read_commands_uds`] drops every message but `TogglePause`, `State`, `GlobalState` and `Stop` | `while_paused_no_event_changes_the_layout_and_manage_takes_in_a_window_it_missed`, `while_paused_only_toggle_pause_state_and_stop_are_handled` | none |
|
|
| A destroyed window leaves the workspace that has it | framework path: the destroy is looked up in the index of managed windows, which is rebuilt at startup and before and after every event | `a_window_that_was_open_before_komorebi_started_is_removed_when_it_closes` | none |
|
|
| The daemon does not start without KWin, unless it is told to run against the fake | startup check: [`connect`] fails, and the daemon exits with the error, code 1 | `without_a_session_bus_komorebi_exits_with_an_error_naming_it` | `--fake-backend` |
|
|
| One komorebi per session | startup check: the session bus gives `org.komorebi.Bridge` to one connection | none | none |
|
|
| A komorebi that cannot connect to KWin leaves the socket of the running one alone | startup order: `main.rs` connects to KWin before [`WindowManager::new`] binds the socket | `without_kwin_komorebi_leaves_the_socket_it_finds_alone` | `--fake-backend` asks no bus, and takes the socket |
|
|
| Events reach the window manager in the order KWin raised them | framework path: the `Event` interface is `spawn = false`, so the calls are handled inline, and one channel and one thread carry the events to the window manager | `vm-kwin-events` ("the Show came first") | none |
|
|
| No event names a window before the event that announces it | convention in the bridge, which announces a window it has not seen when it is activated; the table shows a window once | `a_new_window_raises_show_and_then_focus_in_that_order`, `announcing_the_same_window_twice_shows_it_once`, `vm-kwin-events` | none |
|
|
| A window's id names one window while the daemon runs | framework path: the table numbers each KWin window the first time it sees it, and never gives a number out twice | `ids_are_assigned_once_per_window_and_survive_a_new_snapshot`, `tombstones_are_dropped_after_a_while_and_their_ids_are_not_reused` | none |
|
|
| A backend never waits for the window manager | framework path: an event that does not fit in the channel (20 waiting) is dropped and logged | none | none |
|
|
| Every window is where komorebi asked for it, within the size its client keeps to | VM test | `vm-core-e2e`, `vm-client-geometry` | a client may refuse a size: the frame is what `window_rect` says |
|
|
| A retile reaches KWin as one batch | framework path: the answer to `Poll` waits a millisecond after the first command for the others | `vm-latency` | none |
|
|
| The window manager sees a frame it asked for at once | framework path: the KWin backend's `set_rect` records it in the table before KWin answers | `a_requested_rect_is_visible_at_once_and_replaced_by_the_real_one` | none |
|
|
| A KWin that restarts gets the bridge again, and the windows of the old one leave the layout | framework path: the connection's thread watches `NameOwnerChanged` for `org.kde.KWin`, and the table destroys every window when the name is lost | `when_kwin_goes_away_every_window_it_had_is_destroyed`, `vm-kwin-restart` | none |
|
|
| After KWin restarts, the commands go to the new bridge | framework path: a `Poll` held since before KWin last left the bus is answered with nothing, and passes on the wake-up it got | `a_poll_held_by_the_bridge_of_a_kwin_that_has_gone_gets_no_commands`, `vm-kwin-restart` ("a window opened in the third KWin is tiled") | none |
|
|
| Messages are handled one at a time, and in order on one connection | framework path: [`read_commands_uds`] reads a connection line by line and handles each message with the window manager locked | `the_messages_on_one_connection_are_handled_in_order` | none |
|
|
| A socket left behind by a daemon that did not stop cleanly does not keep the next one from starting | framework path: [`WindowManager::new`] removes the file before it binds | `a_socket_file_left_behind_is_replaced` | none |
|
|
| A daemon that is told to stop leaves no socket behind | framework path: [`WindowManager::stop`] removes it before it exits | `the_daemon_answers_on_its_socket_and_stops_when_it_is_told_to` | `SIGKILL` |
|
|
| A container moved onto a locked index is locked in its place, and the lock leaves the container that moved away | framework path: [`Workspace::swap_containers`] swaps through `LockableSequence`, which keeps the lock with the index | `test_swap_respecting_locks_various_cases`, `a_lock_stays_with_its_index_when_a_window_is_moved_onto_it` | `containers_mut()` is the `VecDeque` itself |
|
|
| A window komorebi puts off the screen stays there | framework path: the bridge asks again for the position of a window KWin pulled back after a `geo` command, for a second, three times at most | `a window KWin pulls back from where it was asked to be is asked for the position again`, `a window that cannot go where it was asked is asked again three times at most`, `vm-scrolling` | a window that cannot go where it was asked is left where KWin put it |
|
|
| A window dragged onto the tile of another swaps places with it, and a window let go of elsewhere goes back to its tile | framework path: `handle_event`'s `MoveEnd` swaps the focused container with the one under the pointer through [`Workspace::swap_containers`], and tiles the workspace again | `dragging_a_window_onto_another_swaps_them`, `a_window_let_go_of_where_there_is_no_tile_goes_back_to_its_own` | a frame that is not as wide and as tall as its tile is not taken for a move, and stays where it was let go |
|
|
| A floating window dragged onto a tile takes nothing from it | framework path: `MoveEnd` acts only on a window that is in a container ([`Workspace::contains_managed_window`]) | `a_floating_window_dragged_onto_a_tile_takes_nothing_from_it` | none |
|
|
| A window let go of on another monitor goes there, in the place of the container under the pointer | framework path: `MoveEnd` sees that the monitor under the pointer is not the one the drag began on, and hands the window to [`WindowManager::transfer_window`] without comparing its frame with its tile | `a_window_let_go_of_on_another_monitor_goes_to_that_monitor`, `a_window_let_go_of_on_a_monitor_with_nothing_on_it_goes_there`, `a_floating_window_let_go_of_on_another_monitor_is_one_of_the_floating_windows_there` | a floating window that is the only window of its workspace is not noticed to have moved |
|
|
| With the window container behavior `Append` a window let go of on a tile joins its container, and with the float override on it floats where it was let go of | framework path: `MoveEnd` reads [`WindowManager::window_management_behaviour`] for the focused workspace | `with_append_a_window_let_go_of_on_a_tile_joins_the_container_of_that_tile`, `with_the_float_override_a_window_let_go_of_on_a_tile_floats_where_it_was_let_go_of` | none |
|
|
| The bridge leaves a window the user has taken hold of where the user puts it | framework path: the bridge forgets the position it was asking again for when a drag of the window starts | `a window the user has taken hold of is not asked again for the position komorebi gave it` | none |
|
|
| A drag of an edge resizes the tile of the window that was dragged, by what the pointer moved the edge, and moves no other edge | framework path: `ResizeEnd` compares each edge with the frame the drag began with ([`Table::drag_origin`]) and resizes the dragged window's container | `resizing_a_tile_by_hand_moves_the_split`, `a_drag_is_measured_from_where_the_window_was_not_from_its_tile`, `the_window_that_was_dragged_is_the_one_resized_not_the_focused_one`, `a_corner_resizes_both_of_its_edges`, `vm-drag-resize` | none |
|
|
| The end of a resize is where the user took the edge, not the frame KWin has then, or reports a moment later | framework path: the bridge sends the frame of the last step as `requested`, the table keeps it for a resize apart from the window's frame ([`Table::drag_end`]), which the frame events that follow the end do not reach, and `ResizeEnd` reads it ([`Backend::drag_end`]) | `the_end_of_a_resize_is_where_the_user_took_it_not_where_the_frame_had_got_to`, `a_frame_that_follows_the_end_of_a_resize_does_not_change_where_the_user_took_it`, `the_end_of_a_resize_is_where_the_user_took_the_edge_when_the_frame_has_not_got_there`, `a drag is reported when the user takes hold of a window and when they let go`, `vm-drag-resize` | a drag with no step ends at the frame |
|
|
| The window whose edge is being dragged is not moved until it is let go | framework path: [`Window::set_position`] returns without a command for a window in the list [`live_resize_begins`] adds it to, whatever tiles the workspace | `the_neighbours_follow_a_drag_while_it_is_going_on_and_the_dragged_window_is_left_alone`, `a_window_opening_part_way_through_a_drag_does_not_break_it` | none |
|
|
| A drag ends the same with its steps as without them, and one taken back puts the neighbors back | framework path: every step and the end start from the layout kept at the start ([`LiveResize`]) | `a_drag_has_the_same_result_with_the_steps_as_without_them`, `a_drag_that_is_taken_back_puts_the_neighbours_where_they_were`, `vm-live-resize` | none |
|
|
| A step of a drag never waits behind another step | framework path: the bridge sends one every 16 ms at most, and [`send_notification`] keeps at most one waiting | `the steps of a resize are sent at most every 16 ms, and the last of a burst is not lost`, `a step waiting when the user lets go is not sent after the end` | none |
|
|
| A dragged edge with no neighbor puts the window back on its tile | framework path: `ResizeEnd` tiles the workspace again when the frame changed | `dragging_an_edge_that_has_no_neighbour_puts_the_window_back`, `vm-drag-resize` | none |
|
|
| Only a tile is resized by dragging: the window of a stack resizes the tile of the stack, and a floating window or the window of a monocle is left as the user made it | framework path: `ResizeEnd` looks for the container of the dragged window among the workspace's containers ([`Workspace::container_idx_for_window`]), which leaves out the floating windows and the monocle container, and does nothing when there is none | `the_edge_of_a_stack_is_the_edge_of_its_tile`, `a_floating_window_dragged_by_its_edge_is_left_as_the_user_made_it`, `the_window_of_a_monocle_dragged_by_its_edge_is_left_as_the_user_made_it` | none |
|
|
| A resize leaves no tile smaller than a tenth of the work area, or than it was | framework path: [`WindowManager::resize_window`] calculates the tiles for the adjustment, and cuts the adjustment down until they all fit | `a_resize_leaves_every_tile_about_a_tenth_of_the_work_area_wide_at_least`, `a_drag_further_than_the_neighbour_can_give_goes_as_far_as_it_can`, `shrinking_a_tile_by_more_than_it_can_is_granted_as_far_as_it_goes` | the paddings are taken off afterwards, so a tile can end up a little smaller |
|
|
| The offset of a workspace wins over its monitor's, which wins over the window manager's | framework path: [`Monitor::update_workspace_globals`] gives the workspace the monitor's offset or else the window manager's, and [`Workspace::update`] takes its own before that | `the_workspaces_offset_wins_over_the_monitors_which_wins_over_the_global_one` | none |
|
|
| A workspace whose tiling is off moves no window | framework path: [`Workspace::update`] returns before the layout when [`Workspace::tile`] is false | `with_tiling_off_no_window_is_moved_until_it_is_back_on` | none |
|
|
| komorebi hides no window to leave a workspace | framework path: showing a workspace is one request to the compositor ([`Backend::switch_workspace`]), which shows and hides the windows of its desktops | `switching_workspace_asks_the_compositor_to_show_the_other_desktop`, `vm-workspace-switch` ("KWin shows the desktop, komorebi hides nothing") | none |
|
|
| Showing a workspace on one monitor leaves the other monitors as they are | KWin's per-output virtual desktops, which [`connect`] turns on | `showing_a_workspace_switches_that_output_only`, `vm-kwin-contract` ("the other keeps its own") | a `kwinrc` komorebi cannot write: every monitor switches |
|
|
| A window is on its new workspace's desktop before the desktop is shown or the window focused | framework path: [`Monitor::load_focused_workspace`] puts every window on its desktop first | `moving_a_window_to_another_workspace_puts_it_on_the_desktop_before_showing_it` | none |
|
|
| A workspace switch or a window move made by something else is followed, and answered with no command | framework path: the table reports what it did not send as a `WorkspaceChange` or a `WorkspaceMove`, and [`WindowManager::adopt_workspace_change`] asks the compositor for nothing | `a_workspace_switch_by_someone_else_is_followed_not_undone`, `a_window_moved_to_another_desktop_by_someone_else_changes_workspace`, `another_party_switching_a_monitor_is_a_workspace_change`, `vm-workspace-switch` ("nothing is flip-flopping") | none |
|
|
| komorebi's own switch or move, reported back by KWin, changes nothing | framework path: the table records a switch and a move when it makes their commands | `the_compositor_reporting_komorebis_own_switch_changes_nothing`, `komorebis_own_move_coming_back_as_an_event_moves_nothing`, `komorebi_switching_a_monitor_comes_back_as_a_change_to_what_it_already_knows` | none |
|
|
| The user's `kwinrc` gets back the value komorebi changed, also after a crash | framework path: [`Kwinrc`] records the first value of a key in a backup file, and the backend's `shutdown` puts it back | `what_komorebi_changed_is_put_back_exactly_and_what_it_added_is_removed`, `a_backup_left_by_a_crash_keeps_the_values_from_before_the_first_run`, `vm-workspace-switch` ("back as they were") | `SIGKILL`: the next run puts it back when it stops |
|
|
| A workspace is closed only when it has no window and no name and is not the monitor's last | framework path: the `CloseWorkspace` arm of [`WindowManager::process_command`] checks all three | `new_workspace_adds_one_and_close_workspace_removes_it_when_it_is_empty_and_unnamed` | none |
|
|
| The focus and a move at the edge of a monitor go to the monitor on that side, and fail only when there is none | framework path: the `None` arms of [`WindowManager::focus_container_in_direction`] and [`WindowManager::move_container_in_direction`] | `the_focus_crosses_to_the_monitor_on_that_side`, `a_move_at_the_edge_of_a_monitor_swaps_with_the_window_on_the_next_one_by_default`, `focus_towards_a_side_with_no_window_is_an_error_and_changes_nothing` | `NoOp` keeps a move on its monitor |
|
|
| A window shown or focused on another monitor focuses that monitor, and nothing else does | framework path: `handle_event` focuses the monitor of a `Show` or `FocusChange` before it acts on it | `a_window_that_opens_on_the_monitor_that_is_not_focused_focuses_it`, `a_scrolling_strip_on_the_next_monitor_over_does_not_take_focus_with_it` | none |
|
|
| Only the window in front of a stack is in sight | framework path: [`Container::add_window`] and [`Container::load_focused_window`] hide every window of the container but the focused one, and every operation that changes the front ends with the call | `a_window_added_to_a_container_is_in_front_and_the_others_are_hidden`, `loading_the_focused_window_shows_it_and_hides_the_rest`, `stacking_puts_two_windows_in_one_container_and_hides_the_one_behind`, `vm-stacks` ("exactly one window is on it") | none |
|
|
| A hidden window is still on its workspace, and is not reported as moved | framework path: the table keeps the desktop a window came from, and a move to the parking desktop is its own | `parking_a_window_hides_it_without_unmanaging_it`, `komorebis_own_move_and_its_park_and_restore_raise_nothing_new`, `a_window_is_put_on_a_workspace_by_putting_it_on_its_desktop` | none |
|
|
| What the compositor reports about a window komorebi hid is not taken for the user's doing | framework path: [`Window::hide`] records the window in [`HIDDEN_WINDOWS`] before it asks the backend, and the `Minimize` arm of `handle_event` checks it | `a_minimize_komorebi_asked_for_is_not_mistaken_for_the_user`, `a_minimize_the_user_asked_for_takes_the_window_out_of_the_layout` | none |
|
|
| While a window is in monocle the windows of the other containers are hidden, and they are shown again when it is turned off | framework path: [`WindowManager::monocle_on`], [`WindowManager::cycle_monocle`] and [`WindowManager::monocle_off`] hide and restore the containers, and [`Workspace::remove_window`] restores them when the window in monocle closes | `a_monocle_hides_the_other_windows_until_it_is_turned_off`, `a_window_in_monocle_that_leaves_shows_the_containers_behind_it`, `the_back_of_a_stack_stays_hidden_in_monocle_and_after_it`, `vm-stacks` ("the window in monocle fills the work area and the other one is out of sight") | none |
|
|
| The window the compositor focuses in a stack comes to the front | framework path: the `FocusChange` arm of `handle_event` focuses the container by the window, which loads it ([`Workspace::focus_container_by_window`]) | `focusing_a_hidden_window_of_a_stack_brings_it_to_the_front`, `the_window_the_user_focuses_in_the_back_of_a_stack_comes_to_the_front` | none |
|
|
| Cycling a stack changes which window is in front and nothing else | framework path: [`Container::load_focused_window`] shows the focused window and hides the rest; a cycle by index only swaps two places | `cycling_the_stack_brings_the_next_window_to_the_front_and_hides_the_one_that_was`, `cycling_the_index_moves_the_front_window_along_the_stack_and_keeps_it_in_front`, `focus_stack_window_brings_the_window_at_an_index_to_the_front`, `vm-stacks` ("cycle the stack") | none |
|
|
| A stack with one window cannot be cycled, and an index it lacks is refused | runtime error: the window manager's functions return it before they change anything | `cycling_a_window_that_is_alone_is_an_error`, `focus_stack_window_refuses_an_index_the_stack_does_not_have` | none |
|
|
| Closing the window in front of a stack brings the next one forward | framework path: [`Workspace::remove_window`] shows the container's new front window and focuses it | `closing_the_window_in_front_of_a_stack_brings_the_next_one_forward`, `vm-stacks` ("closing the window in view") | none |
|
|
| The back of a stack stays hidden when its workspace is shown again | framework path: [`Workspace::restore`] restores the front window of each container and no other | `the_back_of_a_stack_stays_hidden_across_a_workspace_round_trip`, `vm-stacks` ("a workspace round trip") | none |
|
|
| A window is never sent to a parking desktop that is not there | framework path: [`Table::hide_commands`] asks for a new one unless KWin lists the one it made | `a_parking_desktop_that_has_gone_is_made_again_not_sent_windows` | none |
|
|
| A monitor raised onto the parking desktop is sent back | framework path: the table sends the monitor back to the desktop it showed before | `following_a_hidden_window_to_the_parking_desktop_sends_that_output_back` | none |
|
|
| A clean stop leaves no window hidden and no parking desktop | framework path: [`WindowManager::stop`] and the signal handler restore every hidden window, and the backend's `shutdown` removes the desktop after the restoring commands have gone out | `restoring_every_window_brings_back_the_hidden_ones_unless_it_is_told_to_ignore_that`, `vm-stacks` ("a clean stop puts every window back") | `SIGKILL`, and `StopIgnoreRestore`, which restores nothing |
|
|
| A parking desktop a dead daemon left is swept by the next one, and its windows go home | startup check: the first snapshot marks it stale, sends its windows to the first workspace's desktop, and the backend removes it | `a_parking_desktop_left_by_a_crashed_run_is_swept`, `a_stale_parking_desktop_is_not_the_one_windows_are_hidden_on`, `vm-stacks` ("a crash leaves a parking desktop") | none |
|
|
| A monitor that changes size has the windows of the workspace in front tiled in the size it has now | framework path: the table reports a changed size or work area as a `Resize`, and the reconciliator reads the monitors again and tiles the focused monitor | `plugging_in_a_monitor_is_a_connection_change_and_a_new_panel_is_a_resize`, `a_change_of_resolution_is_tiled_again`, `vm-hotplug` ("tiled again inside the monitor as it is now") | `Retile` tiles every monitor |
|
|
| The outputs are sent when they are not what the bridge last sent, and the room a panel leaves is found within a second | convention in the bridge: it sends after `screensChanged`, after a dock window opens, closes or moves, and in its watchdog, which runs every second | `a change of the outputs is sent as the screens, once, and the panel is looked at again`, `a panel that opens, moves or closes has the area of the outputs looked at again`, `the outputs are looked at once a second, which finds a panel that starts after the bridge` | none |
|
|
| A window moved to another output by something other than komorebi is tiled on that monitor | framework path: the bridge reports an output change, the table makes it an `OutputChange` event, and [`WindowManager::follow_windows_to_their_monitors`] moves the containers whose windows are on another monitor | `a_window_sent_to_another_screen_by_a_shortcut_is_tiled_there`, `a_window_on_another_output_raises_an_output_change_with_the_frame_it_has_there`, `a window that is on another output is reported with the frame it has and the output's name`, `vm-hotplug` ("komorebi follows it") | none |
|
|
| A move komorebi made itself is not undone when KWin reports it | framework path: [`WindowManager::follow_windows_to_their_monitors`] compares the frame of each window with the monitor the window manager has it on, and finds them equal | `vm-hotplug` ("still on the right monitor once KWin has said so") | none |
|
|
| Nothing is moved while the user drags a window | framework path: [`WindowManager::follow_windows_to_their_monitors`] returns at once while [`WindowManager::pending_move_op`] or [`WindowManager::pending_resize_op`] holds a drag | `nothing_is_followed_while_the_user_drags_a_window` | none |
|
|
| The windows of a monitor that is unplugged are tiled on the monitors that are left | framework path: [`reconcile_display_change`] has [`WindowManager::evacuate_monitor`] move each container and floating window to the monitor that holds it, before it removes the monitor | `the_windows_of_a_monitor_that_goes_are_tiled_on_the_one_that_is_left`, `vm-hotplug` ("tiled inside the screen that is left") | none |
|
|
| A stack moves with its monitor as a stack | framework path: [`WindowManager::evacuate_monitor`] moves whole containers | `a_stack_goes_with_the_monitor_as_a_stack` | none |
|
|
| A monocle of a monitor that goes comes back as a container, and a maximized window stays maximized | framework path: [`WindowManager::evacuate_monitor`] takes both out of the workspace; the monocle is put in a container on the monitor that is left, and the maximized window becomes the maximized window of its workspace there ([`Workspace::adopt_maximized_window`]), and the one that workspace had goes back to its tile | `a_monocle_of_a_monitor_that_goes_comes_back_as_a_container_with_the_windows_it_hid`, `a_maximized_window_of_a_monitor_that_goes_is_not_lost`, `a_maximized_window_of_a_monitor_that_goes_stays_maximized`, `a_maximized_window_that_comes_to_a_workspace_with_one_takes_its_place` | none |
|
|
| A monitor that is plugged in again has the settings it had, and none of the windows it had | framework path: [`reconcile_display_change`] keeps the monitor in a cache when it goes, and applies it, less the windows the window manager has elsewhere, when a monitor with its serial number id comes | `a_monitor_that_comes_back_gets_what_was_kept_of_it_and_none_of_the_windows_it_had` | none |
|
|
| A monitor komorebi has not seen gets workspaces of its own | framework path: [`load_monitor_information`] adds the monitors the window manager does not have | `a_monitor_that_was_never_there_gets_windows_of_its_own` | none |
|
|
| When KWin reports no output, the window manager keeps the monitors it has | framework path: [`reconcile_display_change`] returns before it removes any | `a_desktop_that_reports_no_monitor_is_left_as_it_was` | none |
|
|
| A monitor is only removed once its windows are on the monitors that are left | framework path: [`reconcile_display_change`] evacuates every monitor that went before it removes any | `the_windows_of_a_monitor_that_goes_are_tiled_on_the_one_that_is_left` | none |
|
|
| Waking up and unlocking each make the window manager look at the desktop again, once, after a delay, and going to sleep and locking do not | framework path: [`kwin::power`] sends a `Wake` after each of the two signals' `false`, and nothing for `true` | `waking_and_unlocking_make_the_window_manager_look_again_and_going_to_sleep_and_locking_do_not` (private buses) | none |
|
|
| Only logind is believed about the machine going to sleep | framework path: the match rule of the system bus names `org.freedesktop.login1` as the sender | the same test sends as another connection too | none |
|
|
| komorebi runs without a system bus or a lock screen | framework path: [`kwin::power`] logs that it is not watching, and a session with no lock screen sends no signal | none | none |
|
|
| On waking, the windows are where their tiles are, and a monitor that went while asleep is dealt with | framework path: [`wake_up`] reconciles the monitors, follows the windows and tiles every monitor | `windows_that_did_not_get_where_they_were_sent_while_the_screen_was_off_are_put_there_on_waking`, `a_monitor_unplugged_while_the_machine_slept_is_dealt_with_on_waking` | none |
|
|
| A paused window manager moves no window on waking | framework path: [`wake_up`] returns first when the window manager is paused | `a_paused_window_manager_is_left_alone_on_waking` | none |
|
|
| The lock screen has no tile | convention in the bridge: a window of the class `kwin_wayland` is not an application window | `dialogs, the lock screen and windows nobody can switch to are not application windows`, `vm-lock-sleep` ("the lock screen is not one") | none |
|
|
| komorebi's idea of the focus follows KWin's after the screen is unlocked | convention in the bridge: it announces KWin's active window when it is not the one it last announced, once a second | `the active window is announced when KWin has another than the one last announced, once`, `vm-lock-sleep` ("its idea of the focus is KWin's") | none |
|
|
| A subscriber that wants only changes of the state is sent every change of the monitors | framework path: [`notify_subscribers`] treats a monitor notification as an override of the filter | `a_change_of_the_monitors_is_sent_to_a_subscriber_that_filters_state_changes_too` | none |
|
|
| Moving the focus moves no window | test | `focus_gives_the_focus_to_the_window_on_that_side_and_moves_nothing`, `vm-core-e2e` ("moving the focus moved no window") | none |
|
|
| A subscriber that is gone is dropped, not retried | test | `a_subscriber_that_is_gone_is_dropped_and_not_tried_again` | none |
|
|
| The bridge holds no window manager logic | convention, with every decision about an event in the table, which the `kwin::state` unit tests pin | the unit tests in `crates/komorebi-linux/src/kwin/state.rs` | none |
|
|
| What the bridge sends is what the table reads | type checker: the JSDoc types of the messages in [`bridge.js`]; tests: the events of [`fixtures`] are what the bridge's tests check it sends and what the table's tests feed it | `what the bridge sends for a session is what the daemon's tests read`, `the_bridges_events_for_a_session_are_the_window_events_komorebi_expects` | none |
|
|
| What the backend sends is what the bridge carries out | tests: the commands of [`fixtures`] are what the backend's tests check it sends and what the bridge's tests answer it with, checking what KWin is asked to do for each | `the_commands_of_a_session_go_to_the_bridge_as_the_bridges_tests_answer_it`, `what the bridge sends for a session is what the daemon's tests read`, `a command for a window KWin no longer has is given back, and the rest are carried out` | none |
|
|
| Everything the bridge sends over D-Bus is one string | type checker: [`kwin-api.d.ts`] gives `callDBus` one string argument | `tsc`, `every call goes to the daemon's names, with one JSON string` | none |
|
|
| The bridge uses nothing a KWin script does not have | type checker: the library is ECMAScript 2016 alone, and KWin's API is what [`kwin-api.d.ts`] declares | `tsc` | a declaration KWin does not match: the VM tests run the bridge in KWin |
|
|
| A border takes room around the windows only while the compositor draws it | framework path: [`border_manager::send_notification`] keeps the backend's answer in [`BORDER_DRAWN`], which [`Monitor::update_workspace_globals`] reads | `a_border_the_compositor_draws_takes_room_around_each_window_and_none_is_taken_without_one`, `a_look_is_a_command_once_and_says_whether_borders_are_drawn` | none |
|
|
| The compositor is asked for the look after every event, every message and every retile, and only a changed look reaches it | framework path: [`WindowManager::process_event`], [`WindowManager::process_command`] and [`WindowManager::retile_all`] call [`border_manager::send_notification`], and a backend compares with the look it applied last | `the_border_messages_set_the_border_and_the_compositor_is_asked_for_it`, `a_look_is_a_command_once_and_says_whether_borders_are_drawn` | the same look applied twice by the KWin backend is not tested |
|
|
| No outline is drawn inside a window, and a border is never thicker than the effect can draw | framework path: [`kwin::effects`](../../../crates/komorebi-linux/src/kwin/effects.rs) sets every inner outline to 0 and clamps the width to 20 | `the_border_is_the_outer_outline_and_nothing_is_drawn_inside_the_window`, `a_border_thicker_than_the_effect_can_draw_is_the_thickest_it_can`, `vm-look` | none |
|
|
| Every window has the border color of what it is to the layout, and a window whose kind changes has the new one at once | framework path: [`border_manager::update_colours`] is called after every event and every message, from [`border_manager::window_kinds`], and the backend sends the colors it did not send last time | `the_focused_window_has_the_focus_colour_and_the_others_do_not`, `a_locked_container_that_is_not_focused_is_marked`, `a_stack_has_its_own_colour_when_it_has_the_focus`, `a_monocled_window_has_the_monocle_colour`, `a_floating_window_has_the_floating_colour_while_it_has_the_focus`, `border_colours_are_a_command_only_when_they_change`, `vm-features` (section C) | the stock effect: two colors for every window |
|
|
| With borders off no window has a color | framework path: [`border_manager::update_colours`] tells the backend an empty list when `BORDER_ENABLED` is false | `with_borders_off_no_window_has_a_colour` | none |
|
|
| The settings of one kind of animation win over the global ones, and a global setting clears them | framework path: the arms of [`WindowManager::process_command`] for `Animation`, `AnimationDuration` and `AnimationStyle`, and [`animation::movement_animation`] and [`animation::movement_style`], which read the kind's setting before the global one | `the_animation_messages_set_the_animation_and_the_compositor_is_asked_for_it` | none |
|
|
| A command that changes a stack is carried out with the animation off, and the animation is on again after it | framework path: the arms of [`WindowManager::process_command`] for the stack messages swap `ANIMATION_ENABLED_GLOBAL` for false and put the value back | `the_animation_is_on_again_after_a_command_that_changes_a_stack` | none |
|
|
| The `animation` key of the file is the animation settings, and a file without it leaves them | framework path: `StaticConfig::apply_globals` sets them only when the key is there, and `duration` and `style` only when the key has one | `the_animation_key_of_the_file_is_the_animation_settings` | none |
|
|
| Every style is a curve Qt has | test: [`QtCurve::qt_curve`] is a `match` of every style, and the test checks each against its name without the "Ease" | `every_style_is_a_curve_qt_has`, the doctest of `qt_curve`, `vm-features` (section D: two curves are in different places at the same moment) | a cubic Bézier is an ease out |
|
|
| Only the windows the rules make see-through are, and only while transparency is on | framework path: [`transparency_manager::apply`] asks [`transparency_manager::transparent_windows`] and sends the difference from [`WindowManager::opacity_applied`] | `unfocused_windows_are_transparent_and_the_focused_one_is_not`, `turning_transparency_off_makes_everything_opaque_again`, `a_monocled_window_is_not_transparent`, `windows_the_rules_ignore_keep_their_opacity`, `vm-features` (section B) | none |
|
|
| The `transparency` keys of the file are the settings, and a key the file leaves out has the default | framework path: `StaticConfig::apply_globals` stores `transparency` and `transparency_alpha` whether the file has them or not, and adds the ignore rules to `TRANSPARENCY_BLACKLIST` | `the_transparency_keys_of_the_file_are_the_transparency_settings` | none |
|
|
| A theme gives every kind of border a color, the one it names or the theme's default | framework path: [`theme_manager::send_notification`] stores a color for each of the six kinds, from the theme's own defaults for the ones the message does not name | `a_named_theme_colours_every_kind_of_border` | none |
|
|
| The `border_colours` of the file are the border colors, and a theme in the file wins over them | framework path: `StaticConfig::apply_globals` stores each color the file names, and then calls [`theme_manager::send_notification`] for the `theme` | `the_colour_and_theme_keys_of_the_file_are_the_border_colours` | none |
|
|
| A picture's palette is made again when the picture changes, and is not mistaken for another picture's | framework path: [`theme_manager::palette_file`] names the file by a hash of the picture's path, size and time of last change, and the variant | the doctest of `palette_file` | the hash is not a proof of identity |
|
|
| A workspace that comes into view has its wallpaper set, its own before its monitor's, however it came into view; none is touched for a workspace and a monitor without one | framework path: [`Workspace::restore`], [`WindowManager::adopt_workspace_change`] and [`WindowManager::retile_all`] call [`Workspace::apply_wallpaper`], which takes `self.wallpaper` before the monitor's | `a_wallpaper_is_set_when_its_workspace_comes_into_view_by_whoever_brings_it`, `no_wallpaper_is_touched_that_was_not_asked_for`, `vm-wallpaper` (sections A and B) | none |
|
|
| The borders have the colors of the wallpaper in front, and keep theirs when it asks for no theme or has no palette | framework path: [`Workspace::apply_wallpaper`] sends a theme only for a wallpaper that does not say `generate_theme: false` and has a palette | `the_borders_take_their_colours_from_the_wallpaper_and_follow_it`, `a_wallpaper_that_asks_for_no_theme_leaves_the_borders_alone`, `a_wallpaper_that_is_not_a_picture_still_sets_and_does_not_break_the_window_manager`, `theme_options_choose_the_variant_and_the_colour_each_border_takes`, `vm-wallpaper` (section C) | none |
|
|
| A wallpaper the screen shows already is not set again, and one the user changed by hand is set back | framework path: [`kwin::wallpaper`]`::apply` compares what the shell answers with the picture, the fill and the plugin | `vm-wallpaper` (section D) | the comparison alone has no test |
|
|
| Any path is a URL the shell can read, and the name of an output cannot end the string it is in in the shell's script | framework path: `kwin::wallpaper::file_url` percent-encodes every byte but a few ASCII ones, and the script writes the name as a JSON string | `a_path_becomes_a_url_the_shell_can_read`, `the_name_of_an_output_cannot_end_the_string_it_is_in`, `vm-wallpaper` (a path with a space in it) | none |
|
|
| Setting a wallpaper never holds up the window manager | framework path: [`Backend::set_wallpaper`] of the KWin backend notes the wish and spawns the request on the runtime | none | the monitor and the path are checked before it returns |
|
|
| The KWin backend refuses a wallpaper for a monitor it does not have and for a path that is no file | framework path: `set_wallpaper` of the KWin backend looks the monitor up in its table and checks the path | `a_wallpaper_is_refused_for_a_monitor_kwin_lacks_and_for_a_path_that_is_no_file` | none |
|
|
| A window komorebi made see-through is opaque again when komorebi exits | framework path: [`WindowManager::stop`] and `main.rs` call [`transparency_manager::restore`] | `what_komorebi_made_see_through_is_opaque_again_when_it_exits` | `SIGKILL`: the windows stay as they were drawn |
|
|
| What the backend sends for an opacity is what the bridge sets | tests: the `opacity` command of [`fixtures`] is what the backend's test sends and what the bridge's test answers with, checking the window's `opacity` | `the_commands_of_a_session_go_to_the_bridge_as_the_bridges_tests_answer_it`, `what the bridge sends for a session is what the daemon's tests read`, `a window is given the opacity it is asked for, and one KWin has not got is given back` | none |
|
|
| The user's focus policy is put back when focus follows mouse is turned off and when komorebi exits | framework path: [`Kwinrc`] records the first value of each key it sets, `set_focus_follows_mouse(false)` puts the two keys back with [`Kwinrc::restore_keys`] and `shutdown` puts back all | `one_key_can_be_put_back_while_the_others_in_its_group_stay_changed`, `what_komorebi_changed_is_put_back_exactly_and_what_it_added_is_removed`, `vm-focus-follows-mouse` | `SIGKILL`: the next run puts it back when it stops |
|
|
| Focus follows mouse is the compositor's, whichever implementation is named | framework path: the `FocusFollowsMouse` and `ToggleFocusFollowsMouse` arms ask the backend the same thing for both | `focus_follows_mouse_is_asked_of_the_compositor_and_toggles` | none |
|
|
| The `focus_follows_mouse` key of the file turns it on, and a file that leaves it out turns it off | framework path: `StaticConfig::postload` and `StaticConfig::reload` set the window manager's field from the file and ask the backend for it | `the_file_turns_focus_follows_mouse_on_and_a_file_that_leaves_it_out_turns_it_off` | none |
|
|
| The window under the pointer has the focus, and komorebi follows | the compositor's focus policy; the window manager follows the focus event as any other | `vm-focus-follows-mouse` (a real pointer over each window) | none |
|
|
| The effect komorebi loaded is unloaded, and the user's settings of it are put back, when komorebi stops using it or exits | framework path: the effect's state unloads what komorebi loaded, and [`Kwinrc`] puts the group back | `one_group_can_be_put_back_while_the_others_stay_changed`, `vm-look` | `SIGKILL`: the next run puts the settings back when it stops |
|
|
| Queries never block on the compositor | convention only | none | the KWin backend's `cursor_pos` waits up to 250 ms |
|
|
| Only the backend module reaches the compositor | convention only | none | none |
|
|
|
|
The rows enforced by convention are rules nothing checks. A real backend that waits inside a
|
|
query breaks one of them, a module that talks to D-Bus directly breaks another, and a bridge
|
|
that decides something in JavaScript breaks a third: the bridge's tests check what it sends,
|
|
not that it decides nothing.
|
|
|
|
## One event, end to end
|
|
|
|
A window is shown, and ends up on a tile:
|
|
|
|
1. KWin raises `windowAdded`. The bridge sends an `added` event with what KWin says about
|
|
the window, and [`Table::handle`] gives the window an id, records it, and returns a
|
|
[`WindowManagerEvent`]`::Show` carrying the window and the compositor signal it came
|
|
from. The event goes on the channel, and the event thread calls
|
|
[`WindowManager::process_event`], which hands it to `handle_event` and logs how long that
|
|
took. The end-to-end tests build the event themselves and call `process_event`.
|
|
2. `handle_event` asks [`Window::should_manage`], which asks the backend's `window(id)`
|
|
whether the window is alive and normal, and then the rules whether it is to be managed. If
|
|
it is not, the event changes nothing. If a float rule matches it, it goes into the
|
|
workspace's floating windows and the steps below do not apply.
|
|
3. The index of managed windows is rebuilt from the containers. If the workspace in front
|
|
already has the window, or the index has it on another monitor or workspace, the event
|
|
creates nothing.
|
|
4. [`Workspace::new_container_for_window`] puts the window in a new container, inserted through
|
|
`LockableSequence` so that a locked container keeps its index, and focuses the new container.
|
|
It goes after the focused one, unless a place was preselected or the workspace's placement
|
|
rules give another.
|
|
5. [`WindowManager::update_focused_workspace`] asks the focused [`Monitor`] to give its
|
|
focused workspace the work area, the padding and the offset ([`WorkspaceGlobals`]),
|
|
and calls [`Workspace::update`].
|
|
6. `Workspace::update` asks the layout for one tile per container, clamps each to the work
|
|
area, and calls [`Window::set_position`] for each window, which is the backend's
|
|
`set_rect`. The fake puts the window there at once; the frame a window ends up with is
|
|
what `window_rect` says afterwards. The KWin backend queues a `geo` command for the
|
|
bridge, the bridge sets the window's frame, the client draws at the size it accepts, and
|
|
KWin reports the frame it ended up with as a `geometry` event, which updates the table.
|
|
7. If the new container is the only one, the window is given the focus with the backend's
|
|
`focus`, and the pointer follows it (on KWin, through its "Move Mouse to Focus"
|
|
shortcut).
|
|
8. The index is rebuilt again, so the next event finds the window.
|
|
9. [`notify_subscribers`] sends every subscriber a [`Notification`] of the `Show`, with the
|
|
state that now has the window. A `Show` is sent to a subscriber with
|
|
`filter_state_changes` too, whether or not it changed the state.
|
|
|
|
## One message, end to end
|
|
|
|
`komorebic state` asks for the state of the window manager:
|
|
|
|
1. `komorebic` calls [`send_query`], which connects to `komorebi.sock` in [`DATA_DIR`],
|
|
writes `{"type":"State"}`, and shuts down its side of the connection for writing.
|
|
2. The thread that [`listen_for_commands`] started accepts the connection and starts a
|
|
thread for it, which calls [`read_commands_uds`].
|
|
3. `read_commands_uds` reads the line, parses it into [`SocketMessage`]`::State`, and locks
|
|
the window manager, waiting up to a second.
|
|
4. [`WindowManager::process_command`] makes a [`State`] from the window manager, a copy of
|
|
every monitor with its workspaces, containers and windows. Each window is serialized
|
|
with what the backend reports about it now: its frame, title, executable, class and path.
|
|
5. The state is written to the connection as pretty-printed JSON, and the index of managed
|
|
windows is rebuilt, as after every message. The subscribers are sent a notification of
|
|
the `State` message, except those with `filter_state_changes`: it changed nothing.
|
|
6. There are no more lines, so `read_commands_uds` returns and the connection is closed.
|
|
`send_query` has read to the end, and returns the JSON, which `komorebic` prints.
|
|
|
|
## Key bindings
|
|
|
|
[`komorebi-hotkeys`](../../../crates/komorebi-hotkeys/src/lib.rs) is a crate of its own, which does not
|
|
depend on the daemon. Its model is the content of a key binding file of any of three hotkey
|
|
daemons: skhd (macOS), whkd (Windows) and sxhkd (X11).
|
|
|
|
### The key binding model
|
|
|
|
- **Keymap**: one file: its modes, the applications in which no binding is active (a
|
|
blacklist), a binding that turns the others off and on (a pause binding), and the names the
|
|
file defined.
|
|
- **Mode**: bindings that are active together. A keymap starts in the mode `default`.
|
|
- **Binding**: a chord (one or more combos, pressed one after the other), whether it fires on
|
|
press or on release, and a body: an action (run a command, or switch to a mode), or an action
|
|
per application (scopes).
|
|
- **Combo**: a key and its modifiers, as the integer KDE stores (a Qt key code with the modifier
|
|
flags or'ed in) and the portable text a `.desktop` file writes (`Alt+H`).
|
|
- **Diagnostic**: something about a file its user should know: its span (byte offsets and line),
|
|
its severity (error, warning, note) and how faithfully the construct is realized (exactly,
|
|
approximated, not at all).
|
|
- **Parsed**: a file, read: the keymap and the diagnostics.
|
|
- **Static shortcut**: a binding KDE realizes from a `.desktop` file, with no komorebi process:
|
|
one combo, pressed, the same in every application, in a file without modes, blacklist or pause
|
|
binding.
|
|
- **Engine**: the part of komorebi that registers keys with KDE and decides what each press does,
|
|
for everything a static shortcut cannot be.
|
|
- **Plan**: the bindings of a keymap sorted into static shortcuts and the engine's keymap, with
|
|
notes on why each binding goes where it goes.
|
|
- **Active set**: the combos the engine holds: KDE swallows an active combo and reports its
|
|
presses, and lets every other key reach the focused application.
|
|
- **Effect**: what the caller of the engine does after an event: activate or deactivate a combo,
|
|
or run a command.
|
|
|
|
### The parts of the key bindings
|
|
|
|
- [`qt`](../../../crates/komorebi-hotkeys/src/qt.rs): key names to Qt key codes, in the vocabulary of each
|
|
format (skhd's literals and macOS keycodes, whkd's virtual key names, sxhkd's X keysyms), and
|
|
`Combo`, which applies the shifted symbol rule: KWin matches the character a key press
|
|
produced, so Alt+Shift+1 is registered as Alt+!.
|
|
- [`ir`](../../../crates/komorebi-hotkeys/src/ir.rs): the model.
|
|
- [`skhd`](../../../crates/komorebi-hotkeys/src/skhd.rs): reads an skhdrc. A mode may be used before it
|
|
is declared, so each binding waits for the end of the file to be put in its modes. skhd.zig's
|
|
`.define` templates and application groups are expanded where they are used.
|
|
- [`whkd`](../../../crates/komorebi-hotkeys/src/whkd.rs): reads a whkdrc, in any order of its sections,
|
|
where whkd requires one. `tests/whkd_parser.rs` compares it with whkd's own parser,
|
|
`whkd-parser` (a dev-dependency), on every whkdrc of the corpus.
|
|
- [`sxhkd`](../../../crates/komorebi-hotkeys/src/sxhkd.rs): reads an sxhkdrc. Braces are expanded first,
|
|
into one binding per alternative. A chord chain (`a ; b`) is one binding with several combos;
|
|
a chain that stays open (`a : b`) is a mode named after its prefix, entered by the prefix and
|
|
left by Escape.
|
|
- [`parse`](../../../crates/komorebi-hotkeys/src/lib.rs) picks the reader by format.
|
|
- [`plan`](../../../crates/komorebi-hotkeys/src/plan.rs): sorts a keymap into static shortcuts and the
|
|
engine's keymap, by mode: `Auto` (static where it can be), `Engine` (the default: everything
|
|
through the engine) or `Static` (no engine; what needs it is left out with a warning).
|
|
- [`engine`](../../../crates/komorebi-hotkeys/src/engine.rs): the engine, a state machine with no I/O and
|
|
no clock. Its inputs are a press or a release of an active combo, a change of focus, and the
|
|
time; after each it computes the active set it wants from the mode, the chord chain being
|
|
typed, the focused application and whether it is paused, and returns the commands to run and
|
|
the difference with the active set as effects. The daemon carries them out with kglobalacceld
|
|
(`setShortcutKeys`, `setInactive`) and feeds it kglobalacceld's signals.
|
|
- [`kde`](../../../crates/komorebi-hotkeys/src/kde.rs): writes static shortcuts as `.desktop` files for
|
|
kglobalacceld (`X-KDE-Shortcuts`, `X-KDE-GlobalAccel-CommandShortcut`, an `Exec` line quoted as
|
|
the desktop entry specification requires), and replaces the files it wrote before, and no
|
|
other.
|
|
- [`text`](../../../crates/komorebi-hotkeys/src/text.rs): what the readers share: line numbers, comments,
|
|
continued lines and the diagnostics' wording.
|
|
- [`tests/fixtures`](../../../crates/komorebi-hotkeys/tests/fixtures): the corpus, `<case>.skhdrc`,
|
|
`<case>.whkdrc` and `<case>.sxhkdrc`, each case in every format that can express it.
|
|
|
|
### The front end
|
|
|
|
[`komorebic/src/linux/hotkeys.rs`](../../../komorebic/src/linux/hotkeys.rs) is the command line over the
|
|
crate, and decides nothing about what a file means. `komorebic hotkeys compile` reads a file (by default the
|
|
first of `skhdrc`, `whkdrc` and `sxhkdrc` in the komorebi config directory, its format from
|
|
the end of its name), plans it in the mode it is given (by default `PlanMode::default()`),
|
|
prints the diagnostics of reading and of planning on standard error, and writes the static
|
|
shortcuts to `$XDG_DATA_HOME/kglobalaccel` or `--output`, replacing the files it wrote before.
|
|
With `--strict`, a diagnostic that is a warning or an error stops it before it writes
|
|
anything. `--dry-run` writes nothing and prints what a write would change, and `--reload`
|
|
hands each new file to the running KWin, which otherwise reads them at the next login.
|
|
`komorebic export-kde-shortcuts` is the same for an skhdrc, in `PlanMode::Static`.
|
|
|
|
`komorebic check` has a section for the key bindings, between the configuration and the desktop.
|
|
`hotkeys::check` finds the file as `compile` does, reads and plans it in the default mode, and
|
|
returns [`Finding`]s: how many bindings the file has and where they go, how many lines cannot be
|
|
read and left out (a warning) and how many bindings are not realized as their own daemon would
|
|
(a note), each with the first five, and whether the generated files in the shortcut directory are
|
|
the ones `kde::diff` says that plan would write. It reads no file the desktop has no part in, so
|
|
`--configuration-only` keeps it.
|
|
|
|
### The engine in the daemon
|
|
|
|
[`crates/komorebi-linux/src/kwin/hotkeys.rs`](../../../crates/komorebi-linux/src/kwin/hotkeys.rs) runs the engine against
|
|
kglobalaccel, on a session bus connection and a thread of its own. The daemon reads the file
|
|
it is given with `--keybindings` (or the first it finds in the config directory), plans it in
|
|
`--keybindings-mode`, and hands the plan's engine keymap to `kwin::hotkeys::start`, unless the
|
|
plan needs no engine. A failure to start is logged, and the window manager runs without keys.
|
|
|
|
- **Calls**: each effect becomes calls on `/kglobalaccel` (`Registry`): an activation is
|
|
`setShortcutKeys(id, [[combo]], 6)`, preceded by `doRegister` the first time; a deactivation
|
|
is `setInactive`. The calls of one event are sent together, and only those for one action
|
|
wait for each other.
|
|
- **Signals**: `globalShortcutPressed` and `globalShortcutReleased` of `/component/komorebi`
|
|
are the engine's presses and releases. `globalShortcutRepeated` is not listened to.
|
|
- **Commands**: a `komorebic` command runs `komorebic` (the one next to the daemon) with its
|
|
words when it needs no shell, and anything else runs through `sh -c`, as processes the
|
|
daemon does not wait for.
|
|
- **Modes**: a mode switch is the engine's deactivations and activations, carried out like
|
|
any others; the daemon logs the mode after each change.
|
|
- **Chord chains**: after the prefix of a chain, only the keys that can come next and Escape
|
|
are grabbed. The transport sleeps until `Engine::deadline` (the chord timeout after the last
|
|
combo of the chain, `--chord-timeout`, 3 seconds by default) and calls `Engine::on_tick`
|
|
then, which abandons the chain; there is no timer otherwise.
|
|
- **Focus**: after every event of the bridge, the KWin backend gives the engine the
|
|
application of the window KWin activated (`Table::focused_application`: its executable, app id
|
|
and desktop entry), and a change makes the engine grab the keys of the bindings that do
|
|
something there and let the others go. The aliases of `--app-alias` are names of an
|
|
application too.
|
|
- **Start**: the component's actions that the keymap names are taken over, and released
|
|
unless the default mode wants them; the others are unregistered. The engine starts in the
|
|
default mode.
|
|
- **Stop**: every key is released and every action unregistered, from
|
|
`KWinBackend::shutdown`, which every way the daemon stops goes through. A daemon that is
|
|
killed cannot: `kwin::hotkeys::release` (`komorebic hotkeys release`, the module's
|
|
`ExecStopPost`) unregisters every action of the component, with or without a daemon.
|
|
- **Reload**: `kwin::hotkeys::load` reads the file, plans it and starts the engine, and keeps
|
|
its settings; `reload` (the message `ReloadHotkeys`, `komorebic hotkeys reload`) does it again.
|
|
A new engine replaces the running one without unregistering its keys first, and takes them
|
|
over as a start does.
|
|
- **KWin restarting**: kglobalaccel runs in KWin. The KWin backend's watch of
|
|
`NameOwnerChanged` also follows `org.kde.kglobalaccel`, and when it has an owner again,
|
|
`kwin::hotkeys::rebind` loads the key bindings into it, trying for 15 seconds.
|
|
- **State**: the message `HotkeysState` is answered with the engine's mode, the combos it
|
|
holds and whether a chain is pending, or `null` when no engine runs; `komorebic hotkeys
|
|
state` adds the actions kglobalaccel has for the component.
|
|
|
|
### What is guaranteed for the key bindings, and by what
|
|
|
|
| Rule | Enforced by | Proof | Escape hatch |
|
|
|---|---|---|---|
|
|
| A combo is in the form KWin matches (the shifted symbol rule is applied) | type system: `Combo`'s fields are private, and `Combo::new` applies the rule | `compile_fail,E0451` on `Combo`; `shift_on_a_symbol_key_is_the_symbol_it_types` | none |
|
|
| A key has the same code in every vocabulary that names it | test | `the_three_vocabularies_name_the_same_keys` | none |
|
|
| Positional key names (macOS keycodes, `oem_*`) are read as on a US layout | convention only | none | none |
|
|
| Reading a file returns a keymap, whatever the text | type system: a reader returns `Parsed`, not a `Result` | none | none |
|
|
| Text that is left out has a diagnostic that points at it | test | `what_cannot_be_read_is_reported_and_left_out`, `reading_goes_on_after_an_error`, `diagnostics_point_at_the_text_they_are_about` | none |
|
|
| A fixture reads, without errors, into the keymap of its case | test | `every_file_reads_without_errors_into_the_keymap_of_its_case` | none |
|
|
| The files of a case read into the same keymap, apart from where each binding was written | test | `the_files_of_a_case_read_into_the_same_keymap` | none |
|
|
| A static shortcut is a single combo, pressed, the same in every application, in the default mode of a file with no modes, blacklist or pause binding | test | `a_plain_binding_is_static_and_the_rest_goes_through_the_engine`, `modes_a_blacklist_or_a_pause_binding_send_every_binding_through_the_engine` | `PlanMode::Engine` sends every binding through the engine |
|
|
| A rewrite of the shortcut files removes only the files it generated | test | `rewriting_replaces_our_files_and_nothing_else` | none |
|
|
| An `Exec` line runs the command as written (quoting, `%%`) | test | `exec_lines_are_quoted_for_the_desktop_entry_spec` | none |
|
|
| The sample configuration plans into the same 50 shortcut files from each format | test | `the_sample_plans_into_the_same_shortcut_files_in_every_format` | none |
|
|
| After every event, the active set is the one the engine wants for its state, and each effect changes it (no combo is activated twice) | framework path: every event handler that changes the state ends in the one reconcile step, which diffs two sets | `a_chain_activates_its_next_keys_until_it_ends`, `a_mode_has_its_own_keys` | none |
|
|
| A key the engine does not want reaches the application | framework path: the daemon turns every deactivation into `setInactive` (`Registry::calls`); VM test | `entering_a_mode_lets_the_keys_of_the_other_go_and_grabs_its_own`, `vm-keys-engine` (a bare H in a mode and out of it, seen by `wev`) | none |
|
|
| A scoped key is active only for an application a scope gives an action | test | `a_scoped_key_is_active_only_where_it_does_something` | none |
|
|
| Stopping the engine gives every key back | test, of the effects; carrying them out is the daemon's | `start_activates_the_default_mode_and_stop_gives_everything_back` | none |
|
|
| The engine does no I/O and reads no clock | convention only | none | none |
|
|
| The whkd reader reads a whkdrc as whkd does | test, on the files it is given | `every_whkdrc_of_the_corpus_reads_as_whkd_reads_it`, `the_example_of_whkds_readme_reads_as_whkd_reads_it` | none |
|
|
| `hotkeys compile` writes the static shortcuts of the plan and nothing else, from a file of any format | test, through the binary | `compile_writes_the_same_shortcut_files_from_each_format`, `compile_in_engine_mode_writes_no_shortcut_and_removes_the_ones_it_wrote` | none |
|
|
| `komorebic check` reads the key binding file `hotkeys compile` would read, the way it does | framework path: `check` and `export` share `find` and `diagnostics`, and call the same `parse` and `plan` | `the_first_file_of_the_three_formats_is_the_one_that_is_read`, `the_key_binding_file_of_the_config_home_is_read_and_a_line_it_cannot_read_is_a_warning` | none |
|
|
| A line of the key binding file that cannot be read does not fail `komorebic check` | test, through the binary | `the_key_binding_file_of_the_config_home_is_read_and_a_line_it_cannot_read_is_a_warning` | none |
|
|
| `komorebic check` says when the shortcut files are not the ones `hotkeys compile` would write | test | `shortcut_files_that_are_not_the_ones_of_the_plan_are_a_warning_that_says_how_many`, `shortcut_files_the_file_does_not_call_for_are_a_warning_until_compile_replaces_them` | none |
|
|
| `--strict` writes nothing when a diagnostic is a warning or an error | test, through the binary | `strict_refuses_a_file_with_a_warning_and_writes_nothing` | none |
|
|
| KWin runs a static shortcut's command on its key, from the system profile at the start of a session and from the user's directory after `--reload` | VM test | `vm-keys-static` | none |
|
|
| An effect becomes the calls that carry it out, and an action is registered once | test | `Registry` doctest, `a_start_with_nothing_left_behind_registers_and_grabs_the_default_mode` | none |
|
|
| A start takes over the keys a killed daemon left, releases those the default mode does not want, and unregisters the actions its keymap does not name | test, of the calls; VM test, against kglobalaccel | `a_start_takes_over_the_keys_of_a_killed_komorebi_and_drops_the_rest`, `vm-keys-engine` | none |
|
|
| A stop of the daemon unregisters every action | framework path: `KWinBackend::shutdown` calls `kwin::hotkeys::stop`, and both ways to stop call it; VM test | `vm-keys-engine` (`komorebic stop`) | `SIGKILL` leaves the keys until the next start |
|
|
| A press of a grabbed key runs its binding's command, `komorebic` without a shell where it needs none | test, of the process; VM test | `command` doctest, `a_press_runs_the_command_and_changes_no_key`, `vm-keys-engine` | none |
|
|
| Entering a mode lets go of the keys of the mode it leaves and grabs its own, a bare key among them | test, of the calls; VM test | `entering_a_mode_lets_the_keys_of_the_other_go_and_grabs_its_own`, `vm-keys-engine` | none |
|
|
| After the prefix of a chain only its next keys and Escape are grabbed, until a key ends the chain or its deadline passes, and the keys of the mode come back then | test, of the calls; VM test | `a_chain_grabs_its_next_keys_until_its_deadline`, the `Engine::deadline` doctest, `vm-keys-engine` | none |
|
|
| The application of the focus is the one of the window KWin activated, a dialog included, and none once that window is gone | test | `the_focused_application_is_the_one_of_the_window_kwin_activated` | none |
|
|
| A per-application key is grabbed only while the focused application (or an alias of it) has an action for it, and reaches the application otherwise | test, of the calls; VM test | `a_scoped_key_is_grabbed_only_while_its_application_has_the_focus`, `vm-keys-engine` | none |
|
|
| A key of a daemon that stopped answering is given back by `hotkeys release`, and a new daemon registers it again | VM test | `vm-restart-survival` | none |
|
|
| After KWin restarts, the keys are registered with the new kglobalaccel | framework path: the KWin backend's name-owner watch calls `kwin::hotkeys::rebind`; VM test | `vm-restart-survival` | none |
|
|
| A reload reads the file again, and the keys both files have stay grabbed | framework path: `start` replaces an engine without unregistering, and `Registry::startup` takes over; VM test | `a_start_takes_over_the_keys_of_a_killed_komorebi_and_drops_the_rest`, `vm-restart-survival` | none |
|
|
| Without key bindings, the daemon shows no engine and refuses a reload | test | `without_key_bindings_there_is_no_engine_to_show_or_reload` | none |
|
|
| Holding a key runs its binding once | convention only: the transport listens to `globalShortcutPressed` and not to `globalShortcutRepeated` | none (measured in PLATFORM.md) | none |
|
|
|
|
## Where to look in the code
|
|
|
|
- [`crates/komorebi-linux/src/backend/mod.rs`](../../../crates/komorebi-linux/src/backend/mod.rs): the trait.
|
|
- [`crates/komorebi-linux/src/backend/fake.rs`](../../../crates/komorebi-linux/src/backend/fake.rs): the fake and its
|
|
tests.
|
|
- [`crates/komorebi-linux/src/core/mod.rs`](../../../crates/komorebi-linux/src/core/mod.rs): the vocabulary the rest is written
|
|
in.
|
|
- [`crates/komorebi-linux/src/workspace.rs`](../../../crates/komorebi-linux/src/workspace.rs): tiling, in
|
|
`Workspace::update`.
|
|
- [`crates/komorebi-linux/src/monitor.rs`](../../../crates/komorebi-linux/src/monitor.rs): the work area, padding and
|
|
offset a monitor gives the workspace it shows.
|
|
- [`crates/komorebi-linux/src/static_config.rs`](../../../crates/komorebi-linux/src/static_config.rs): `komorebi.json`.
|
|
[`crates/komorebi-linux/tests/config.rs`](../../../crates/komorebi-linux/tests/config.rs) loads files over the fake
|
|
desktop, [`crates/komorebi-linux/tests/config_displays.rs`](../../../crates/komorebi-linux/tests/config_displays.rs) one
|
|
with several monitors, [`komorebi/tests/linux_hot_reload.rs`](../../../komorebi/tests/linux_hot_reload.rs) the
|
|
daemon following a file, and
|
|
[`crates/komorebi-linux/tests/config_parity.rs`](../../../crates/komorebi-linux/tests/config_parity.rs) the files of other
|
|
platforms and the example, which load;
|
|
[`crates/komorebi-linux/tests/replace_configuration.rs`](../../../crates/komorebi-linux/tests/replace_configuration.rs)
|
|
the `ReplaceConfiguration` message;
|
|
[`crates/komorebi-linux/tests/workspace_rules.rs`](../../../crates/komorebi-linux/tests/workspace_rules.rs) the workspace
|
|
rules; [`crates/komorebi-linux/tests/schemas.rs`](../../../crates/komorebi-linux/tests/schemas.rs) the schemas the daemon answers
|
|
with; [`schemas/linux/schema.json`](../../../schemas/linux/schema.json), [`schemas/linux/schema.asc.json`](../../../schemas/linux/schema.asc.json) and
|
|
[`nix/linux/komorebi-options.nix`](../../../nix/linux/komorebi-options.nix), which are generated;
|
|
[`nix/linux/tests/default.nix`](../../../nix/linux/tests/default.nix) and
|
|
[`nix/linux/tests/komorebi-example.nix`](../../../nix/linux/tests/komorebi-example.nix), the check of the options
|
|
and the example it evaluates them against; [`crates/komorebi-linux/tests/asc.rs`](../../../crates/komorebi-linux/tests/asc.rs)
|
|
the applications files; [`crates/komorebi-linux/src/core/asc.rs`](../../../crates/komorebi-linux/src/core/asc.rs) their
|
|
format.
|
|
- [`crates/komorebi-linux/src/check.rs`](../../../crates/komorebi-linux/src/check.rs): the checks of `komorebic check`, with
|
|
their tests; [`komorebic/tests/linux_check.rs`](../../../komorebic/tests/linux_check.rs) runs the command on
|
|
configurations, on key binding files and on buses that are not there, and reads what it prints
|
|
and its exit status;
|
|
[`nix/linux/tests/vm/suites/check/test.py`](../../../nix/linux/tests/vm/suites/check/test.py), the desktop check against
|
|
a real KWin, with the daemon running, with its script gone, with no daemon and with no KWin.
|
|
- [`crates/komorebi-linux/src/process_event.rs`](../../../crates/komorebi-linux/src/process_event.rs): what each event
|
|
does, and the thread that handles them.
|
|
- [`crates/komorebi-linux/src/process_command.rs`](../../../crates/komorebi-linux/src/process_command.rs): the socket, what
|
|
each message does, and the tests that send messages through a real socket.
|
|
- [`crates/komorebi-client/src/lib.rs`](../../../crates/komorebi-client/src/lib.rs): the client side of the
|
|
socket.
|
|
- [`crates/komorebi-client/tests/linux_subscribe.rs`](../../../crates/komorebi-client/tests/linux_subscribe.rs):
|
|
subscribers and the notifications they are sent, through real sockets.
|
|
- [`komorebic/src/linux/mod.rs`](../../../komorebic/src/linux/mod.rs): the subcommands, and
|
|
[`komorebic/tests/linux_cli.rs`](../../../komorebic/tests/linux_cli.rs), what each writes on the socket.
|
|
- [`crates/komorebi-linux/src/kwin/mod.rs`](../../../crates/komorebi-linux/src/kwin/mod.rs): the KWin backend, its D-Bus
|
|
service and the held call.
|
|
- [`crates/komorebi-linux/src/kwin/state.rs`](../../../crates/komorebi-linux/src/kwin/state.rs): the table, with the
|
|
shapes of the bridge's events, and its tests.
|
|
- [`crates/komorebi-linux/src/kwin/bridge.js`](../../../crates/komorebi-linux/src/kwin/bridge.js): the bridge, with the
|
|
types of its messages; [`kwin-api.d.ts`] and
|
|
[`tsconfig.json`](../../../crates/komorebi-linux/src/kwin/tsconfig.json), what `tsc` checks it against;
|
|
[`bridge.test.js`], its tests; [`fixtures`], the messages both sides' tests read.
|
|
- [`nix/linux/tests/vm/suites/kwin-events/test.py`](../../../nix/linux/tests/vm/suites/kwin-events/test.py): the
|
|
events of a real KWin, through the daemon.
|
|
- [`nix/linux/tests/vm/suites/core-e2e/test.py`](../../../nix/linux/tests/vm/suites/core-e2e/test.py): the daemon
|
|
tiling a real KWin, checked against what KWin has, with a window in monocle, and read and
|
|
stopped with `komorebic`.
|
|
- [`nix/linux/tests/vm/suites/kwin-restart/test.py`](../../../nix/linux/tests/vm/suites/kwin-restart/test.py): KWin
|
|
restarting under the daemon.
|
|
- [`nix/linux/kde-rounded-corners.nix`](../../../nix/linux/kde-rounded-corners.nix): the build of the KWin
|
|
effect that draws an outline around each window, with
|
|
[`komorebi-border-colours.patch`](../../../nix/linux/kde-rounded-corners/komorebi-border-colours.patch),
|
|
which lets komorebi give each window a color;
|
|
[`nix/linux/kwin-effect-geometry-change.nix`](../../../nix/linux/kwin-effect-geometry-change.nix), the
|
|
effect that animates a window going to its place, with
|
|
[`curve.patch`](../../../nix/linux/kwin-effect-geometry-change/curve.patch), which makes its easing
|
|
curve a setting; and
|
|
[`nix/linux/tests/effects.nix`](../../../nix/linux/tests/effects.nix), the checks of the files each effect
|
|
package installs.
|
|
- [`crates/komorebi-linux/tests/borders.rs`](../../../crates/komorebi-linux/tests/borders.rs) and
|
|
[`crates/komorebi-linux/tests/border_commands.rs`](../../../crates/komorebi-linux/tests/border_commands.rs): the room a drawn
|
|
border takes, and the messages that set the border, against the fake;
|
|
[`crates/komorebi-linux/tests/animation.rs`](../../../crates/komorebi-linux/tests/animation.rs), the messages that set the
|
|
animation, [`crates/komorebi-linux/tests/animation_config.rs`](../../../crates/komorebi-linux/tests/animation_config.rs), its
|
|
key of the file, and [`crates/komorebi-linux/tests/animation_stack.rs`](../../../crates/komorebi-linux/tests/animation_stack.rs),
|
|
the stack commands that turn it off; [`crates/komorebi-linux/tests/themes.rs`](../../../crates/komorebi-linux/tests/themes.rs),
|
|
a named theme and [`crates/komorebi-linux/tests/theme_config.rs`](../../../crates/komorebi-linux/tests/theme_config.rs), the
|
|
colors and the theme of the file; [`crates/komorebi-linux/tests/focus_follows_mouse.rs`](../../../crates/komorebi-linux/tests/focus_follows_mouse.rs),
|
|
focus follows mouse; [`crates/komorebi-linux/tests/transparency.rs`](../../../crates/komorebi-linux/tests/transparency.rs)
|
|
and [`crates/komorebi-linux/tests/transparency_config.rs`](../../../crates/komorebi-linux/tests/transparency_config.rs),
|
|
transparency and its keys of the file; [`crates/komorebi-linux/tests/border_colours.rs`](../../../crates/komorebi-linux/tests/border_colours.rs) and
|
|
[`crates/komorebi-linux/tests/borders_off.rs`](../../../crates/komorebi-linux/tests/borders_off.rs), the color of each
|
|
window's border, and none with borders off;
|
|
[`nix/linux/tests/vm/suites/look/test.py`](../../../nix/linux/tests/vm/suites/look/test.py), the border in a real
|
|
Plasma session, read from the pixels of a screenshot, and
|
|
[`nix/linux/tests/vm/suites/features/test.py`](../../../nix/linux/tests/vm/suites/features/test.py), the colors of the
|
|
kinds of window; [`nix/linux/tests/vm/suites/focus-follows-mouse/test.py`](../../../nix/linux/tests/vm/suites/focus-follows-mouse/test.py),
|
|
focus follows mouse with a real pointer, which [`Pointer`](../../../nix/linux/tests/vm/lib/komorebi_vm/pointer.py)
|
|
moves from inside the guest; [`crates/komorebi-linux/tests/wallpaper.rs`](../../../crates/komorebi-linux/tests/wallpaper.rs), the
|
|
wallpapers and the colors made of them, against the fake; and
|
|
[`nix/linux/tests/vm/suites/wallpaper/test.py`](../../../nix/linux/tests/vm/suites/wallpaper/test.py), against the
|
|
real Plasma shell.
|
|
- [`crates/komorebi-linux/tests/wm_events.rs`](../../../crates/komorebi-linux/tests/wm_events.rs): the window manager
|
|
driven end to end against the fake.
|
|
- [`komorebi/tests/linux_socket.rs`](../../../komorebi/tests/linux_socket.rs): the daemon over its socket,
|
|
from its start to a `Stop`.
|
|
- [`crates/komorebi-linux/tests/focus.rs`](../../../crates/komorebi-linux/tests/focus.rs),
|
|
[`crates/komorebi-linux/tests/moving.rs`](../../../crates/komorebi-linux/tests/moving.rs) and
|
|
[`crates/komorebi-linux/tests/layouts.rs`](../../../crates/komorebi-linux/tests/layouts.rs): the focus, move and layout
|
|
commands against the fake;
|
|
[`crates/komorebi-linux/tests/layout_options.rs`](../../../crates/komorebi-linux/tests/layout_options.rs) and
|
|
[`crates/komorebi-linux/tests/scrolling.rs`](../../../crates/komorebi-linux/tests/scrolling.rs), the layout options and the
|
|
scrolling layout; [`crates/komorebi-linux/tests/resize.rs`](../../../crates/komorebi-linux/tests/resize.rs), resizing;
|
|
[`crates/komorebi-linux/tests/promote.rs`](../../../crates/komorebi-linux/tests/promote.rs), promoting;
|
|
[`crates/komorebi-linux/tests/float.rs`](../../../crates/komorebi-linux/tests/float.rs), floating;
|
|
[`crates/komorebi-linux/tests/lock.rs`](../../../crates/komorebi-linux/tests/lock.rs), locking;
|
|
[`crates/komorebi-linux/tests/floating_layer.rs`](../../../crates/komorebi-linux/tests/floating_layer.rs), the layers;
|
|
[`crates/komorebi-linux/tests/monocle.rs`](../../../crates/komorebi-linux/tests/monocle.rs), the monocle;
|
|
[`crates/komorebi-linux/tests/maximize.rs`](../../../crates/komorebi-linux/tests/maximize.rs) and
|
|
[`crates/komorebi-linux/tests/maximize_commands.rs`](../../../crates/komorebi-linux/tests/maximize_commands.rs), maximizing;
|
|
[`crates/komorebi-linux/tests/user_maximize.rs`](../../../crates/komorebi-linux/tests/user_maximize.rs) and
|
|
[`crates/komorebi-linux/tests/user_maximize_cases.rs`](../../../crates/komorebi-linux/tests/user_maximize_cases.rs), a window
|
|
the user maximizes;
|
|
[`crates/komorebi-linux/tests/fullscreen.rs`](../../../crates/komorebi-linux/tests/fullscreen.rs), a window that goes
|
|
fullscreen and comes back;
|
|
[`crates/komorebi-linux/tests/placement.rs`](../../../crates/komorebi-linux/tests/placement.rs) and
|
|
[`crates/komorebi-linux/tests/placement_cases.rs`](../../../crates/komorebi-linux/tests/placement_cases.rs), the placement
|
|
rules;
|
|
[`crates/komorebi-linux/tests/manage.rs`](../../../crates/komorebi-linux/tests/manage.rs), minimizing, closing,
|
|
unmanaging and managing the focused window, and pausing;
|
|
[`crates/komorebi-linux/tests/padding.rs`](../../../crates/komorebi-linux/tests/padding.rs), the paddings and the work area
|
|
offsets; [`crates/komorebi-linux/tests/rules.rs`](../../../crates/komorebi-linux/tests/rules.rs) and
|
|
[`crates/komorebi-linux/tests/rule_commands.rs`](../../../crates/komorebi-linux/tests/rule_commands.rs), the rules and the
|
|
messages that add and show them;
|
|
[`crates/komorebi-linux/tests/drag_swap.rs`](../../../crates/komorebi-linux/tests/drag_swap.rs), dragging a window onto
|
|
another tile and onto another monitor;
|
|
[`crates/komorebi-linux/tests/drag_resize.rs`](../../../crates/komorebi-linux/tests/drag_resize.rs), dragging an edge;
|
|
[`crates/komorebi-linux/tests/live_resize.rs`](../../../crates/komorebi-linux/tests/live_resize.rs), the neighbors following
|
|
it.
|
|
- [`nix/linux/tests/vm/suites/scrolling/test.py`](../../../nix/linux/tests/vm/suites/scrolling/test.py): the scrolling
|
|
strip in a real KWin, with its columns out of view off the screen.
|
|
- [`nix/linux/tests/vm/suites/features/test.py`](../../../nix/linux/tests/vm/suites/features/test.py): what is on the
|
|
screen of a real compositing session, read from screenshots.
|
|
- [`nix/linux/tests/vm/suites/user-maximize/test.py`](../../../nix/linux/tests/vm/suites/user-maximize/test.py): a window
|
|
the user maximizes, in a real KWin.
|
|
- [`nix/linux/tests/vm/suites/placement/test.py`](../../../nix/linux/tests/vm/suites/placement/test.py): where the
|
|
windows of a real KWin go in the layout, by the placement rules of a `komorebi.json`.
|
|
- [`nix/linux/tests/vm/suites/commands/test.py`](../../../nix/linux/tests/vm/suites/commands/test.py): every layout,
|
|
and every `komorebic` command, against a real KWin with two outputs.
|
|
- [`nix/linux/tests/vm/suites/drag-resize/test.py`](../../../nix/linux/tests/vm/suites/drag-resize/test.py): edges and
|
|
corners dragged with a pointer in a Plasma session;
|
|
[`nix/linux/tests/vm/suites/live-resize/test.py`](../../../nix/linux/tests/vm/suites/live-resize/test.py), the
|
|
neighbors read while the edge is held.
|
|
- [`crates/komorebi-linux/tests/monitor_resize.rs`](../../../crates/komorebi-linux/tests/monitor_resize.rs),
|
|
[`crates/komorebi-linux/tests/hotplug.rs`](../../../crates/komorebi-linux/tests/hotplug.rs) and
|
|
[`crates/komorebi-linux/tests/follow_output.rs`](../../../crates/komorebi-linux/tests/follow_output.rs): a monitor that
|
|
changes size, monitors that are unplugged and plugged in, and windows that move to another
|
|
output, against the fake;
|
|
[`nix/linux/tests/vm/suites/hotplug/test.py`](../../../nix/linux/tests/vm/suites/hotplug/test.py), both against a real
|
|
KWin with two outputs.
|
|
- [`crates/komorebi-linux/tests/wake.rs`](../../../crates/komorebi-linux/tests/wake.rs): the desktop after a wake, against the
|
|
fake; [`crates/komorebi-linux/tests/power.rs`](../../../crates/komorebi-linux/tests/power.rs), the signals of logind and
|
|
of the lock screen, on private buses;
|
|
[`nix/linux/tests/vm/suites/lock-sleep/test.py`](../../../nix/linux/tests/vm/suites/lock-sleep/test.py), the lock
|
|
screen of a real Plasma session.
|
|
- [`crates/komorebi-linux/tests/workspaces.rs`](../../../crates/komorebi-linux/tests/workspaces.rs): workspaces as the
|
|
fake's desktops;
|
|
[`crates/komorebi-linux/tests/workspace_commands.rs`](../../../crates/komorebi-linux/tests/workspace_commands.rs), naming,
|
|
cycling and the other workspace commands;
|
|
[`crates/komorebi-linux/tests/monitors.rs`](../../../crates/komorebi-linux/tests/monitors.rs), two monitors: crossing from
|
|
one to the other, the monitor commands and `EagerFocus`; [`crates/komorebi-linux/src/kwin/kwinrc.rs`](../../../crates/komorebi-linux/src/kwin/kwinrc.rs), the
|
|
`kwinrc` editor and its tests.
|
|
- [`crates/komorebi-linux/tests/stacks.rs`](../../../crates/komorebi-linux/tests/stacks.rs): stacking, unstacking and the windows
|
|
a stack hides, the behavior of new windows and the float override, against the fake;
|
|
[`nix/linux/tests/vm/suites/stacks/test.py`](../../../nix/linux/tests/vm/suites/stacks/test.py), the same in a real
|
|
KWin, with the parking desktop, a crash and a stop.
|
|
- [`nix/linux/tests/vm/suites/workspace-rules/test.py`](../../../nix/linux/tests/vm/suites/workspace-rules/test.py):
|
|
workspace rules sending windows to other workspaces of a real KWin, out of sight on the parking
|
|
desktop until the workspace is shown.
|
|
- [`nix/linux/tests/vm/suites/workspace-switch/test.py`](../../../nix/linux/tests/vm/suites/workspace-switch/test.py):
|
|
workspaces as a real KWin's desktops, switched by `komorebic` and by KWin;
|
|
[`nix/linux/tests/vm/suites/kwin-contract/test.py`](../../../nix/linux/tests/vm/suites/kwin-contract/test.py), the
|
|
behavior of KWin's per-output virtual desktops komorebi relies on, with two outputs and no
|
|
komorebi.
|
|
- [`crates/komorebi-hotkeys/src/lib.rs`](../../../crates/komorebi-hotkeys/src/lib.rs): the key binding model,
|
|
[`crates/komorebi-hotkeys/src/qt.rs`](../../../crates/komorebi-hotkeys/src/qt.rs) the keys, and one module per
|
|
format, [`skhd.rs`](../../../crates/komorebi-hotkeys/src/skhd.rs),
|
|
[`whkd.rs`](../../../crates/komorebi-hotkeys/src/whkd.rs) and
|
|
[`sxhkd.rs`](../../../crates/komorebi-hotkeys/src/sxhkd.rs);
|
|
[`plan.rs`](../../../crates/komorebi-hotkeys/src/plan.rs) and [`kde.rs`](../../../crates/komorebi-hotkeys/src/kde.rs)
|
|
for the shortcuts, [`engine.rs`](../../../crates/komorebi-hotkeys/src/engine.rs) for the engine.
|
|
- [`komorebic/src/linux/hotkeys.rs`](../../../komorebic/src/linux/hotkeys.rs): `export-kde-shortcuts` and
|
|
`hotkeys compile`, and the key bindings section of `komorebic check`;
|
|
[`komorebic/tests/linux_hotkeys.rs`](../../../komorebic/tests/linux_hotkeys.rs), what they write.
|
|
- [`nix/linux/tests/vm/suites/keys-static/test.py`](../../../nix/linux/tests/vm/suites/keys-static/test.py): static
|
|
shortcuts run by a real KWin.
|
|
- [`crates/komorebi-linux/src/kwin/hotkeys.rs`](../../../crates/komorebi-linux/src/kwin/hotkeys.rs): the engine against
|
|
kglobalaccel, and
|
|
[`nix/linux/tests/vm/suites/keys-engine/test.py`](../../../nix/linux/tests/vm/suites/keys-engine/test.py), keys
|
|
pressed in a real KWin with the daemon running;
|
|
[`nix/linux/tests/vm/suites/restart-survival/test.py`](../../../nix/linux/tests/vm/suites/restart-survival/test.py),
|
|
the keys when the daemon and KWin go away.
|
|
- [`crates/komorebi-hotkeys/tests`](../../../crates/komorebi-hotkeys/tests): the corpus test and the comparison
|
|
with `whkd-parser`.
|
|
- [`nix/linux/tests/keybindings.nix`](../../../nix/linux/tests/keybindings.nix): `keybindings-fixtures`, and
|
|
[`nix/linux/tests/vm/keys-corpus.py`](../../../nix/linux/tests/vm/keys-corpus.py), the corpus case `vm`
|
|
pressed in the three formats on a machine the module configured.
|
|
- [`nix/linux/nixos-module.nix`](../../../nix/linux/nixos-module.nix): the NixOS module;
|
|
[`nix/linux/supported-kwin.nix`](../../../nix/linux/supported-kwin.nix), the KWin versions it checks;
|
|
[`nix/linux/tests/nixos-module.nix`](../../../nix/linux/tests/nixos-module.nix), its scenarios, evaluated in
|
|
real NixOS systems; and [`nix/linux/tests/vm/suites/module/test.py`](../../../nix/linux/tests/vm/suites/module/test.py),
|
|
a Plasma session in which the module's unit runs the daemon.
|
|
- [`scripts/linux/compare-gen.py`](../../../scripts/linux/compare-gen.py): writes the tables of
|
|
[COMPARE.md](COMPARE.md) from this checkout and from checkouts of komorebi for Windows and
|
|
komorebi for Mac, and checks them. Continuous integration has neither checkout and does not run
|
|
it.
|
|
- [`scripts/linux/wsl2-probe.sh`](../../../scripts/linux/wsl2-probe.sh), wrapped by
|
|
[`nix/linux/dev/wsl2-probe.nix`](../../../nix/linux/dev/wsl2-probe.nix): probes a NixOS-WSL2 machine for KVM,
|
|
WSLg, a GPU and RDP ([wsl2-probe.md](wsl2-probe.md)). The flake checks `wsl2-probe` (ShellCheck
|
|
over the script and over the guest's init) and `wsl2-probe-dry-run` (the script's `--dry-run` in
|
|
the build sandbox) keep it building.
|
|
|
|
[`nixos-module.nix`]: ../../nix/nixos-module.nix
|
|
[`supported-kwin.nix`]: ../../nix/supported-kwin.nix
|
|
[`StaticConfig`]: ../../komorebi/src/static_config.rs
|
|
[`StaticConfig::preload`]: ../../komorebi/src/static_config.rs
|
|
[`StaticConfig::postload`]: ../../komorebi/src/static_config.rs
|
|
[`StaticConfig::reload`]: ../../komorebi/src/static_config.rs
|
|
[`Workspace::load_static_config`]: ../../komorebi/src/workspace.rs
|
|
[`HOME_DIR`]: ../../komorebi/src/lib.rs
|
|
[`check`]: ../../komorebi/src/check.rs
|
|
[`Finding`]: ../../komorebi/src/check.rs
|
|
[`Level`]: ../../komorebi/src/check.rs
|
|
[`Report`]: ../../komorebi/src/check.rs
|
|
[`Wants`]: ../../komorebi/src/check.rs
|
|
[`Report::render`]: ../../komorebi/src/check.rs
|
|
[`Report::has_errors`]: ../../komorebi/src/check.rs
|
|
[`check_configuration`]: ../../komorebi/src/check.rs
|
|
[`check_environment`]: ../../komorebi/src/check.rs
|
|
[`check_keys`]: ../../komorebi/src/check.rs
|
|
[`known_keys`]: ../../komorebi/src/check.rs
|
|
[`snippet`]: ../../komorebi/src/check.rs
|
|
[`WORKSPACE_MATCHING_RULES`]: ../../komorebi/src/lib.rs
|
|
[`WindowManager::enforce_workspace_rules`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::already_moved_window_handles`]: ../../komorebi/src/window_manager.rs
|
|
[`Monitor::assign_windows_to_workspaces`]: ../../komorebi/src/monitor.rs
|
|
[`LAYOUT_DEFAULTS`]: ../../komorebi/src/lib.rs
|
|
[`IGNORE_IDENTIFIERS`]: ../../komorebi/src/lib.rs
|
|
[`MANAGE_IDENTIFIERS`]: ../../komorebi/src/lib.rs
|
|
[`FLOATING_APPLICATIONS`]: ../../komorebi/src/lib.rs
|
|
[`hotwatch`]: https://crates.io/crates/hotwatch
|
|
[`WindowManager::init`]: ../../komorebi/src/window_manager.rs
|
|
[`Backend`]: ../../komorebi/src/backend/mod.rs
|
|
[`WindowInfo`]: ../../komorebi/src/backend/mod.rs
|
|
[`MonitorInfo`]: ../../komorebi/src/backend/mod.rs
|
|
[`set_backend`]: ../../komorebi/src/backend/mod.rs
|
|
[`backend`]: ../../komorebi/src/backend/mod.rs
|
|
[`with_backend`]: ../../komorebi/src/backend/mod.rs
|
|
[`Look`]: ../../komorebi/src/backend/mod.rs
|
|
[`Backend::apply_look`]: ../../komorebi/src/backend/mod.rs
|
|
[`border_manager`]: ../../komorebi/src/border_manager/mod.rs
|
|
[`border_manager::current_look`]: ../../komorebi/src/border_manager/mod.rs
|
|
[`border_manager::send_notification`]: ../../komorebi/src/border_manager/mod.rs
|
|
[`BORDER_DRAWN`]: ../../komorebi/src/border_manager/mod.rs
|
|
[`animation`]: ../../komorebi/src/animation/mod.rs
|
|
[`KomorebiTheme`]: ../../komorebi/src/core/mod.rs
|
|
[`theme_manager::send_notification`]: ../../komorebi/src/theme_manager.rs
|
|
[`theme_manager::CURRENT_THEME`]: ../../komorebi/src/theme_manager.rs
|
|
[`theme_manager::wallpaper_palette`]: ../../komorebi/src/theme_manager.rs
|
|
[`theme_manager::palette_file`]: ../../komorebi/src/theme_manager.rs
|
|
[`Wallpaper`]: ../../komorebi/src/static_config.rs
|
|
[`Workspace::apply_wallpaper`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::restore`]: ../../komorebi/src/workspace.rs
|
|
[`Backend::set_wallpaper`]: ../../komorebi/src/backend/mod.rs
|
|
[`kwin::wallpaper`]: ../../komorebi/src/kwin/wallpaper.rs
|
|
[`komorebi_themes::generate_base16_palette`]: https://github.com/LGUG2Z/komorebi/tree/f08aa55511d77ca56ab0e789efabeb0a237a463e/komorebi-themes
|
|
[`WindowManager::focus_follows_mouse`]: ../../komorebi/src/window_manager.rs
|
|
[`Backend::set_focus_follows_mouse`]: ../../komorebi/src/backend/mod.rs
|
|
[`Kwinrc::restore_keys`]: ../../komorebi/src/kwin/kwinrc.rs
|
|
[`transparency_manager`]: ../../komorebi/src/transparency_manager.rs
|
|
[`transparency_manager::apply`]: ../../komorebi/src/transparency_manager.rs
|
|
[`transparency_manager::transparent_windows`]: ../../komorebi/src/transparency_manager.rs
|
|
[`transparency_manager::restore`]: ../../komorebi/src/transparency_manager.rs
|
|
[`Backend::set_opacity`]: ../../komorebi/src/backend/mod.rs
|
|
[`WindowManager::opacity_applied`]: ../../komorebi/src/window_manager.rs
|
|
[`animation::movement_animation`]: ../../komorebi/src/animation/mod.rs
|
|
[`animation::movement_style`]: ../../komorebi/src/animation/mod.rs
|
|
[`QtCurve::qt_curve`]: ../../komorebi/src/core/animation.rs
|
|
[`AnimationPrefix`]: ../../komorebi/src/animation/prefix.rs
|
|
[`Backend::set_border_colours`]: ../../komorebi/src/backend/mod.rs
|
|
[`border_manager::window_kinds`]: ../../komorebi/src/border_manager/mod.rs
|
|
[`border_manager::update_colours`]: ../../komorebi/src/border_manager/mod.rs
|
|
[`WindowManager::retile_all`]: ../../komorebi/src/window_manager.rs
|
|
[`Ring`]: ../../komorebi/src/ring.rs
|
|
[`Monitor`]: ../../komorebi/src/monitor.rs
|
|
[`Workspace`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::update`]: ../../komorebi/src/workspace.rs
|
|
[`WorkspaceGlobals`]: ../../komorebi/src/workspace.rs
|
|
[`Container`]: ../../komorebi/src/container.rs
|
|
[`Window`]: ../../komorebi/src/window.rs
|
|
[`Window::should_manage`]: ../../komorebi/src/window.rs
|
|
[`should_act`]: ../../komorebi/src/window.rs
|
|
[`should_act_individual`]: ../../komorebi/src/window.rs
|
|
[`RuleDebug`]: ../../komorebi/src/window.rs
|
|
[`GlobalState`]: ../../komorebi/src/state.rs
|
|
[`MatchingRule`]: ../../komorebi/src/core/config_generation.rs
|
|
[`ApplicationIdentifier`]: ../../komorebi/src/core/mod.rs
|
|
[`Window::set_position`]: ../../komorebi/src/window.rs
|
|
[`Window::hide`]: ../../komorebi/src/window.rs
|
|
[`Window::restore`]: ../../komorebi/src/window.rs
|
|
[`HIDDEN_WINDOWS`]: ../../komorebi/src/lib.rs
|
|
[`Container::add_window`]: ../../komorebi/src/container.rs
|
|
[`Container::hide`]: ../../komorebi/src/container.rs
|
|
[`Container::load_focused_window`]: ../../komorebi/src/container.rs
|
|
[`Container::restore`]: ../../komorebi/src/container.rs
|
|
[`Workspace::move_window_to_container`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::new_container_for_focused_window`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::remove_window`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::focus_container_by_window`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::window_container_behaviour`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::float_override`]: ../../komorebi/src/workspace.rs
|
|
[`WindowManager::add_window_to_container`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::remove_window_from_container`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::stack_all`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::unstack_all`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::restore_all_windows`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::cycle_container_window_in_direction`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::cycle_container_window_index_in_direction`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::focus_container_window`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::window_management_behaviour`]: ../../komorebi/src/window_manager.rs
|
|
[`Backend::hide`]: ../../komorebi/src/backend/mod.rs
|
|
[`Table::hide_commands`]: ../../komorebi/src/kwin/state.rs
|
|
[`Table::restore_commands`]: ../../komorebi/src/kwin/state.rs
|
|
[`Table::window_workspace`]: ../../komorebi/src/kwin/state.rs
|
|
[`Table::stale_park`]: ../../komorebi/src/kwin/state.rs
|
|
[`NeedsPark`]: ../../komorebi/src/kwin/state.rs
|
|
[`Workspace::new_container_for_window`]: ../../komorebi/src/workspace.rs
|
|
[`WindowManager`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::process_event`]: ../../komorebi/src/process_event.rs
|
|
[`WindowManager::update_focused_workspace`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::focus_container_in_direction`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::focus_container_in_cycle_direction`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::mouse_follows_focus`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::move_container_in_direction`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::move_container_in_cycle_direction`]: ../../komorebi/src/window_manager.rs
|
|
[`Workspace::swap_containers`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::layout_flip`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::layout_options`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::resize_dimensions`]: ../../komorebi/src/workspace.rs
|
|
[`WindowManager::resize_window`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::toggle_float`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::toggle_lock`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::toggle_monocle`]: ../../komorebi/src/window_manager.rs
|
|
[`WorkspaceLayer`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::layer`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::floating_layer_behaviour`]: ../../komorebi/src/workspace.rs
|
|
[`WindowManager::focus_floating_window_in_direction`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::focus_floating_window_in_cycle_direction`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::move_floating_window_in_direction`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::toggle_maximize`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::monocle_on`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::monocle_off`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::cycle_monocle`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::monocle_focus_behaviour`]: ../../komorebi/src/window_manager.rs
|
|
[`Workspace::monocle_container`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::new_monocle_container`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::reintegrate_monocle_container`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::maximized_window`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::new_maximized_window`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::reintegrate_maximized_window`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::adopt_maximized_window`]: ../../komorebi/src/workspace.rs
|
|
[`WindowManager::adopt_maximize_change`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowInfo::maximized`]: ../../komorebi/src/backend/mod.rs
|
|
[`WindowInfo::fullscreen`]: ../../komorebi/src/backend/mod.rs
|
|
[`InitialWindowPlacementRules`]: ../../komorebi/src/core/mod.rs
|
|
[`PlacementTarget`]: ../../komorebi/src/core/mod.rs
|
|
[`WindowPlacement`]: ../../komorebi/src/core/mod.rs
|
|
[`Workspace::initial_window_placement_rules`]: ../../komorebi/src/workspace.rs
|
|
[`load_workspace_information`]: ../../komorebi/src/desktop.rs
|
|
[`WindowManager::adjust_workspace_padding`]: ../../komorebi/src/window_manager.rs
|
|
[`Workspace::apply_window_based_work_area_offset`]: ../../komorebi/src/workspace.rs
|
|
[`Monitor::update_workspace_globals`]: ../../komorebi/src/monitor.rs
|
|
[`WindowManager::manage_focused_window`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::unmanage_focused_window`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::is_paused`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::unmanaged_window_operation_behaviour`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::handle_unmanaged_window_behaviour`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManagementBehaviour::toggle_float_placement`]: ../../komorebi/src/core/mod.rs
|
|
[`WindowManager::promote_container_to_front`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::promote_container_swap`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::promote_focus_to_front`]: ../../komorebi/src/window_manager.rs
|
|
[`Workspace::promotion_swap_container_idx`]: ../../komorebi/src/workspace.rs
|
|
[`WindowManager::resize_delta`]: ../../komorebi/src/window_manager.rs
|
|
[`Workspace::latest_layout`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::container_idx_from_current_point`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::contains_managed_window`]: ../../komorebi/src/workspace.rs
|
|
[`WindowManager::pending_move_op`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::transfer_window`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::pending_resize_op`]: ../../komorebi/src/window_manager.rs
|
|
[`Workspace::container_idx_for_window`]: ../../komorebi/src/workspace.rs
|
|
[`Table::drag_origin`]: ../../komorebi/src/kwin/state.rs
|
|
[`Table::drag_end`]: ../../komorebi/src/kwin/state.rs
|
|
[`Backend::drag_end`]: ../../komorebi/src/backend/mod.rs
|
|
[`LiveResize`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::live_resize`]: ../../komorebi/src/window_manager.rs
|
|
[`live_resize_begins`]: ../../komorebi/src/window.rs
|
|
[`send_notification`]: ../../komorebi/src/window_manager_event_listener.rs
|
|
[`LayoutOptions`]: ../../komorebi/src/core/mod.rs
|
|
[`Workspace::tile`]: ../../komorebi/src/workspace.rs
|
|
[`Layout`]: ../../komorebi/src/core/mod.rs
|
|
[`Container::preselect`]: ../../komorebi/src/container.rs
|
|
[`WindowManager::change_workspace_layout_default`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::cycle_layout`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::flip_layout`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::preselect_container_in_direction`]: ../../komorebi/src/window_manager.rs
|
|
[`Workspace::new_idx_for_direction`]: ../../komorebi/src/workspace.rs
|
|
[`Workspace::new_idx_for_cycle_direction`]: ../../komorebi/src/workspace.rs
|
|
[`OperationDirection`]: ../../komorebi/src/core/mod.rs
|
|
[`CycleDirection`]: ../../komorebi/src/core/mod.rs
|
|
[`WindowManagerEvent`]: ../../komorebi/src/window_manager_event.rs
|
|
[`window_manager_event_listener`]: ../../komorebi/src/window_manager_event_listener.rs
|
|
[`listen_for_events`]: ../../komorebi/src/process_event.rs
|
|
[`KWinBackend`]: ../../komorebi/src/kwin/mod.rs
|
|
[`connect`]: ../../komorebi/src/kwin/mod.rs
|
|
[`kwin`]: ../../komorebi/src/kwin/mod.rs
|
|
[`kwin::state`]: ../../komorebi/src/kwin/state.rs
|
|
[`Table`]: ../../komorebi/src/kwin/state.rs
|
|
[`Table::handle`]: ../../komorebi/src/kwin/state.rs
|
|
[`Table::compositor_lost`]: ../../komorebi/src/kwin/state.rs
|
|
[`Table::switch_commands`]: ../../komorebi/src/kwin/state.rs
|
|
[`Table::window_workspace_commands`]: ../../komorebi/src/kwin/state.rs
|
|
[`Kwinrc`]: ../../komorebi/src/kwin/kwinrc.rs
|
|
[`Backend::switch_workspace`]: ../../komorebi/src/backend/mod.rs
|
|
[`Monitor::load_focused_workspace`]: ../../komorebi/src/monitor.rs
|
|
[`WindowManager::sync_workspace_desktops`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::adopt_workspace_change`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::apply_wallpaper_for_monitor_workspace`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::focus_workspace`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::move_container_to_workspace`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::monitor_workspace_index_by_name`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::focus_monitor`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::monitor_idx_in_direction`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::monitor_idx_from_window`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::move_container_to_monitor`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::cross_monitor_move_behaviour`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::cross_boundary_behaviour`]: ../../komorebi/src/window_manager.rs
|
|
[`Monitor::focus_workspace`]: ../../komorebi/src/monitor.rs
|
|
[`Monitor::move_container_to_workspace`]: ../../komorebi/src/monitor.rs
|
|
[`monitor_reconciliator`]: ../../komorebi/src/monitor_reconciliator.rs
|
|
[`MonitorNotification`]: ../../komorebi/src/monitor_reconciliator.rs
|
|
[`MonitorNotification::Resize`]: ../../komorebi/src/monitor_reconciliator.rs
|
|
[`listen_for_notifications`]: ../../komorebi/src/monitor_reconciliator.rs
|
|
[`update_monitor_work_areas`]: ../../komorebi/src/desktop.rs
|
|
[`load_monitor_information`]: ../../komorebi/src/desktop.rs
|
|
[`MonitorNotification::DisplayConnectionChange`]: ../../komorebi/src/monitor_reconciliator.rs
|
|
[`reconcile_display_change`]: ../../komorebi/src/monitor_reconciliator.rs
|
|
[`insert_in_monitor_cache`]: ../../komorebi/src/monitor_reconciliator.rs
|
|
[`kwin::power`]: ../../komorebi/src/kwin/power.rs
|
|
[`MonitorNotification::Wake`]: ../../komorebi/src/monitor_reconciliator.rs
|
|
[`wake_up`]: ../../komorebi/src/monitor_reconciliator.rs
|
|
[`kwin::connect`]: ../../komorebi/src/kwin/mod.rs
|
|
[`WindowManager::evacuate_monitor`]: ../../komorebi/src/reconcile.rs
|
|
[`WindowManager::follow_windows_to_their_monitors`]: ../../komorebi/src/reconcile.rs
|
|
[`settle_later`]: ../../komorebi/src/monitor_reconciliator.rs
|
|
[`NotificationEvent::Monitor`]: ../../komorebi/src/lib.rs
|
|
[`bridge.js`]: ../../komorebi/src/kwin/bridge.js
|
|
[`kwin-api.d.ts`]: ../../komorebi/src/kwin/kwin-api.d.ts
|
|
[`bridge.test.js`]: ../../komorebi/src/kwin/bridge.test.js
|
|
[`fixtures`]: ../../komorebi/src/kwin/fixtures
|
|
[`DATA_DIR`]: ../../komorebi/src/lib.rs
|
|
[`LOG_DIR`]: ../../komorebi/src/lib.rs
|
|
[`SocketMessage`]: ../../komorebi/src/core/mod.rs
|
|
[`StateQuery`]: ../../komorebi/src/core/mod.rs
|
|
[`State`]: ../../komorebi/src/state.rs
|
|
[`State::has_been_modified`]: ../../komorebi/src/state.rs
|
|
[`SubscribeOptions`]: ../../komorebi/src/core/mod.rs
|
|
[`Notification`]: ../../komorebi/src/lib.rs
|
|
[`NotificationEvent`]: ../../komorebi/src/lib.rs
|
|
[`notify_subscribers`]: ../../komorebi/src/lib.rs
|
|
[`SUBSCRIPTION_SOCKETS`]: ../../komorebi/src/lib.rs
|
|
[`SUBSCRIPTION_SOCKET_OPTIONS`]: ../../komorebi/src/lib.rs
|
|
[`WindowManager::new`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::stop`]: ../../komorebi/src/window_manager.rs
|
|
[`WindowManager::process_command`]: ../../komorebi/src/process_command.rs
|
|
[`listen_for_commands`]: ../../komorebi/src/process_command.rs
|
|
[`read_commands_uds`]: ../../komorebi/src/process_command.rs
|
|
[`send_query`]: ../../crates/komorebi-client/src/lib.rs
|
|
[`send_message`]: ../../crates/komorebi-client/src/lib.rs
|
|
[`send_batch`]: ../../crates/komorebi-client/src/lib.rs
|
|
[`subscribe`]: ../../crates/komorebi-client/src/lib.rs
|
|
[`subscribe_with_options`]: ../../crates/komorebi-client/src/lib.rs
|