Files
komorebi/docs/dev/core-model.md
T
LGUG2Z e4a5a03b99 docs: review the Workspace hook design against KDE's update and hide
docs/dev/core-model.md section 9: three corrections to section 8 found by
reading KDE's workspace.rs next to the Windows one (begin_update must be
fallible and mutating because KDE prunes duplicates and clamps the focus
first; end_update must return Pass::Again because Windows' update loops
for scrolling; a scrolling_in_motion condition is read mid-update on both
platforms; adjust_layout is per container for the stackbar), the error
policy of place_window, the orphan-rule cost of extension traits, that
WorkspaceConfig is already shared, that Monitor cannot move before
Workspace (it uses about 25 of its members), the visible-windows decision
for the owner, and the order of what remains.

Verified: not applicable (documentation).
2026-10-31 12:14:35 -07:00

485 lines
36 KiB
Markdown

# 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.
## 6. First layer moved: `Container` is in `komorebi-core` (wip-u-core)
Done, on a throwaway merge of `wip-u-kfde` and `wip-u-kfw` (bookmark `wip-u-core`):
- `crates/komorebi-core` (root workspace member; KDE takes it by path) holds `Container<W>`, moved
from the KDE file and made generic, and the trait `ContainerWindow` that says what a container
asks of a platform window: `type Id` (`isize` on Windows, `u32` on KDE), `id`, `exe_name`,
`hide_window`, `restore_window`, and the pair `hide_stacked` / `restore_stacked` that
`load_focused_window` uses (on Windows they leave the border alone, on KDE they are the plain
ones). The container makes no `Backend` call itself.
- KDE and Windows each have `type Container = komorebi_core::Container<Window>` plus the
`ContainerWindow` impl for their `Window`. Windows keeps `hwnd_from_exe` as the extension trait
`ContainerExt` (workspace.rs imports it). The macOS tree does not use the crate yet and still
type-checks.
- The JSON of a container is pinned by a test in each tree and in the crate (`id`, `locked`,
`windows: {elements, focused}`); `locked` may be missing when read (KDE gained that from Windows);
the schemars schema of `Container<W>` is renamed to `Container`, with a test.
- The Windows converge is marked BEHAVIOUR because it was only type-checked (clippy) and the
crate's tests run natively; the Windows e2e scenarios it needs are in that revision's body.
### Where it stops: `Monitor`
After kfw's convergence the Windows and KDE `monitor.rs` still differ by 657 changed code lines
(710 vs 675 lines), and the difference is not mechanical:
- **Fields**: Windows has `name`, `device_id`, `workspace_names: HashMap<usize, String>`;
`serial_number_id` is `Option<String>` (KDE `String`); `id` is a `MonitorId` that the Windows file
serialises through `hmonitor_json` as an `i64` (KDE: a plain `u32`). Making them the same changes
what one daemon's `komorebic state` prints, so it is a decision, not a refactor: either a
`Monitor<P>` with an associated `MonitorExtra` (Windows: the three fields) and `MonitorId` with a
serde adapter per platform, or the extra fields as `Option`s that KDE leaves `None` and skips when
serialising.
- **Methods**: `MonitorInformation` has 6 fields on Windows and 4 on KDE; Windows has the two serde
helpers, KDE has `assign_windows_to_workspaces`. The 16 other public methods are common, but
`load_focused_workspace` (Windows hides and restores the workspaces itself, KDE asks the compositor
for the desktop) and `update_workspaces_globals` / `update_workspace_globals` (Windows pushes the
globals into every workspace, KDE into the focused one) differ in behaviour, and need the
`Platform::show_workspace` / `hide_workspace` hooks of section 3.
- `BORDER_ENABLED` (Windows) vs `BORDER_DRAWN` (KDE) are separate statics.
### What remains, in order
1. `Monitor`: decide the field-set question above, add the two workspace hooks to a `Platform`
trait (the first thing in the crate that is a `Platform` rather than a window), converge the
method bodies, then move. The diff after that decision should be a few dozen lines.
2. `Workspace`: 2314 changed code lines. Needs, besides the hooks, the Windows-only
`windows_on_show` / `border_kinds_on_show` and the KDE-only `reap_invalid_windows`,
`prune_duplicate_windows`, `adopt_maximized_window`, `focus_floating_window`,
`focus_previous_floating_window`, `remove_floating_window` as optional `Platform` hooks or
extension traits; the scrolling glue is measured before it moves.
3. `WindowManager`, then the router and `process_event` (unchanged from section 3).
## 7. Classification of the `workspace.rs` and `monitor.rs` differences
Method: each file is cut into functions (impl-level and free `fn`s, tests apart), comments and blank
lines are dropped, and each function is compared with its namesake on the other side (`diff`; a
changed line counts on both sides). The earlier whole-file figures (workspace 2314, monitor 657)
come from one `diff` of the stripped files, which misaligns after every insertion; per function the
picture is much smaller. Classes: **(a)** cosmetic or ordering drift, **(b)** same behaviour written
differently, **(c)** a real platform difference (a hook is needed), **(d)** a feature one side lacks.
Measured at the tip of `wip-u-core` (Container already moved; the Windows tree is kfw's `wip-u-kfw`).
### `workspace.rs`
| | functions | changed lines |
|---|---|---|
| identical on both sides (non-test) | 36 | 0 |
| (a) drift | 1 | 31 |
| (b) same behaviour, different text | 15 | 196 |
| (c) platform difference | 6 | 356 |
| (d) feature on one side only | 14 | 270 |
| tests present on both sides | 26 | 71 (all (b): the id rename) |
| tests only in KDE | 20 | 275 |
(c) is `update` (161: Windows adds title bars, stackbars, the animation batch, the scrolling
`adopt_actual_widths` pass and `live_resize::is_stepping`; KDE clamps layouts to the work area, has the
percentage override, `scrolling_placed` and `has_settled`), `apply_wallpaper` (110: Windows writes
the wallpaper through `WindowsApi` and builds the palette theme inline, KDE asks the backend and
calls `theme_manager::wallpaper_palette`), `remove_window` (41: Windows deletes the border and
returns `()`; KDE returns the `Window` and unmaximizes it), `hide` (28: KDE re-runs `update` for a
scrolling workspace whose resize dimensions are set), `reintegrate_maximized_window` (12: KDE
unmaximizes and adds through `add_window`; Windows loads the focused window) and
`new_floating_window` (4: KDE unmaximizes).
(d) Windows: `border_kinds_on_show` (34), `windows_on_show` (10), `hwnd_from_exe` (26), custom layouts
in `load_static_config` (23), `promote_container` (5), `effective_layout_options` (1). KDE:
`adopt_maximized_window` (60), `prune_duplicate_windows` (40), `reap_invalid_windows` (33),
`remove_floating_window` (19), `focus_floating_window` (4), `focus_previous_floating_window` (4),
`percentage_within_horizontal_bounds` (11), and the `scrolling_placed` field.
(b) is almost all of: `hwnd: isize` against `window_id: u32` (`contains_window`,
`contains_managed_window`, `container_idx_for_window`, `container_for_window`,
`focus_container_by_window`, `is_focused_window_monocle_or_maximized`, `move_container_to_workspace`
of the monitor), `WindowsApi::foreground_window()` / `cursor_pos()` against `backend()`,
`let _ = backend().hide(WindowId::from_hwnd(..))` against `window.hide()` (also restore, focus,
maximize), `Option` against `Result` for the title, exe and class (`resolve_placement_index`,
`location_from_exe`), `try_into` against `into` (`visible_window_details`),
`new_container_for_window(Window)` against `(&Window) -> Result<()>`, and `by value` against
`&self.maximized_window` patterns.
### `monitor.rs`
| | functions | changed lines |
|---|---|---|
| identical (non-test) | 8 | 0 |
| (b) | 2 | 18 (`move_container_to_workspace`, `add_container`) |
| (c) | 3 | 40 (`load_focused_workspace`, `update_workspaces_globals`, `update_workspace_globals`) |
| (d) | 9 | 113 (the Windows `name`, `device_id` and `workspace_names` fields: `new`, `placeholder`, `From`, `focus_workspace`, the id serde adapter; Custom layouts in `add_container_with_direction`; KDE `assign_windows_to_workspaces`) |
### What each remaining cluster needs
KDE-side converge revisions (this stack): the (a) and (b) clusters where the Windows form is the
better one. What stays, and what kfw does (the list to hand over):
1. **id parameter name and type** (b): KDE keeps `window_id`; kfw renames `hwnd` to `window_id` and
takes the id as the `ContainerWindow::Id` (`isize`) so the text is the same.
2. **Backend calls inline** (b): kfw's `backend().hide(WindowId::from_hwnd(..))` and KDE's
`window.hide()` both become `ContainerWindow::hide_window()` / `restore_window()` (already in the
crate); focus and maximize become `Window::focus` and `Window::maximize` on both. kfw: Windows `Window`
needs `focus`, `maximize`, `unmaximize` that call `backend()`; the Windows result handling
(`let _ =`) moves inside them.
3. **`Option` against `Result`** for title, exe, class, path (b): kfw's `Window::title/exe/class/path`
return `Option` (as KDE) or both return `Result`; recommended `Option`, since Windows never
distinguishes the failures.
4. **`WindowDetails`** (b): `From` (KDE) against `TryFrom` (Windows); kfw makes Windows' infallible
(the `Err` arms were never distinguished).
5. **`new_container_for_window(Window)`** (b): KDE adopts the Windows form in this stack.
6. **`remove_focused_floating_window`** (b): kfw adopts KDE's two-line form over
`remove_floating_window` (which Windows lacks: add it, it is 19 lines).
7. **Error strings** of `reintegrate_maximized_window` (b): KDE's ("no maximized restore index") are
right; Windows' copy the monocle ones. kfw adopts KDE's.
8. **(c) hooks**, to be a `WorkspacePlatform` trait the workspace is generic over: `update` needs
`before_layout` / `after_layout` (Windows: animation batch, `INITIAL_CONFIGURATION_LOADED`, title
bars, stackbar; KDE: clamp, `scrolling_placed`), `place_window(window, rect)` (set_rect with the
top flag against `set_position`), and `apply_wallpaper`; `remove_window` needs `on_window_removed`
(Windows: `border_manager::delete_border`) and a decision on the return value (KDE's `Window` is
what callers need: kfw returns it too); `hide` needs the scrolling step as a hook
(`after_hide`) or moves into KDE only; `reintegrate_maximized_window` and `new_floating_window`
need `unmaximize_window`, a no-op on Windows if its native maximize is not undone there (kfw to
state which).
9. **(d)** Windows-only features become optional hooks or extension traits (`border_kinds_on_show`,
`windows_on_show`, custom layouts) and KDE-only ones likewise (`adopt_maximized_window`,
`prune_duplicate_windows`, `reap_invalid_windows`, floating-window focus helpers,
`scrolling_placed`). Custom layouts exist in `komorebi-layouts` for both; KDE ignores them today
(`Layout::Custom` arms): the question for the owner is whether KDE gains them (a behaviour change)
or the shared code keeps `#[cfg]`-free arms that KDE never reaches.
10. **Monitor fields** (d): `Monitor<P>` with `#[serde(flatten)] platform: P` (decided: the schemars
`required` order of the runtime notification and socket schemas changes, nothing checked in
does).
## 8. Design: the platform hooks of a shared `Workspace` (not implemented)
Status: design from kfw (Windows side), written against `wip-u-core` plus the revisions of
`wip-u-kfw2` (items 1 to 7 of section 7 are done on Windows: `window_id`, `hide_window`/`restore_window`,
`Window::focus`/`maximize`/`unmaximize` over `backend()`, `Option` lookups, `WindowDetails: From`,
`remove_floating_window`, the shared error strings). Nothing below is code; no trait exists yet.
### 8.1 Shape
`Workspace<P: WorkspacePlatform>` lives in `komorebi-core`, like `Container<W>`; `P::Window` is the
`ContainerWindow` (`Copy`, with the `Id`). Windows and KDE implement `WorkspacePlatform` on a unit
struct; the daemon uses the alias `type Workspace = komorebi_core::Workspace<WindowsPlatform>`. Every
hook has a default that does nothing (or the generic behaviour), so a platform lists only its
differences. The trait is a generic parameter, not `dyn`: the calls are in the layout hot path and the
platform is fixed at compile time. All hooks take `&mut Workspace<Self>` or the relevant parts, never
the `WindowManager`, so the layer below `window_manager` stays independent of it.
The window-facing operations are NOT hooks: `focus`, `maximize`, `unmaximize`, `hide_window`,
`restore_window`, `title`/`exe`/`class`/`path` (all `Option`) are methods the `ContainerWindow` bound
(extended with `focus`, `maximize`, `unmaximize`, `title`, `exe`, `class`, `path`) requires, which both
`Window` types now have under these names. Hooks are only for behaviour the shared code cannot express
with window operations.
### 8.2 The (c) hooks
| Where | Hook | Windows does | KDE does | Default |
|---|---|---|---|---|
| `update` start | `fn begin_update(&mut self_ws) -> Flow` | returns `Skip` until `INITIAL_CONFIGURATION_LOADED`; opens the `AnimationBatch` (a guard returned in `Flow::Run(guard)`, dropped at the end of `update`, so every move of the pass animates together) | nothing | `Run(())` |
| `update` per layout | `fn adjust_layout(&self_ws, layouts: &mut [Rect], work_area: &Rect)` | nothing (the borders/padding are already applied by `calculate`) | clamps every layout to the work area, applies the percentage override | no-op |
| `update`, a window gets its rect | `fn place_window(&self_ws, window: &P::Window, rect: &Rect, first: bool) -> eyre::Result<()>` | title-bar removal or restore (`REMOVE_TITLEBARS` and the per-window rules), `set_position(rect, top)` with the top flag set for the maximized and monocle windows, stackbar update for stacks | `set_rect`, `scrolling_placed` bookkeeping | `backend().set_rect` |
| `update` end | `fn end_update(&mut self_ws)` | the scrolling `adopt_actual_widths` pass when animations are on and the user is not stepping a live resize (`live_resize::is_stepping`); stackbar redraw | `has_settled` | no-op |
| `apply_wallpaper` | `fn apply_wallpaper(&self_ws, monitor: &MonitorId, wallpaper: &Wallpaper) -> eyre::Result<()>` | `WindowsApi::set_wallpaper` plus the palette theme built inline | asks the backend and calls `theme_manager::wallpaper_palette` | `Ok(())` (wallpaper is KDE/Windows only: the trait has no wallpaper operation, D36) |
| `remove_window` | `fn on_window_removed(&mut self_ws, id: P::Window::Id)` | `border_manager::delete_border(id)` | nothing | no-op |
| `remove_window` result | none: the shared version returns `eyre::Result<Option<P::Window>>`; Windows callers that used `()` ignore it (KDE's callers need the window to unmaximize it) | | | |
| `hide` | `fn after_hide(&mut self_ws)` | nothing | re-runs `update` for a scrolling workspace whose resize dimensions are set (the windows would otherwise stay where the hidden layout left them) | no-op |
| `reintegrate_maximized_window`, `new_floating_window` | `fn unmaximize_window(w: &P::Window)` | none needed: `Window::unmaximize` is `ShowWindow(SW_NORMAL)`, and `update` re-places the window afterwards; KDE needs the compositor told. Statement for the owner: **Windows does not need to unmaximize there, and does not today** (`reintegrate_maximized_window` loads the focused window; `new_floating_window` does not unmaximize), so the hook is a no-op on Windows and the shared code calls it in both | `window.unmaximize()?` | no-op |
`Flow` for `begin_update` is `Option<Guard>`-like: `enum Flow<G> { Skip, Run(G) }` with
`type UpdateGuard` on the trait, `()` by default. `place_window` carries the whole per-window part of
`update`; the shared `update` computes the layouts, walks containers, asks `place_window` for each
window and nothing else. That is where most of the 161 changed lines of `update` go.
Order of the calls inside `update`: `begin_update` -> (maximized/monocle/floating first: `place_window`) ->
for each container: layouts from `adjust_layout`, `place_window` per visible window ->
`end_update`. Both platforms already order it this way; the Windows `AnimationBatch` guard is dropped
after `end_update` (so `end_update` still sees the batch open, as `adopt_actual_widths` does today).
### 8.3 The (d) features
| Feature | Where | Becomes |
|---|---|---|
| `border_kinds_on_show`, `windows_on_show` (Windows) | `workspace.rs`, read by the border manager and `update` | an extension trait `WorkspaceWindowsExt for Workspace<WindowsPlatform>` in the Windows crate: they only read fields (`containers`, `monocle_container`, `maximized_window`, `floating_windows`), so they need those to be `pub`, which they are |
| `hwnd_from_exe` (Windows) | `ContainerExt` today | stays an extension trait in the Windows crate (`ContainerExt`, `WorkspaceExt::window_id_from_exe`) |
| custom layouts in `load_static_config` and `add_container_with_direction` (Windows) | `Layout::Custom` arms | stay in shared code behind the `Layout` enum, which already has `Custom` in `komorebi-layouts`; KDE's arms for it return the default layout. Open question for the owner (section 7 item 9): whether KDE should gain custom layouts (a KDE behaviour change) |
| `promote_container`, `effective_layout_options` (Windows, 5 and 1 lines) | | shared as they are; KDE gains them unchanged (they are generic) |
| `adopt_maximized_window`, `prune_duplicate_windows`, `reap_invalid_windows`, `percentage_within_horizontal_bounds`, the `scrolling_placed` field (KDE) | | a KDE-only extension trait `WorkspaceKdeExt`; `scrolling_placed` moves into `P::WorkspaceState`, an associated type with `Default + Serialize`-skipping (`#[serde(skip)]`) that the platform owns |
| `focus_floating_window`, `focus_previous_floating_window`, `remove_floating_window`'s focus step (KDE) | | the focus step of `remove_floating_window` is a hook `fn after_floating_removed(&mut self_ws)` (no-op on Windows, which does not refocus), the two helpers become shared methods (they are generic) |
| the Windows-only `Monitor` fields (`name`, `device_id`, `workspace_names`) | `monitor.rs` | section 7 item 10: `Monitor<P>` with `#[serde(flatten)] platform: P`; `P` is a `MonitorExtra`, `()` on KDE |
### 8.4 What this needs first
1. `ContainerWindow` (or a `WorkspaceWindow: ContainerWindow` supertrait) grows `focus`, `maximize`,
`unmaximize`, `title`, `exe`, `class`, `path`. Both `Window`s have them with the same signatures
after the revisions above (`focus` returns `eyre::Result<()>`, the others `()`/`Option`).
2. `WindowDetails` and `WindowKind`-keyed border data are already plain data; `Wallpaper` and
`WorkspaceLayer` are in `komorebi-protocol`.
3. The statics that `update` reads (`REMOVE_TITLEBARS`, `INITIAL_CONFIGURATION_LOADED`, the padding and
border globals) are read inside the hooks, not in shared code. `WorkspaceGlobals` (padding, border
offset/width, work-area offset) is plain data and is the only global state the shared `Workspace`
takes, through `update_globals` as today.
4. `border_manager` and `stackbar_manager` stay in the Windows crate and are reached only from
`place_window`, `on_window_removed` and `end_update`.
### 8.5 Order of work and gates
(1) the `ContainerWindow` extension (pure), (2) move the identical and (b) functions of `workspace.rs`
into the crate generic over the window, keeping the hooks as direct calls under `#[cfg]` for one
revision each (pure move), (3) introduce `WorkspacePlatform` with the defaults and implement it for
both platforms (behaviour-preserving on both), one hook per revision in the order of the table:
`on_window_removed`, `apply_wallpaper`, `place_window`, `begin_update`/`end_update`, `after_hide`,
`unmaximize_window`. Gates: Windows `commands`, `workspaces`, `stacking`, `scrolling`, `resize`,
`borders`, `animation` scenarios per hook; KDE fast tier and VM for `adjust_layout`, `after_hide`,
`unmaximize_window`.
## 9. Review of section 8 against KDE's code, and the order that follows from it
Read against `komorebi-for-kde/komorebi/src/workspace.rs` (`update`, `hide`, `remove_window`) and the
Windows `update`. Section 8 holds, with these corrections and additions; none changes the idea, but
three change a signature.
1. **`begin_update` must be fallible and may change the workspace.** KDE's `update` starts with
`self.prune_duplicate_windows()?` and then clamps `focused_container_idx` to the container count.
Both are before anything is laid out, so they belong in `begin_update(&mut ws) ->
eyre::Result<Flow<G>>`, not in the shared `update`. (Windows has no such step.)
2. **`end_update` must say whether to run the pass again.** The Windows `update` is a loop: after the
placement pass, `if pass == 0 && is_scrolling && !animations_enabled() && adopt_actual_widths(..)
{ pass += 1; continue; }`. A hook that returns nothing cannot express that; it must be
`end_update(&mut ws) -> eyre::Result<Pass>` with `enum Pass { Done, Again }`, and the shared `update`
loops while it answers `Again` (at most one repeat, as Windows does; KDE always says `Done`).
3. **There is one more condition read in the middle of `update`, a hook of its own.** Where the scrolling
layout decides whether the pass is animated, Windows reads `crate::scrolling::animations_enabled() &&
!live_resize::is_stepping()` and KDE reads `scrolling_placed.is_some_and(Placed::has_settled) &&
!live_resize_in_progress()`. Section 8 files `has_settled` under `end_update`, but KDE reads it before
the layouts are computed. Add `fn scrolling_in_motion(&ws) -> bool` (default `false`); `scrolling_placed`
is set at the end of KDE's pass, in `end_update`.
4. **Error policy of `place_window` differs** and the hook returns `Result` to carry both: Windows
propagates a failed `set_rect` with `?`, KDE logs it (`tracing::warn!("failed to set window
position")`) and goes on, while `center(..)?` in the percentage override does propagate. The KDE
`place_window` swallows exactly the `set_position` error; the shared caller always uses `?`.
5. **`adjust_layout` is not enough for Windows' stackbar**: it subtracts the stackbar height from the
layout of a container with more than one window (`should_have_stackbar(window_count)`). That is a
per-container adjustment, so `adjust_layout` is called per container with its index,
`adjust_layout(&ws, idx: usize, layout: &mut Rect, work_area: &Rect)`, not once with the slice; KDE's
clamp is per layout as well and only when the workspace is not scrolling.
6. **KDE wraps the placement in a closure** (`let result = (|| { .. })(); result?;`) with nothing between the closure and the `?`, so it is equivalent to no closure (class (a) drift: the closure only keeps the `?`s of the placement local). The shared `update` needs no such shape; a Windows `AnimationBatch` guard drops on any exit and is compatible with either.
7. **Orphan rule:** a method only one platform has cannot be an inherent method of the generic
`Workspace<P>` in the crate. `adopt_maximized_window`, `prune_duplicate_windows` and
`reap_invalid_windows` (KDE), `border_kinds_on_show`, `windows_on_show` (Windows) become extension
traits, and each file that calls them imports the trait (6 call sites of the KDE three outside
`workspace.rs` today). The extension traits are the price of keeping the `Workspace` fields `pub`.
8. **`WorkspaceConfig` and `Wallpaper` are already shared** (`komorebi-config`, `komorebi-protocol`), so
`load_static_config` is not blocked by the daemon's `static_config.rs`; it can move into the crate
(the Windows custom-layout arms stay in it).
9. **`Monitor` cannot move before `Workspace`.** The shared Monitor code uses about 25 members of its
workspace (`layout`, `globals`, `tile`, `layer`, `containers_mut`, `add_container_to_back`,
`insert_container_at_idx`, `floating_windows_mut`, `reintegrate_monocle_container`, ...). Once
`Workspace<P>` exists those are inherent members and `Monitor<P>` takes `Ring<Workspace<P>>`
directly; before that, `Monitor` would need the 25-member trait that was ruled out. The order is
therefore: `WorkspaceGlobals` (plain data), `Workspace<P>`, `Monitor<P>`; the request to do `Monitor`
first is only possible after the first.
10. **Decision for the owner, `visible-windows`.** Windows' `visible_window_details` used `try_into` and
skipped a window whose fields could not be read; the shared `visible_window_details` uses `into`, so
after kfw's revision 4 Windows lists such a window with empty strings in `komorebic visible-windows`.
The shared code depends on the infallible form; the alternative is a `WindowDetails: TryFrom`
shared with a skip for `Err`, which KDE (always `Ok`) would not notice. Recommended: keep `into`.
### Order of the code that remains (all gated like section 8.5)
1. `WorkspaceGlobals` into `komorebi-core` (identical plain data; this stack does it).
2. `Workspace<P>` struct and the 50 identical functions, `P::Window: WorkspaceWindow`
(`ContainerWindow` plus `focus`, `maximize`, `unmaximize`, `title`, `exe`, `class`, `path`, `rect`,
`center`, `set_position`), the differing functions as hooks or extension traits as in sections 8
and 9. About 2000 lines move; every call site that used an inherent method of a one-platform
function imports the extension trait. This is one revision per cluster of functions, and it is
the first step that cannot be verified on Windows without the e2e scenarios.
3. `Monitor<P>` with `#[serde(flatten)] platform: P`.