Files
komorebi/docs/dev/core-model.md
T
LGUG2Z 6a548f4f62 docs: compare the KDE and Windows core model and propose komorebi-core
docs/dev/core-model.md: per-file measured differences between the
Windows, KDE and macOS container, monitor, workspace, window and
window_manager (stripped code lines, changed lines, identical lines),
the platform-specific fields and methods of each layer, a proposed
split into a komorebi-core crate generic over a Platform (Backend plus a
Copy-or-Clone Window and a state type) with the exact order of layers
(container, monitor, workspace, window_manager, router), what the KDE
stack of wip-u-kfde already did, what Windows must do before the crate
can be created, and the list of KDE process_command arms over 20 lines
with the method each became. Measurements are from diff of the files at
this tip; nothing was run on Windows or macOS.

Verified: not applicable (documentation); the numbers were produced by
diff and wc on the files at wip-u-kfde.
2026-10-31 11:09:17 -07:00

11 KiB

The core model: KDE and Windows compared, and the split into komorebi-core

Status: measurement and proposal (plan step 9 preparation, KDE side). Measured at the tip of wip-u-kfde (based on wip-main 633e7453); the Windows tree is komorebi/src, KDE is komorebi-for-kde/komorebi/src, macOS (komorebi-for-mac/komorebi/src) is listed only where it helps. Nothing here has been run on Windows or macOS.

1. Measured differences

"Code lines" drop // lines and blank lines and strip indentation, so KDE's much heavier doc comments do not count; the inline #[cfg(test)] modules are included. "Changed" is the number of </> lines of diff between the two stripped files (an insertion of n lines counts n, a replacement of n lines counts 2n). "Identical" is the number of stripped lines present in both in the same order (a common subsequence, so it is a lower bound on shared text).

file code lines Windows / KDE / macOS changed W vs K changed K vs M changed W vs M identical W&K
container.rs 230 / 254 / 241 196 119 185 144
monitor.rs 643 / 666 / 675 613 205 616 348
workspace.rs 2203 / 2491 / 2196 2314 1097 2113 1190
window.rs 1134 / 916 / 1641 1168 1413 1767 441
window_manager.rs 4716 / 4023 / 4896 4543 1597 5434 2098

Whole-file raw diff sizes (comments included): container 271, monitor 752, workspace 3167, window 1520, window_manager 6358 changed lines (Windows vs KDE). ring.rs is already shared (crates/komorebi-collections, Ring, Lockable).

Two things stand out. First, every file KDE and macOS share is much closer to each other than either is to Windows (K vs M changed is a third to a half of W vs K for monitor, workspace, window_manager, static_config, state). Windows is the odd one out, mostly because it carries older and larger machinery (see section 2). Second, the structs themselves are almost the same: Workspace has 31 fields on both sides and they differ only in the order of work_area_offset_rules/work_area_offset; the differences are in the methods around them.

2. Platform-specific fields and methods, per layer

