With the model in place, the window manager can do its job: load the desktop from the backend, tile it, and keep it tiled as the compositor reports what happens to windows. The events are the Windows implementation's, each about one window and carrying it: a window is shown, destroyed, given the focus, minimized or restored. A new window gets a container of its own after the focused one, a closed one leaves whichever workspace has it, a window the user minimizes is out of the layout until it is restored, and every event ends with the workspace in front tiled again. At startup every monitor is loaded, and every open window that is normal, alive and not minimized goes on the monitor its middle is on. An index from each managed window to its monitor and workspace is rebuilt around every event, so that the first event komorebi sees can be the close of a window it picked up at startup. Whether a window is managed is, for now, only whether the compositor calls it a normal window and it is alive; the rules that ignore, manage or float applications come later. When a window closes, komorebi leaves the choice of the next focus to KWin unless KWin focuses nothing, so the two do not fight. The daemon runs this against a backend chosen on the command line. --fake-backend, an in-memory desktop with one monitor, is the only one, and without it komorebi exits with a usage error instead of pretending to manage a desktop it cannot reach.
13 KiB
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, 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, and how an event about a window becomes a tile. It grows with the code: each part of the daemon that is added has its place here.
Vocabulary
- Window: a top-level surface a client has asked the compositor for, named by the
compositor's
u32id. - Monitor: a connected output, with its bounds and its work area (its bounds minus the panels).
- 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.
- Container: what a layout places. It gets one tile, and every window in it is put on that tile.
- Layout: the arrangement (BSP, columns, rows, ...) that turns a work area and a number of containers into one rectangle, a tile, for each container.
- Event: what the compositor reports about a window: it was shown, destroyed, given
the focus, minimized or restored (
WindowManagerEvent). - Compositor: the program that owns the screen and the windows. On KDE Plasma it is KWin.
- Backend: the implementation of the
Backendtrait that talks to a compositor. - Fake:
FakeBackend, the in-memory backend the tests use.
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.
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 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 withalivefalse, 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, minimize it or bring it back, close it, and put the pointer in the middle of a rectangle.
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, and its workspaces. It shows the focused one. - A workspace (
Workspace) has a layout, its padding, and its containers, in order. - A container (
Container) has its windows. A new window gets a container of its own. A container can be locked, which keeps it at its index while others are inserted, removed or swapped around it. - A window (
Window) is aCopyhandle 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 and the
offset (WorkspaceGlobals). The workspace takes the offset and its padding off the work
area, 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, and every open window that is normal, alive and not minimized, on the focused
workspace 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 parts
backend: the trait, the two data types, and the process-wide handle (set_backend,backend,with_backend).backend::fake: the fake.ring: theRing, and the macro that gives every type holding one the same accessors (containers(),focused_container_mut(), ...).monitor,workspace,container,window: the four levels of the tree.lockable_sequence: inserting, removing and swapping in a list where some elements are locked in place, which is how a workspace changes its containers.window_manager: theWindowManagerand the operations on it (tile the workspace in front, tile every monitor, take a minimized window out).desktop: loads the monitors and the open windows from the backend.window_manager_eventandprocess_event: the events, and what the window manager does with each.main.rs: the daemon. It installs the backend chosen on the command line (--fake-backendis the only one), loads and tiles the desktop, and waits to be stopped.
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.monitorsmay 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_rectsays 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.
shutdown,is_window,window_rectandmonitor_from_pointhave defaults, written in terms of the queries. An implementor should not need to override them, exceptshutdownwhen it created something in the compositor that has to be given up.
What is guaranteed, and by what
| Rule | Enforced by | Proof | Escape hatch |
|---|---|---|---|
| A backend implements every query and command | type system: the methods have no defaults | 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 and a_command_about_a_closed_window_is_an_error, against the fake only |
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 and test_locked_containers_remove_window |
containers_mut() is the VecDeque itself |
| 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 |
none |
| A destroyed window leaves whichever workspace 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 a backend | startup check: a usage error, exit code 2 | without_a_backend_komorebi_exits_with_an_error_naming_the_flag |
none |
| Queries never block on the compositor | convention only | none | none |
| Only the backend module reaches the compositor | convention only | none | none |
The last two rows are rules nothing checks. A real backend that waits inside a query breaks the first, and a module that talks to D-Bus directly breaks the second.
One event, end to end
A window is shown, and ends up on a tile:
- The event is a
WindowManagerEvent::Showcarrying the window and the compositor signal it came from. The end-to-end tests build it and callWindowManager::process_event, which hands it tohandle_eventand logs how long that took. handle_eventasksWindow::should_manage, which asks the backend'swindow(id)whether the window is alive and normal. If it is not, the event changes nothing.- 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.
Workspace::new_container_for_windowputs the window in a new container after the focused one, inserted throughLockableSequenceso that a locked container keeps its index, and focuses the new container.WindowManager::update_focused_workspaceasks the focusedMonitorto give its focused workspace the work area, the padding and the offset (WorkspaceGlobals), and callsWorkspace::update.Workspace::updateasks the layout for one tile per container, clamps each to the work area, and callsWindow::set_positionfor each window, which is the backend'sset_rect. The fake puts the window there at once; the frame a window ends up with is whatwindow_rectsays afterwards.- If the new container is the only one, the window is given the focus with the backend's
focus, and the pointer follows it. - The index is rebuilt again, so the next event finds the window.
Where to look in the code
komorebi/src/backend/mod.rs: the trait.komorebi/src/backend/fake.rs: the fake and its tests.komorebi/src/core: the vocabulary the rest is written in.komorebi/src/workspace.rs: tiling, inWorkspace::update.komorebi/src/monitor.rs: the work area, padding and offset a monitor gives the workspace it shows.komorebi/src/process_event.rs: what each event does.komorebi/tests/wm_events.rs: the window manager driven end to end against the fake.