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).
36 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'windowsis private andlockedhas#[serde(default)]; KDE'swindowsispub. - Window id type:
hwnd: isize(Windows) vsid: u32(KDE) incontains_window,idx_for_window,hide(omit). - Windows only:
hwnd_from_exe(KDE keeps onlyidx_from_exe),Window::restore_with_border/hide_with_borderinload_focused_window(Windows' border and stackbar are separate windows).exe()returnsResulton Windows,Optionon 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), noResult) 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_idisOption<String>(KDE:String);idisisize(KDEu32). - 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_workspacerestores/hides workspaces itself, KDE asks the compositor to switch desktops (Backend::switch_workspace) and tiles what is in front; the Windowsupdate_workspaces_globalspushes padding/border/work area into every workspace (WorkspaceGlobals), KDE callsupdate_workspace_globals(idx, offset)for the focused one. KDE has aMonitorInformationwith 4 fields, Windows 6. BORDER_ENABLED(Windows) vsBORDER_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 fnnames are common. Windowsrestore/hidetake the monitor handle and wallpaper and do the showing and hiding (WindowsApi, 8 mentions in the file); KDE leaves showing to the compositor (5backend()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 callWindowsApi, 1134 code lines) vsWindow { id: u32 }(KDE,Copy, every method callsbackend(), 916 code lines) vs macOS (AX element, application, observer; notCopy, 1641 code lines).- Both Windows and KDE
WindowareCopyhandles, which is what lets the container and workspace code hold windows by value. macOS is not (clone). Windowis where the split goes: the core must not call a method ofWindowthat 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_opwith a handle and rect,already_moved_window_handlesofisize. - KDE only fields:
opacity_applied,minimized_windows,pending_resize_op,live_resize,known_window_ids(au32map);pending_move_ophas no rect. - 43
WindowsApimentions in Windows'window_manager.rs, 15backend()in KDE's. Every one of them becomes aBackendcall or aWindowmethod in the core. process_commandandprocess_eventare 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>, withPlatform::Window: Copy + Eq + Hash + Serialize + Deserializeandfn id(&self) -> WindowId. The three id types (isize,u32,u32) disappear intoWindowId(WindowId::from_hwnd/from_u32, already inkomorebi-backend). macOS needsCloneinstead ofCopy: the generic code has to take&P::Windowand clone, or macOS keeps aCopyhandle and an id registry as the seam doc proposes (docs/dev/seam.md section 7, consequence 1).Backendis a trait object or a type parameter;docs/dev/seam.mdand the KDE conversion show it can be the shared trait unchanged, plus KDE's extension (drag_*, look, wallpaper) behindPlatformhooks.- 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)andPlatform::hide_workspace(...), with the Windows implementation hiding/restoring windows and the KDE one callingBackend::switch_workspace.
Exact order of layers (bottom-up, as in the plan)
- ring is done (
komorebi-collections). - container (230 lines): needs
Windowgeneric andid(). Smallest; do first. Windows must renamehwndparameters toWindowIdor anid()accessor and giveexe()the KDE return type (Option<String>) or both useResult. - monitor (643): needs
Workspacegeneric,MonitorId, and thePlatformshow/hide hooks. Windows' extra fields (name,device_id,workspace_names) stay in the shared struct asOption/default fields or become aP::MonitorExtraassociated type; KDEserial_number_idbecomesOption<String>with""mapped toNone. - workspace (2203): by far the biggest; the platform split is
restore/hide/windows_on_show/ border_kinds_on_show(Windows) andreap/prune/adopt_maximized(KDE). Scrolling glue is measured before it moves. - window_manager (4716): after the first three are shared. The platform fields (section 2) go
in a
P::Stateassociated type. - 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
Backendextends the shared trait; ids, errors and info types are the shared ones at the trait; the core still holdsWindow { id: u32 }and converts at the call. container.rsandmonitor.rsare in the same order as Windows', andContainerhas the Windows signatures (add_window(Window), infalliblehide,restore,load_focused_window).- All
process_commandarms 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:
- Finish adopting
Backendinwindow_manager.rs,workspace.rs,monitor.rs(the 8 + 43 + 2WindowsApicall sites there), so no core file namesWindowsApi. - Give
Containerthe same API KDE now has: accept aWindowId(orWindow::id()), returnOptionfromexe(), and drophwnd_from_exeor move it to a platform extension. - Make
Monitor.idandserial_number_idthe shared types (MonitorId,Option<String>), which changes whatkomorebic stateprints for Windows (a BEHAVIOUR change for the JSON; a test must pin it). - Convert its own multi-line
process_commandarms (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) holdsContainer<W>, moved from the KDE file and made generic, and the traitContainerWindowthat says what a container asks of a platform window:type Id(isizeon Windows,u32on KDE),id,exe_name,hide_window,restore_window, and the pairhide_stacked/restore_stackedthatload_focused_windowuses (on Windows they leave the border alone, on KDE they are the plain ones). The container makes noBackendcall itself.- KDE and Windows each have
type Container = komorebi_core::Container<Window>plus theContainerWindowimpl for theirWindow. Windows keepshwnd_from_exeas the extension traitContainerExt(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});lockedmay be missing when read (KDE gained that from Windows); the schemars schema ofContainer<W>is renamed toContainer, 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_idisOption<String>(KDEString);idis aMonitorIdthat the Windows file serialises throughhmonitor_jsonas ani64(KDE: a plainu32). Making them the same changes what one daemon'skomorebic stateprints, so it is a decision, not a refactor: either aMonitor<P>with an associatedMonitorExtra(Windows: the three fields) andMonitorIdwith a serde adapter per platform, or the extra fields asOptions that KDE leavesNoneand skips when serialising. - Methods:
MonitorInformationhas 6 fields on Windows and 4 on KDE; Windows has the two serde helpers, KDE hasassign_windows_to_workspaces. The 16 other public methods are common, butload_focused_workspace(Windows hides and restores the workspaces itself, KDE asks the compositor for the desktop) andupdate_workspaces_globals/update_workspace_globals(Windows pushes the globals into every workspace, KDE into the focused one) differ in behaviour, and need thePlatform::show_workspace/hide_workspacehooks of section 3. BORDER_ENABLED(Windows) vsBORDER_DRAWN(KDE) are separate statics.
What remains, in order
Monitor: decide the field-set question above, add the two workspace hooks to aPlatformtrait (the first thing in the crate that is aPlatformrather than a window), converge the method bodies, then move. The diff after that decision should be a few dozen lines.Workspace: 2314 changed code lines. Needs, besides the hooks, the Windows-onlywindows_on_show/border_kinds_on_showand the KDE-onlyreap_invalid_windows,prune_duplicate_windows,adopt_maximized_window,focus_floating_window,focus_previous_floating_window,remove_floating_windowas optionalPlatformhooks or extension traits; the scrolling glue is measured before it moves.WindowManager, then the router andprocess_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 fns, 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):
- id parameter name and type (b): KDE keeps
window_id; kfw renameshwndtowindow_idand takes the id as theContainerWindow::Id(isize) so the text is the same. - Backend calls inline (b): kfw's
backend().hide(WindowId::from_hwnd(..))and KDE'swindow.hide()both becomeContainerWindow::hide_window()/restore_window()(already in the crate); focus and maximize becomeWindow::focusandWindow::maximizeon both. kfw: WindowsWindowneedsfocus,maximize,unmaximizethat callbackend(); the Windows result handling (let _ =) moves inside them. OptionagainstResultfor title, exe, class, path (b): kfw'sWindow::title/exe/class/pathreturnOption(as KDE) or both returnResult; recommendedOption, since Windows never distinguishes the failures.WindowDetails(b):From(KDE) againstTryFrom(Windows); kfw makes Windows' infallible (theErrarms were never distinguished).new_container_for_window(Window)(b): KDE adopts the Windows form in this stack.remove_focused_floating_window(b): kfw adopts KDE's two-line form overremove_floating_window(which Windows lacks: add it, it is 19 lines).- Error strings of
reintegrate_maximized_window(b): KDE's ("no maximized restore index") are right; Windows' copy the monocle ones. kfw adopts KDE's. - (c) hooks, to be a
WorkspacePlatformtrait the workspace is generic over:updateneedsbefore_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 againstset_position), andapply_wallpaper;remove_windowneedson_window_removed(Windows:border_manager::delete_border) and a decision on the return value (KDE'sWindowis what callers need: kfw returns it too);hideneeds the scrolling step as a hook (after_hide) or moves into KDE only;reintegrate_maximized_windowandnew_floating_windowneedunmaximize_window, a no-op on Windows if its native maximize is not undone there (kfw to state which). - (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 inkomorebi-layoutsfor both; KDE ignores them today (Layout::Customarms): 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. - Monitor fields (d):
Monitor<P>with#[serde(flatten)] platform: P(decided: the schemarsrequiredorder 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
ContainerWindow(or aWorkspaceWindow: ContainerWindowsupertrait) growsfocus,maximize,unmaximize,title,exe,class,path. BothWindows have them with the same signatures after the revisions above (focusreturnseyre::Result<()>, the others()/Option).WindowDetailsandWindowKind-keyed border data are already plain data;WallpaperandWorkspaceLayerare inkomorebi-protocol.- The statics that
updatereads (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 sharedWorkspacetakes, throughupdate_globalsas today. border_managerandstackbar_managerstay in the Windows crate and are reached only fromplace_window,on_window_removedandend_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.
begin_updatemust be fallible and may change the workspace. KDE'supdatestarts withself.prune_duplicate_windows()?and then clampsfocused_container_idxto the container count. Both are before anything is laid out, so they belong inbegin_update(&mut ws) -> eyre::Result<Flow<G>>, not in the sharedupdate. (Windows has no such step.)end_updatemust say whether to run the pass again. The Windowsupdateis 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 beend_update(&mut ws) -> eyre::Result<Pass>withenum Pass { Done, Again }, and the sharedupdateloops while it answersAgain(at most one repeat, as Windows does; KDE always saysDone).- 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 readscrate::scrolling::animations_enabled() && !live_resize::is_stepping()and KDE readsscrolling_placed.is_some_and(Placed::has_settled) && !live_resize_in_progress(). Section 8 fileshas_settledunderend_update, but KDE reads it before the layouts are computed. Addfn scrolling_in_motion(&ws) -> bool(defaultfalse);scrolling_placedis set at the end of KDE's pass, inend_update. - Error policy of
place_windowdiffers and the hook returnsResultto carry both: Windows propagates a failedset_rectwith?, KDE logs it (tracing::warn!("failed to set window position")) and goes on, whilecenter(..)?in the percentage override does propagate. The KDEplace_windowswallows exactly theset_positionerror; the shared caller always uses?. adjust_layoutis 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, soadjust_layoutis 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.- 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 sharedupdateneeds no such shape; a WindowsAnimationBatchguard drops on any exit and is compatible with either. - 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_windowsandreap_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 outsideworkspace.rstoday). The extension traits are the price of keeping theWorkspacefieldspub. WorkspaceConfigandWallpaperare already shared (komorebi-config,komorebi-protocol), soload_static_configis not blocked by the daemon'sstatic_config.rs; it can move into the crate (the Windows custom-layout arms stay in it).Monitorcannot move beforeWorkspace. 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, ...). OnceWorkspace<P>exists those are inherent members andMonitor<P>takesRing<Workspace<P>>directly; before that,Monitorwould need the 25-member trait that was ruled out. The order is therefore:WorkspaceGlobals(plain data),Workspace<P>,Monitor<P>; the request to doMonitorfirst is only possible after the first.- Decision for the owner,
visible-windows. Windows'visible_window_detailsusedtry_intoand skipped a window whose fields could not be read; the sharedvisible_window_detailsusesinto, so after kfw's revision 4 Windows lists such a window with empty strings inkomorebic visible-windows. The shared code depends on the infallible form; the alternative is aWindowDetails: TryFromshared with a skip forErr, which KDE (alwaysOk) would not notice. Recommended: keepinto.
Order of the code that remains (all gated like section 8.5)
WorkspaceGlobalsintokomorebi-core(identical plain data; this stack does it).Workspace<P>struct and the 50 identical functions,P::Window: WorkspaceWindow(ContainerWindowplusfocus,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.Monitor<P>with#[serde(flatten)] platform: P.