container (smallest)

  • Same fields (id, locked, windows: Ring<Window>). Windows' windows is private and locked has #[serde(default)]; KDE's windows is pub.
  • Window id type: hwnd: isize (Windows) vs id: u32 (KDE) in contains_window, idx_for_window, hide(omit).
  • Windows only: hwnd_from_exe (KDE keeps only idx_from_exe), Window::restore_with_border / hide_with_border in load_focused_window (Windows' border and stackbar are separate windows). exe() returns Result on Windows, Option on KDE.
  • After the converge revisions of this stack the signatures are the same (add_window(Window), hide(&self, Option<id>), restore(&self), load_focused_window(&mut self), no Result) and the item order is the same. What is left is the id type and the three Windows-only lines above.

monitor

  • Windows only fields: name, device_id, workspace_names: HashMap<usize, String>; serial_number_id is Option<String> (KDE: String); id is isize (KDE u32).
  • Windows only methods: none in the public API; KDE has assign_windows_to_workspaces (it pushes the workspace of each window to the compositor's desktops, Backend::set_window_workspace).
  • Behaviour that differs: Windows load_focused_workspace restores/hides workspaces itself, KDE asks the compositor to switch desktops (Backend::switch_workspace) and tiles what is in front; the Windows update_workspaces_globals pushes padding/border/work area into every workspace (WorkspaceGlobals), KDE calls update_workspace_globals(idx, offset) for the focused one. KDE has a MonitorInformation with 4 fields, Windows 6.
  • BORDER_ENABLED (Windows) vs BORDER_DRAWN (KDE): the compositor draws borders.

workspace

  • Windows only methods: windows_on_show, border_kinds_on_show, hwnd_from_exe.
  • KDE only methods: adopt_maximized_window, focus_floating_window, focus_previous_floating_window, prune_duplicate_windows, reap_invalid_windows, remove_floating_window (my reading, not verified: KDE learns of closes and moves through the bridge snapshot, so the workspace reaps and prunes).
  • 42 of 45/48 pub fn names are common. Windows restore/hide take the monitor handle and wallpaper and do the showing and hiding (WindowsApi, 8 mentions in the file); KDE leaves showing to the compositor (5 backend() calls).
  • Scrolling layout glue (scrolling.rs): 1328 lines KDE, 1315 macOS, ~2200 Windows; measured first, not part of this comparison.

window (not shared: the platform handle)

  • Window { hwnd: isize } (Windows, methods that call WindowsApi, 1134 code lines) vs Window { id: u32 } (KDE, Copy, every method calls backend(), 916 code lines) vs macOS (AX element, application, observer; not Copy, 1641 code lines).
  • Both Windows and KDE Window are Copy handles, which is what lets the container and workspace code hold windows by value. macOS is not (clone).
  • Window is where the split goes: the core must not call a method of Window that does not exist on all three.

window_manager

  • Windows only fields: known_hwnds, virtual_desktop_id, uncloack_to_ignore, has_pending_raise_op, pending_move_op with a handle and rect, already_moved_window_handles of isize.
  • KDE only fields: opacity_applied, minimized_windows, pending_resize_op, live_resize, known_window_ids (a u32 map); pending_move_op has no rect.
  • 43 WindowsApi mentions in Windows' window_manager.rs, 15 backend() in KDE's. Every one of them becomes a Backend call or a Window method in the core.
  • process_command and process_event are not in this table (see section 4 and the plan).

3. Proposed split: komorebi-core

komorebi-core                 (new crate, no platform dependency)
  ring, container, monitor, workspace, window_manager      (generic)
  trait Platform: Backend + { type Window; hooks }         (what is not in Backend)
komorebi-backend              (exists: Backend, WindowId, MonitorId, WindowInfo, Error, Capability)
komorebi (windows) / (kde) / (mac)
  Window (the platform handle), the Backend implementation, event sources, borders, ...

Generic over what:

  • WindowManager<P: Platform>, with Platform::Window: Copy + Eq + Hash + Serialize + Deserialize and fn id(&self) -> WindowId. The three id types (isize, u32, u32) disappear into WindowId (WindowId::from_hwnd/from_u32, already in komorebi-backend). macOS needs Clone instead of Copy: the generic code has to take &P::Window and clone, or macOS keeps a Copy handle and an id registry as the seam doc proposes (docs/dev/seam.md section 7, consequence 1).
  • Backend is a trait object or a type parameter; docs/dev/seam.md and the KDE conversion show it can be the shared trait unchanged, plus KDE's extension (drag_*, look, wallpaper) behind Platform hooks.
  • Methods that are still platform behaviour (Window::hide_with_border, showing and hiding a workspace on Windows vs asking for a desktop on KDE) move behind two hooks: Platform::show_workspace(monitor, idx) and Platform::hide_workspace(...), with the Windows implementation hiding/restoring windows and the KDE one calling Backend::switch_workspace.

Exact order of layers (bottom-up, as in the plan)

  1. ring is done (komorebi-collections).
  2. container (230 lines): needs Window generic and id(). Smallest; do first. Windows must rename hwnd parameters to WindowId or an id() accessor and give exe() the KDE return type (Option<String>) or both use Result.
  3. monitor (643): needs Workspace generic, MonitorId, and the Platform show/hide hooks. Windows' extra fields (name, device_id, workspace_names) stay in the shared struct as Option/default fields or become a P::MonitorExtra associated type; KDE serial_number_id becomes Option<String> with "" mapped to None.
  4. workspace (2203): by far the biggest; the platform split is restore/hide/windows_on_show/ border_kinds_on_show (Windows) and reap/prune/adopt_maximized (KDE). Scrolling glue is measured before it moves.
  5. window_manager (4716): after the first three are shared. The platform fields (section 2) go in a P::State associated type.
  6. the flat router and process_event (separate steps in the plan).

4. What KDE has done (this stack) and what Windows must do before the crate exists

Landed on wip-u-kfde (all native tests green, none of it run on a VM):

  • KDE's Backend extends the shared trait; ids, errors and info types are the shared ones at the trait; the core still holds Window { id: u32 } and converts at the call.
  • container.rs and monitor.rs are in the same order as Windows', and Container has the Windows signatures (add_window(Window), infallible hide, restore, load_focused_window).
  • All process_command arms longer than 20 lines are named methods (list in section 5).

I did not create the shared crate: the Windows tree still has isize handles in Container, Monitor::id: isize, and calls WindowsApi directly in 43 places of window_manager.rs. Windows (kfw) has to:

  1. Finish adopting Backend in window_manager.rs, workspace.rs, monitor.rs (the 8 + 43 + 2 WindowsApi call sites there), so no core file names WindowsApi.
  2. Give Container the same API KDE now has: accept a WindowId (or Window::id()), return Option from exe(), and drop hwnd_from_exe or move it to a platform extension.
  3. Make Monitor.id and serial_number_id the shared types (MonitorId, Option<String>), which changes what komorebic state prints for Windows (a BEHAVIOUR change for the JSON; a test must pin it).
  4. Convert its own multi-line process_command arms (the plan counts 22-23 per platform) into named methods with the same names KDE used (section 5), so the router can later be shared.

5. process_command: arms over 20 lines (KDE, before conversion)

arm lines now
ToggleWorkspaceLayer 90 toggle_workspace_layer()
EagerFocus 75 eager_focus(exe)
ResizeWindowAxis 60 resize_window_axis(axis, sizing)
IgnoreRule 55 add_ignore_rule(identifier, id)
Query 51 answer_query(query, reply)
SessionFloatRule 39 add_session_float_rule()
ReplaceConfiguration 34 replace_configuration(config)
CycleFocusEmptyWorkspace 32 cycle_focus_empty_workspace(direction)
CloseWorkspace 29 close_workspace()
MoveContainerToLastWorkspace 28 move_container_to_last_workspace(follow)
SendContainerToLastWorkspace 27 same method, follow = false
CycleFocusWorkspace 26 cycle_focus_workspace(direction)
LayoutRatios 23 set_layout_ratios(columns, rows)
BorderColour 21 set_border_colour(kind, r, g, b)
NamedWorkspaceRule 21 add_named_workspace_rule(identifier, id, workspace)
InitialNamedWorkspaceRule 21 add_initial_named_workspace_rule(identifier, id, workspace)
ToggleWorkspaceWindowContainerBehaviour 21 toggle_workspace_window_container_behaviour()

All are pub fn name(&mut self, ...) -> eyre::Result<()> in impl WindowManager (process_command.rs), except set_border_colour, which cannot fail.