Adds section 7 (the seven consequences of plan step 7, each with its supporting citations and a status: CONFIRMED, CORRECTED or TARGET; plus three findings the plan did not list: a batch/transaction scope, the workspace methods that exist only on KDE, and error-type differences) and section 8 (everything not confirmed, with how to confirm it on a Mac, a Windows guest or by reading). Verified against the citations of sections 2 to 6 on wip-main aa52fdc4d9e5. Not verified: see section 8.
76 KiB
The platform seam: evidence
Status: evidence document (step 7a of the consolidation). No Backend trait is proposed here as code, and none may be written before this document has been reviewed. Every statement about the code carries a citation; where I could not confirm something it says "not verified" and the item is collected in section 8.
1. Purpose and how to read it
A shared Backend seam has to sit between code that is to be shared (windows, containers, workspaces, monitors, commands, event handling) and three platform layers that today do the same jobs in three different ways. A trait written from memory of one platform would bake that platform's assumptions into the other two. This document records what each platform actually does, so that the trait can be derived from facts.
- Facts are the tables and the numbered findings, each with a
file:linecitation read on the tips below. Nothing in a table is a proposal. - Proposals are confined to section 7 (consequences). Each consequence is a statement about what the seam must satisfy, with the facts that support it, and is marked CONFIRMED, CORRECTED or TARGET (not true of any current code, but something the seam would have to make true).
- "Not verified" means I read the code and could not settle the point by reading (usually: Apple or Microsoft runtime behaviour, or behaviour that needs a Mac). It does not mean "false".
Tips read: integration line wip-main at aa52fdc4d9e5 (build(nix): relock the nested flakes after the step 1 and crates changes). The three trees live in one checkout:
| Tree | Prefix used below | Path |
|---|---|---|
| Windows (repo root) | W/ |
komorebi/src/ |
| KDE | K/ |
komorebi-for-kde/komorebi/src/ |
| macOS | M/ |
komorebi-for-mac/komorebi/src/ |
Shared crates are cited with their full path (komorebi-layouts/src/rect.rs). Only reading and grep were used; nothing was compiled or run, and no macOS or Windows runtime behaviour was observed.
Corrections to earlier claims
Things in the plan (generic-percolating-matsumoto.md, Step 7 and "Risks") or the progress log that this reading contradicts or refines.
- KDE
Backendmethod count. The trait declares 31 methods: 19 required (K/backend/mod.rs:242-258,325-389,408-432) and 12 provided (:280,:311,:473,:504,:529,:553,:586,:597,:607,:612,:617,:622). The plan's "28 (not 31)" matches only if the three derived helpersis_window,window_rect,monitor_from_point(:612-628, written in terms ofwindowandmonitors) are left out. Use "28 primitives plus 3 derived". - macOS
monitors()is not a cache (already corrected by the kfm A0 report, confirmed).MacosApi::latest_monitor_informationqueriesNSScreenlive and doesMainThreadMarker::new().unwrap()(M/macos_api.rs:212-213), so it panics off the main thread; every caller is already on main (section 3.3). "monitors() reads a cache refreshed by the display callback" is a TARGET (consequence 4). - Consequence 2 is too strong as written. "
set_rectis never read back to confirm" is false of macOS: after a direct write the layout reads the frame back to learn whether the app refused the width (M/window.rs:944-972,M/arrival.rs:1-12), and the animator verifies each journey (M/animation/animator.rs:902). KDE returns an optimistic value:set_rectwrites the requested rect into its own table at once (K/kwin/state.rs:596-600, called fromK/kwin/mod.rs:855). The defensible contract is in section 7. - The KDE
u32id is not "the compositor's". The doc comments say so (K/backend/mod.rs:65,K/window.rs:78-81), but the id is a counter that the daemon assigns to each KWin UUID string (K/kwin/state.rs:153-154,603-611). KDE already has a registry (uuid <-> id) behind its backend, andKWinBackend::commandresolves every command through it (K/kwin/mod.rs:797-806). - The macOS deadlock has more entry points than the plan lists. The plan names
destroy_all_bordersfrom the event thread (M/process_event.rs:128, 139). A blocking main-queue dispatch under the window-manager lock also happens from the event thread atM/process_event.rs:791(activate_finder,M/macos_api.rs:492), and from command threads (M/process_command.rs:294, 303, 1776, 1802, 1806) and workspace switching (M/window_manager.rs:1385,2286). Section 3.3. - Windows HWND width: the code never narrows an HWND, so it assumes nothing about 32-bit significance (section 4). The Microsoft-documented fact that handles are 32-bit significant is not verified here.
- Counts (re-measured; method in section 2.3). KDE trait: 31 (not 28, see 1). Windows
WindowsApi: 71 distinct functions used outsidewindows_api.rs(matches), 280 call sites inW/plus 3 inkomorebi-bar/src/bar.rs= 283 (earlier survey 279). The generic / Windows-only split depends entirely on the rule used; with the rule stated in 2.3 it is 41 / 30 functions and 219 / 61 sites (earlier survey 42 / 29 and 209 / 70). macOS: 14MacosApifunctions, 5 of 8CoreGraphicsApifunctions, 13AccessibilityApifunctions are used outside their API files; the earlier "52 raw AX sites, 33 in window.rs" does not reproduce for the total: 67AccessibilityApi::sites in 12 files, 33 of them inM/window.rs(that part agrees). - The plan's list of macOS main-thread-only work is incomplete in one respect. The AX observer and event-tap callbacks run on main and do synchronous AX IPC to other applications there (
M/window.rs:288-350,M/input_event_listener.rs:96-100): "main thread" is not just "cheap UI work". - macOS has no focus-follows-mouse implementation, only config/state fields (
grepfindsFocusFollowsMouseinM/static_config.rs,M/state.rs,M/core/mod.rsand nowhere else); KDE and Windows both have one. The plan lists focus-follows-mouse among platform subsystems; for macOS it is absent, not platform-specific.
2. The operation table
Conventions. "Rect" is the shared komorebi_layouts::Rect: left, top are the origin and right, bottom hold the width and height, not the far edge (komorebi-layouts/src/rect.rs:19-28; the conversions From<RECT> at :32-41, From<CGRect> at :67-76). "Queued" means the call returns before the platform has acted; "sync" means the calling thread waits for the platform to answer. Row numbers are for reference in later sections.
2.1 Operations present on at least two platforms
| # | Operation | KDE Backend method |
Windows | macOS | Semantic differences |
|---|---|---|---|---|---|
| 1 | Set frame | set_rect(id, &Rect) K/backend/mod.rs:325; KWinBackend::set_rect K/kwin/mod.rs:852-857 |
Window::set_position(&Rect, top) W/window.rs:374; WindowsApi::position_window W/windows_api.rs:501-555 (SetWindowPos :639-652); move_window :655-666 |
Window::set_position(&Rect) M/window.rs:915; direct path set_position_direct = set_point then set_size, two AX writes M/window.rs:1050-1058, 1148-1184; animated path animator::retarget M/window.rs:1061-1082 |
KDE: queued. A geo command is pushed (command_rect K/kwin/mod.rs:786-788) and carried out as the answer to a held Poll (:268-300, 1 ms coalescing :141); the requested rect is written into the table at once (K/kwin/state.rs:596-600). Windows: sync by default (WINDOW_HANDLING_BEHAVIOUR default Sync, W/lib.rs:266-267, W/core/mod.rs:816-822); with Async and with_async_window_pos the call adds ASYNC_WINDOW_POS (W/windows_api.rs:521-528). macOS: sync AX round trips (M/animation/animator.rs:6-7) when animation is off; queued to the animator lane when on. Rect: Windows adds the DWM shadow offset before the call (W/windows_api.rs:534-540, shadow_rect :808-821); KDE sets frameGeometry (K/kwin/bridge.js:378-381); macOS writes AX position and size as written. A client may refuse the size (documented for KDE K/backend/mod.rs:319-320; macOS reads it back, M/arrival.rs:1-12). Errors: KDE only for an unknown or dead id (K/kwin/mod.rs:797-806); Windows returns the SetWindowPos error (.process() :651); macOS returns the AX error and a failed write asks the reaper whether the window is gone (M/window.rs:1155-1162, M/reaper.rs:164-173). |
| 2 | Get frame | window_rect(id) (provided) K/backend/mod.rs:617-619 over window(id) :245 |
WindowsApi::window_rect(hwnd) W/windows_api.rs:789-802 |
MacosApi::window_rect(&AXUIElement) -> CGRect M/macos_api.rs:385-427 |
KDE: from the daemon's table, never waits on KWin; holds the optimistic requested rect until a geometry event replaces it (K/kwin/state.rs:594-600). Windows: visible bounds (DWMWA_EXTENDED_FRAME_BOUNDS, without the shadow) with fallback to GetWindowRect (includes shadow) (:792-801). macOS: AX position and size, two sync IPC calls, errors logged and returned (:386-410). macOS takes an element, not an id. |
| 3 | Focus | focus(id, mouse_follows_focus) K/backend/mod.rs:332; K/kwin/mod.rs:859-880 |
Window::focus W/window.rs:591-622 -> WindowsApi::raise_and_focus_window W/windows_api.rs:722-747 |
Window::focus M/window.rs:1084-1139 |
KDE queues an activate, then (if mouse follows) spawns a task that polls up to 60 x 5 ms for the window to be foreground before moving the pointer (:864-877). Windows: a synthetic SendInput to pass the foreground lock, SetWindowPos (error ignored), then SetForegroundWindow, whose failure is the returned error; skips the work when already foreground (W/window.rs:599-607). macOS: tries the private SkyLight set_front_process, else NSRunningApplication::activateWithOptions (M/window.rs:1089-1102, M/skylight.rs:206), then sets AXMain (:1131); tabbed apps use main_window() (:1105-1129). Pointer centring reads the frame back over AX. |
| 4 | Raise | raise(id) K/backend/mod.rs:339; K/kwin/mod.rs:882-884 |
Window::raise W/window.rs:791-798 -> raise_window W/windows_api.rs:563-583 |
Window::raise M/window.rs:1141-1146 |
Windows raises without activating (SetWindowPos(HWND_TOP, NOACTIVATE)) and also raises the window's border. macOS has no raise-without-focus: it sets AXMain and AXFocused (:1144-1145). |
| 5 | Minimize / unminimize | minimize(id), unminimize(id) K/backend/mod.rs:368, 375; K/kwin/mod.rs:972-978 |
Window::minimize W/window.rs:556-561 (skips an exe containing komorebi-bar) -> minimize_window W/windows_api.rs:686; unminimize is restore_window (SW_SHOWNOACTIVATE) :706-708 |
Window::minimize/unminimize set AXMinimized true/false M/window.rs:794-805 |
Windows has no separate unminimize, and show_window ignores the result (W/windows_api.rs:668-684, "TODO: error handling"; ShowWindowAsync when Async), so these return (). KDE and macOS return Result. |
| 6 | Hide / restore (back of a stack, other workspace) | hide(id), restore(id) K/backend/mod.rs:353, 361; K/kwin/mod.rs:886-925 |
Window::hide_with_border/restore_with_border W/window.rs:488-554 |
Window::hide/hide_on M/window.rs:602-676, restore :807-829 |
Three different mechanisms. KDE: the window is moved to an extra virtual desktop komorebi-park that nobody looks at; it stays alive, minimized-false, on its workspace (K/kwin/mod.rs:55-61 module doc, K/backend/mod.rs:32-38). Windows: by HidingBehaviour: Hide (SW_HIDE), Minimize, Cloak (the undocumented SetCloak through COM, W/com/mod.rs:68-); default Cloak (W/lib.rs:205-206); komorebi keeps HIDDEN_HWNDS (W/lib.rs:89, W/window.rs:495-500) so it does not take its own hiding for the user's. macOS: the window is moved to leave a 1-point sliver in a display corner (M/lib.rs:334-348) after its restore rect is stored in the static WINDOW_RESTORE_POSITIONS (M/lib.rs:121, M/window.rs:616-676); restore rewrites position and size. A hidden macOS window is an ordinary window at another position, so the app and the user see it move (observers report it; PARKED_WINDOWS bookkeeping M/window.rs:1152). |
| 7 | Close | close(id) K/backend/mod.rs:382; K/kwin/mod.rs:980-982 |
Window::close W/window.rs:563-565 -> close_window posts WM_CLOSE W/windows_api.rs:694-700 |
AdhocWindow::close(&AXUIElement) presses the AX close button M/window.rs:1444-1453, called on the foreground element M/process_command.rs:327-332 |
All three are requests, not guarantees. macOS returns Ok(()) when the window has no close button (:1451). macOS closes the foreground element, not a stored Window. |
| 8 | Alive check | is_window(id) (provided, window(id).alive) K/backend/mod.rs:612-614; a destroyed window stays queryable with alive == false for a while :18-22 |
WindowsApi::is_window (IsWindow) W/windows_api.rs:1090; the reaper polls it every 20 ms W/reaper.rs:173-210 |
Window::is_valid = copy_attribute_names returns something M/window.rs:572-574 |
KDE: table lookup, never waits. Windows: kernel call, no app involvement, polled without the WM lock (W/reaper.rs:173-210). macOS: an AX round trip to the owning app, only driven on mouse-up/key-up from the event tap (M/input_event_listener.rs:79-82, M/reaper.rs:84-93) and after failed AX writes (M/reaper.rs:164-173): macOS has no pollable "is this window gone". |
| 9 | Foreground window | foreground_window() -> Option<u32> K/backend/mod.rs:248; K/kwin/mod.rs:818-820 |
WindowsApi::foreground_window() -> Result<isize> W/windows_api.rs:718-720 |
MacosApi::foreground_window_id() -> Option<u32> M/macos_api.rs:429-444; foreground_window() -> Option<AXUIElement> :446-456 |
KDE: table. Windows: GetForegroundWindow; a null handle is an Err. macOS: AX system-wide -> focused application -> focused window, then _AXUIElementGetWindow (private, M/accessibility/private.rs:12-16); None whenever any AX step fails; sync IPC. |
| 10 | Window list | windows() -> Vec<WindowInfo> K/backend/mod.rs:242; K/kwin/mod.rs:810-812 |
WindowsApi::load_workspace_information W/windows_api.rs:437, enum_windows :433 (callbacks W/windows_callbacks.rs) |
MacosApi::load_workspace_information M/macos_api.rs:312-374 over CoreGraphicsApi::window_list_info M/core_graphics/mod.rs:126-133 |
KDE returns every candidate window with all fields (WindowInfo K/backend/mod.rs:63-91). Windows and macOS enumerate at start-up (Windows also has alt_tab_windows, W/windows_api.rs:763-771) and otherwise learn from events. macOS joins two worlds by title: CG gives window ids and names, AX gives elements, and the match is window_by_title (== or contains) (M/application.rs:195-208, M/macos_api.rs:346). |
| 11 | Monitors and work areas | monitors() -> Result<Vec<MonitorInfo>> K/backend/mod.rs:255; K/kwin/mod.rs:830-836 |
WindowsApi::load_monitor_information W/windows_api.rs:292, monitor(hmonitor) :1126-1166 |
MacosApi::latest_monitor_information M/macos_api.rs:212-309, load_monitor_information :96, update_monitor_work_areas :182 |
KDE: table, logical pixels, Err when empty (:831-834). Windows: win32_display_data, physical pixels (process is PerMonitorV2, W/main.rs:223, W/windows_api.rs:1169-1172), id: isize (W/monitor.rs:33). macOS: live query of NSScreen, CoreGraphics and IOKit (:213-221), main-thread only (:213), id is the CGDirectDisplayID; work area is NSScreen.visibleFrame flipped into Quartz coordinates (:275-291). |
| 12 | Monitor from point | monitor_from_point(x, y) (provided) K/backend/mod.rs:622-628 |
WindowsApi::monitor_from_point W/windows_api.rs:492-496 |
CoreGraphicsApi::display_with_point M/core_graphics/mod.rs:102-123, MacosApi::monitor_from_point M/macos_api.rs:481-483 |
KDE and macOS return None outside every monitor; Windows uses MONITOR_DEFAULTTONEAREST so it never fails (:493). KDE reads size.left + size.right (origin plus width) (:624-625). |
| 13 | Cursor position | cursor_pos() -> Option<(i32,i32)> K/backend/mod.rs:258; K/kwin/mod.rs:838-850 |
WindowsApi::cursor_pos() -> Result<POINT> W/windows_api.rs:847-852 |
MacosApi::cursor_pos() -> CGPoint M/macos_api.rs:475-479 |
KDE is the odd one: it asks the KWin script and blocks the caller up to 250 ms (:846), falling back to the last position seen; Windows and macOS read it directly. |
| 14 | Center cursor | center_cursor_in(&Rect) K/backend/mod.rs:389; K/kwin/mod.rs:991-1001 |
WindowsApi::center_cursor_in_rect W/windows_api.rs:862-864 |
MacosApi::center_cursor_in_rect M/macos_api.rs:376-381 |
KDE ignores the rect: KWin can only centre on the active window, so the call is correct only right after focusing that window (:992-993). Windows and macOS centre on the rect (left + right/2, width halved). KDE queues; the other two are sync. |
| 15 | Set wallpaper | set_wallpaper(monitor_id, &Path) K/backend/mod.rs:586; K/kwin/mod.rs:1050-1084 |
WindowsApi::set_wallpaper(path, hmonitor) W/windows_api.rs:1488-1510 |
MacosApi::set_wallpaper(path, display_id) M/macos_api.rs:56-91 |
KDE: validates the monitor and the file, then a task asks Plasma and retries; returns at once. Windows: sync COM IDesktopWallpaper. macOS: exec_async on the main queue and Ok(()) regardless (:62, 90; failures only logged :83). |
| 16 | Maximize | maximize(id, bool) (provided, default no-op) K/backend/mod.rs:607; K/kwin/mod.rs:984-989 |
Window::maximize/unmaximize W/window.rs:567-589 -> W/windows_api.rs:710-716 |
none: Workspace::maximized_window exists (M/workspace.rs:52, 124) but is never assigned Some; ToggleMaximize is commented out (M/core/mod.rs:321) |
The "maximized" state query is also generic: WindowInfo::maximized (KDE) and is_zoomed (Windows, 9 sites). |
| 17 | Opacity | set_opacity(id, u8) (provided, default no-op) K/backend/mod.rs:529; K/kwin/mod.rs:1017-1022 |
Window::transparent/opaque W/window.rs:625-, set_transparent W/windows_api.rs:1346-1358 |
none: transparency config is commented out (M/static_config.rs:535-541); kCGWindowAlpha is read only to filter windows (M/window.rs:1547) |
Both implementations take a u8 alpha (KDE divides by 255, :1020). |
| 18 | Workspaces / desktops | five methods ensure_workspaces, switch_workspace, set_window_workspace, window_workspace, current_workspaces K/backend/mod.rs:408-432 |
none in the OS: workspaces are komorebi's own, made visible by hide/restore (row 6) | none in the OS: komorebi's own; a guard ignores events and commands while the macOS Space is not the managed one (M/process_event.rs:120-140, M/lib.rs:184) |
KDE alone delegates workspaces to the compositor (virtual desktops, per output, K/kwin/mod.rs:47-54). The shared core cannot call these five on Windows or macOS; it needs a capability query or a split (section 7, consequence 3). |
| 19 | Focus follows mouse | set_focus_follows_mouse(bool) (provided, default no-op) K/backend/mod.rs:553; rewrites kwinrc K/kwin/mod.rs:1024-1048 |
enable_/disable_/focus_follows_mouse W/windows_api.rs:1224-1257 (system setting) plus a komorebi implementation on its own thread W/process_movement.rs:14-60 |
none (finding 9 above) | KDE and Windows each ask the OS to do it; Windows also has its own mouse-hook implementation. |
| 20 | Window identity fields | WindowInfo fields pid, title, class, exe, path K/backend/mod.rs:63-75 |
window_thread_process_id W/windows_api.rs:866-879, window_text_w :1004, real_window_class_w :1052, exe/exe_path :1031-1050 (each a separate call) |
Window::title/exe/path/bundle_identifier/role/subrole M/window.rs:833-905; no window class, a bundle identifier instead |
KDE delivers them in one snapshot with the window; Windows asks per field (the hook callback comments on the cost, W/windows_callbacks.rs:197-203); macOS asks the app per field over AX. |
2.2 Platform subsystems (one platform, or the same name with unrelated implementations)
| Subsystem | Windows | KDE | macOS |
|---|---|---|---|
| Borders | own WS_EX_TOPMOST windows each on a thread with a message loop W/border_manager/border.rs:304-340, W/windows_api.rs:1322-1344 |
delegated to the compositor through Backend::apply_look, set_border_colours K/backend/mod.rs:473, 504; K/kwin/effects.rs |
one NSWindow per border created on main (exec_async M/border_manager/ns_window.rs:53), AX observers per border M/border_manager/border.rs:153-170; thread M/border_manager/mod.rs:323 |
| Stackbars | W/stackbar_manager/ (windows with their own message-loop thread W/stackbar_manager/stackbar.rs:117-150) |
none | none |
| Animation | ghost windows with DWM thumbnails W/animation/ghost.rs:1-14, W/animation/engine.rs; dwm_* fns |
Look::animation is a request to the compositor K/backend/mod.rs:141-152 |
animator thread plus a lane thread per app pid M/animation/animator.rs:122, 597; NSWindow ghosts M/animation/ghost.rs:125-142 |
| Device / power / session notifications | hidden window with WTSRegisterSessionNotification, power and device notifications W/monitor_reconciliator/hidden.rs:60-110, W/windows_api.rs:1410-1426, 1484 |
K/kwin/power.rs:60-72 (logind and screen-lock over D-Bus) |
NSWorkspace notifications M/notification_center_listener.rs:183-229; CG display callback M/display_reconfiguration_listener.rs:12-30, 76 |
| Window-event source | SetWinEventHook on a message-pump thread W/winevent_listener.rs:24-56 |
the KWin script reports over D-Bus K/kwin/mod.rs:228-255 |
AX observers and NSWorkspace on the main run loop (section 3.3) |
| Raw input | mouse hook via winput W/process_movement.rs:14-20 |
KWin global shortcuts K/kwin/hotkeys.rs |
CGEventTap M/input_event_listener.rs:63-160 |
| Private/undocumented OS calls | SetCloak COM W/com/mod.rs:68- |
none found | SkyLight: CGSGetActiveSpace M/lib.rs:184, SLSDisableUpdate/SLSReenableUpdate batching M/skylight.rs:18-75, _SLPSSetFrontProcessWithOptions M/skylight.rs:123, 206; _AXUIElementGetWindow M/accessibility/private.rs:12-16 |
| Pan gestures | W/pan.rs |
none | M/pan.rs (508 lines) |
| Compositor-only (KDE) | rounded-corner effect, kwinrc editing, hotkey daemon (K/kwin/effects.rs, K/kwin/kwinrc.rs, K/kwin/hotkeys.rs) |
||
| Window style edits, title-bar removal, DWM accents | update_style, remove_title_bar, set_window_accent, round_corners W/window.rs:707-790, W/windows_api.rs:985-1002, 1295-1320 |
none | none |
2.3 Re-measured counts
Method (reproducible from the repo root). Windows: every WindowsApi::<name> occurrence in komorebi/src outside windows_api.rs, lines that are // comments dropped, trailing // comments cut; plus komorebi-bar/src. macOS: the same for MacosApi::, CoreGraphicsApi::, AccessibilityApi:: in komorebi-for-mac/komorebi/src outside macos_api.rs, core_graphics/mod.rs, accessibility/mod.rs. (A naive grep over-counts: M/window_manager.rs:490-492, 2489 are commented-out MacosApi:: calls.)
| API | Distinct fns used outside the API file | Call sites | Defined in the API file |
|---|---|---|---|
WindowsApi |
71 | 280 in W/ + 3 in komorebi-bar/src/bar.rs = 283 |
92 pub fn lines (some duplicated under cfg) |
MacosApi |
14 | 78 | 14 |
CoreGraphicsApi |
5 | 13 | 8 (+1 free fn display_for_frame M/core_graphics/mod.rs:143) |
AccessibilityApi |
13 | 67 (33 in M/window.rs, 11 in M/application.rs, 4 each in M/animation/animator.rs, M/border_manager/border.rs, M/process_event.rs, rest 1-2 each) |
13 |
KDE Backend |
31 methods = 19 required + 12 provided (28 primitives + 3 derived) | 65 backend() call sites outside K/backend/ (non-comment lines) |
n/a |
Split rule. A function is generic when the operation it performs has a counterpart on at least one other platform (a KDE Backend method or a macOS function in the table above). It is split in two tiers because the boundary is a judgement:
- G1 (strict): the operation is in section 2.1.
- G2: a window-state read or z-order helper with no counterpart today but that a shared
WindowInfoorBackendcould carry (lower_window,bring_window_to_top,is_window_visible,is_window_cloaked,monitor_from_window,get_transparent,process_handle,close_process,window_at_cursor_pos). - Platform-only (W / P): everything else (class registration, DWM thumbnails, GWL styles, notification registration, raw AX primitives, ...).
| Platform | G1 fns / sites | G2 fns / sites | Platform-only fns / sites |
|---|---|---|---|
| Windows (71 fns, 283 sites) | 32 / 196 | 9 / 23 | 30 / 61 |
macOS (MacosApi + CoreGraphicsApi + AccessibilityApi: 32 fns, 158 sites) |
15 / 83 | 1 / 4 (left_mouse_button_is_pressed) |
16 / 71 (13 raw AX primitives, frontmost_process_id, activate_finder, contains_rect) |
Windows G1 + G2 = 41 fns / 219 sites (earlier survey 42 / 209); platform-only = 30 / 61 (earlier 29 / 70). The per-function counts are in 2.4 so that the split can be re-derived with another rule.
What the macOS count says about the seam: 67 raw AX call sites live outside accessibility/mod.rs, 33 of them in M/window.rs and 11 in M/application.rs; M/macos_api.rs itself has only 6. Most of the macOS seam is therefore the Window and Application types, not the *Api structs, which is why step 6 normalises Window methods first.
2.4 Per-function counts (name:sites)
Windows, WindowsApi:: outside windows_api.rs (class in brackets: G1, G2, W):
- G1: window_rect:39, foreground_window:26, is_window:21, restore_window:16, position_window:15, is_zoomed:9, disable_focus_follows_mouse:6, center_cursor_in_rect:6, real_window_class_w:5, raise_and_focus_window:5, monitor:4, is_iconic:4, focus_follows_mouse:4, enable_focus_follows_mouse:4, window_thread_process_id:3, maximize_window:3, hide_window:3, close_window:3, unmaximize_window:2, set_transparent:2, raise_window:2, minimize_window:2, load_monitor_information:2, cursor_pos:2, window_text_w:1, set_wallpaper:1, move_window:1, monitor_rects:1, monitor_from_point:1, load_workspace_information:1, exe_path:1, exe:1
- G2: is_window_visible:6, is_window_cloaked:4, window_at_cursor_pos:2, process_handle:2, monitor_from_window:2, lower_window:2, get_transparent:2, close_process:2, bring_window_to_top:1
- W: module_handle_w:8, register_class_w:6, dwm_update_thumbnail_properties:5, dwm_unregister_thumbnail:4, desktop_window:4, register_device_notification:3, dwm_register_thumbnail:3, create_solid_brush:3, update_style:2, update_ex_style:2, set_window_accent:2, wts_register_session_notification:1, set_process_dpi_awareness_context:1, set_border_pos:1, register_power_setting_notification:1, process_id_to_session_id:1, monitors_have_same_dpi:1, monitor_info_w:1, left_click:1, invalidate_rect:1, gwl_style:1, gwl_ex_style:1, foreground_lock_timeout:1, enum_windows:1, enum_display_monitors:1, dpi_for_monitor:1, current_process_id:1, create_hidden_window:1, create_border_window:1, allow_set_foreground_window:1
macOS, outside the API files (G1, G2, P):
- G1: MacosApi::window_rect:39, MacosApi::foreground_window_id:14, MacosApi::foreground_window:7, MacosApi::center_cursor_in_rect:3, MacosApi::load_monitor_information:2, MacosApi::cursor_pos:2, MacosApi::update_monitor_work_areas:1, MacosApi::set_wallpaper:1, MacosApi::monitor_from_point:1, MacosApi::load_workspace_information:1, MacosApi::latest_monitor_information:1, CoreGraphicsApi::display_bounds_for_window_rect:5, CoreGraphicsApi::display_bounds:4, CoreGraphicsApi::window_list_info:1, CoreGraphicsApi::connected_display_ids:1
- G2: MacosApi::left_mouse_button_is_pressed:4
- P: MacosApi::frontmost_process_id:1, MacosApi::activate_finder:1, CoreGraphicsApi::contains_rect:2, AccessibilityApi::copy_attribute_value:17, AccessibilityApi::window_id:11, AccessibilityApi::set_attribute_cf_value:9, AccessibilityApi::set_attribute_ax_value:8, AccessibilityApi::set_messaging_timeout:6, AccessibilityApi::invalidate_observer:3, AccessibilityApi::create_observer:3, AccessibilityApi::copy_attribute_names:3, AccessibilityApi::add_observer_to_run_loop:3, AccessibilityApi::remove_notification_from_observer:1, AccessibilityApi::perform_action:1, AccessibilityApi::create_application:1, AccessibilityApi::add_notification_to_observer:1
3. Threads, locks and who calls the platform
Common to all three trees: the window manager is Arc<parking_lot::Mutex<WindowManager>>; the event consumer thread takes wm.lock() and holds it for the whole event; command connections take it with try_lock_for(1 s) on UDS (K/process_command.rs:2125, M/process_command.rs:1882, W/process_command.rs:2280; the Windows TCP path uses a blocking lock(), :2333). The event channel producers use try_send on KDE and macOS (K/window_manager_event_listener.rs:55, M/window_manager_event_listener.rs:54) and a blocking send on Windows (W/windows_callbacks.rs:212, 233); see the step 5 prep entries for the channel facts, not repeated here.
3.1 KDE: who calls backend()
backend() returns OVERRIDE (thread-local) else the process-wide BACKEND (RwLock<Option<Arc<dyn Backend>>>) else, in the library's own unit tests only, a permissive fake installed in the calling thread's thread-local (K/backend/mod.rs:631-657, 661-673). 65 call sites outside K/backend/ (window.rs 17, window_manager.rs 15, workspace.rs 5, transparency_manager.rs 5, process_command.rs 5, process_event.rs 4, monitor.rs 4, desktop.rs 4, border_manager/mod.rs 3, static_config.rs 2, main.rs 1; non-comment lines, counted by grep -rn "backend()"); all of them are reached from window-manager code.
| Thread | Started at | Locks it takes | Calls backend()? |
|---|---|---|---|
| main | process entry K/main.rs:271 |
WM lock for init, sync_workspace_desktops, retile_all, colour/transparency setup (:320-342); again for the restore at stop (:382-386) |
Yes: indirectly through those WM methods, and directly at K/main.rs:387 (backend().shutdown(), WM lock released). Installs the backend first (:281/:284). Then blocks on quit_receiver.recv() (:379), so it is idle while running. |
kwin-bridge-main plus two tokio workers kwin-bridge |
K/kwin/mod.rs:366-412 |
Shared::table, Shared::queue (own mutexes); never the WM lock |
No (no backend() in K/kwin/*.rs outside tests, grep). The D-Bus handlers Events::event (:230-255) and Poll::poll (:268-300) only update the table, try_send window and monitor notifications (Shared::dispatch :191-206), push commands, answer cursor waiters and call hotkeys::focus. |
| tokio tasks spawned by the backend on that runtime | K/kwin/mod.rs:865, 995, 1081; K/kwin/power.rs:66-70 |
Shared mutexes |
No: they move the pointer, ask Plasma for a wallpaper, watch logind (they send monitor notifications). |
kwin-hotkeys (current-thread runtime) |
K/kwin/hotkeys.rs:599-617 |
its own | No: a key runs komorebic as a child process (K/kwin/hotkeys.rs:356-360), which reaches the daemon like any other client, i.e. through a command thread. |
| event consumer | K/process_event.rs:132-149 |
WM lock held for each event (:136) |
Yes: backend().window, foreground_window, drag_origin, drag_end directly (:460, 521, 796, 864) and everything in Window methods. |
| command accept thread, restarted on panic, and one thread per client | K/process_command.rs:296-330 |
WM lock for try_clone of the listener (:303-306); then try_lock_for(1 s) per connection (:2125) |
Yes: every command is processed under the WM lock. |
| monitor reconciliator | K/monitor_reconciliator.rs:155- |
WM lock per notification (:538, 545, 552, 559, 567) |
Yes: desktop::latest_monitor_information = backend().monitors() (K/desktop.rs:83-89) under the lock. |
| settle thread (spawned once per burst) | K/monitor_reconciliator.rs:123-135 |
WM lock after 1.5 s (:127) |
Yes: follow_windows_to_their_monitors. |
deadlock detector (feature deadlock_detection) |
K/main.rs:136-159 |
none | No. |
| tests | with_backend K/backend/mod.rs:697-707 |
n/a | see below |
Findings (KDE).
- Every
Backendcall from non-test code happens either at start-up/shutdown on the main thread, or from a thread that holds the WM lock. The KWin bridge threads never call it. So aBackendmethod may block the WM lock but never the bridge. - Two kinds of backend methods block their caller, which is why finding 1 matters.
cursor_posblocks up to 250 ms on a reply that arrives on the bridge runtime (K/kwin/mod.rs:841-849).ensure_workspaces,hidewhen the park desktop does not exist yet,apply_look,set_border_colours,set_focus_follows_mouseandshutdownuseHandle::block_on(Shared::block_on:178-183, uses at:901, 931, 1007, 1027, 1097, 1114).block_onfrom inside a tokio worker panics (tokio's documented behaviour, not re-verified here), so the code relies on no runtime thread callingbackend(); finding 1 shows that holds today. - Tests.
with_backendsets a thread-local override (:633-635, 697-707), so a thread spawned inside it does not see the test's fake: it falls through toBACKEND(unset in a test), then to a fresh permissive fake of its own in unit tests, or panics with "no backend installed" elsewhere (:649-657). In integration tests (komorebi/tests/*.rs) the library is not built withcfg(test), sotest_defaultreturnsNone(:670-673) and a test must install a backend. 54 of the 60 files inkomorebi-for-kde/komorebi/tests/mentionwith_backend,set_backendorFakeBackend; the six that do not arepower.rs,config_parity.rs,hot_reload.rs,no_kwin.rs,version.rs,socket.rs. Production code that spawns a thread which callsbackend()would work underset_backendbut silently get a different (permissive) fake in a unit test: nothing today does (the only test-spawned thread,K/process_command.rs:2229, only reads a socket). FakeBackendrecords 18 command kinds (K/backend/fake.rs:43-81), has amin_sizestable so a window can refuse a size (:90), and has no failure injection: its only error isno such window(:509). It has no frame offset (shadow) model either.
3.2 Windows: who calls WindowsApi
WindowsApi is a unit struct of associated functions (W/windows_api.rs:252-254), so there is no state and no handle: any thread may call any function. Threads, from W/main.rs and the listen_for_* functions:
| Thread | Started at | WM lock | WindowsApi calls (examples) |
|---|---|---|---|
| main | W/main.rs |
before the loop: init, retile_all (:327); after ctrl-c: restore_all_windows (:368) |
start-up (:205-223: allow_set_foreground_window, set_process_dpi_awareness_context, foreground_lock_timeout), shutdown disable_focus_follows_mouse (:372). Otherwise blocked in ctrlc_receiver.recv() (:357): there is no message pump on the main thread. |
| WinEvent pump | W/winevent_listener.rs:24-56 |
none | Calls SetWinEventHook and then runs GetMessageW / DispatchMessageW on the same thread (:28-50). The hook callback win_event_hook (W/windows_callbacks.rs:144-236) runs here: window.exe(), title(), class() (:224-230), SendNotifyMessageW to a border, and a blocking event_tx().send(..) (:212, 233). |
| event consumer | W/process_event.rs:117-134 |
held per event (:121) |
every Window method: position_window, restore_window, foreground_window, ... |
| command threads (UDS accept + one per client, and TCP) | W/process_command.rs:105-130, :151 |
try_lock_for(1 s) (:2280); TCP blocking lock() (:2333) |
the same, via process_command |
| reaper (two threads) | W/reaper.rs:58, 155 |
handler: WM lock (:79); poller: none |
poller calls Window::is_window/is_visible every 20 ms (:173-203) |
| border manager | W/border_manager/mod.rs:390 (+ watcher :365, helper threads :1068-1194) |
WM lock per notification (:421) |
window_rect, set_border_pos, border window create/destroy |
| one thread per border window | W/border_manager/border.rs:304-340 |
none | creates the border window (create_border_window) and runs its own GetMessageW loop; the Border is owned by that thread (W/border_manager/mod.rs:1054-1056) |
| one thread per stackbar window | W/stackbar_manager/stackbar.rs:117-150 |
none | CreateWindowExW and its own message loop; position_window is called from another thread under a documented ordering constraint (:178-193) |
| stackbar manager | W/stackbar_manager/mod.rs:74-98 |
WM lock (:98) |
stackbar updates |
| hidden window thread | W/monitor_reconciliator/hidden.rs:82-100 |
none | create_hidden_window, message loop; registers WTS session, power and device notifications |
| monitor reconciliator | W/monitor_reconciliator/mod.rs:226- |
WM lock (:276) |
load_monitor_information, monitor |
| transparency, focus manager, theme manager | W/transparency_manager.rs:52, W/focus_manager.rs:47, W/theme_manager.rs:57 |
WM lock (:87, :67; theme: none) |
set_transparent; Window::focus (W/focus_manager.rs:68) |
| live resize | W/live_resize.rs:707-775, :442, :533, :760 |
WM lock per step (:732, 767) |
position_window (:522, 548, 1044) |
| animation engine | W/animation/engine.rs:100-130, 217-235 (frame thread, a scoped thread per animation, a landing thread per animation) |
none; ANIMATION_MANAGER lock only |
position_window through the dispatchers (W/animation/ghost_dispatcher.rs:376, 513, 599, 673), DWM thumbnails |
| ghost owner | W/animation/ghost.rs:154-156 |
none | creates the host window, owns the thumbnails (:10-14) |
winput mouse thread |
W/process_movement.rs:14-20 |
WM lock per move (:20, 32) |
raise_window_at_cursor_pos |
| config watcher and loader | W/window_manager.rs:348, 465, W/main.rs:304 |
WM lock inside | everything |
Thread-affinity findings (Windows).
- Documented or evident thread-affine operations are all window creation and message handling, not window manipulation. A window must be created by a thread that pumps its messages (
W/animation/ghost.rs:10-14); the border's Direct2D brush update must run on the window's own thread, which is why it is posted asWM_UPDATE_BRUSHES(W/border_manager/border.rs:75-79, 457-459); theBorderis owned by the thread that created its window (W/border_manager/mod.rs:1054-1056); the WinEvent hook is installed on the thread that pumps (W/winevent_listener.rs:26-53). ThatWINEVENT_OUTOFCONTEXTcallbacks are delivered on the installing thread is Microsoft's documented behaviour, not verified here. - No
WindowsApifunction that acts on a managed window is documented thread-affine, and none is called from a single thread.position_windowalone is called from the event thread, command threads, animation threads and live-resize threads (see the list above). The code comments explain the one cross-thread cost:SetWindowPoswaits for the target window's own thread unlessASYNC_WINDOW_POSis used (W/windows_api.rs:518-520), which is a property of the target app, not of komorebi's threads.SetCloakinitialises COM per thread through a thread-local (W/com/mod.rs:40-45, 68-72), so it is callable from any thread too. - Consequence for a Windows
Backendadapter: it can be a stateless wrapper overWindowsApi, callable from any thread, except for the pieces that create windows (borders, stackbars, ghosts, the hidden window and the WinEvent hook), which stay with their own message-loop threads and are exactly the "platform subsystems" of section 2.2. - Hazard that exists today: the hook callback blocks in
sendwhen the 20-slot channel is full (W/winevent_listener.rs:59,W/windows_callbacks.rs:212, 233), while the consumer needs the WM lock that a command thread may hold while itself sending into the same channel (W/window_manager.rs:802, 809, 865). Reading only; not reproduced (this is the plan's step 5 latent bug 2).
3.3 macOS: the main thread, the dispatches and the possible deadlock
The main thread (M/main.rs:202-367) is the only thread that may touch AppKit. main creates NSApplication, calls finishLaunching rather than run (:226-230), registers the NSWorkspace observer and the CoreGraphics display callback (:232-233), takes CFRunLoop::current() and installs the event tap on it (:235-236, 322-330), builds the window manager, starts all other threads (:282-290), and then spins in a loop: nextEventMatchingMask(.., 0.1 s) plus CFRunLoop::run_in_mode(default, 0.1 s) (:314-349; the comment at :333-334 says the explicit NSApp pumping is what makes display callbacks fire on macOS Tahoe). Because of the 0.1 s bound, anything added to that run loop from another thread is noticed within 0.1 s even without a wake-up call. The main thread takes the WM lock only before the loop (:274-280) and after it (:354).
What runs on main, from callbacks: AX observer callbacks for windows (M/window.rs:288-350) and applications (M/application.rs:54), the NSWorkspace handler (M/notification_center_listener.rs:183-229, selector based, no queue argument), the CG display reconfiguration callback (M/display_reconfiguration_listener.rs:12-30, registered :76), the event tap callback (M/input_event_listener.rs:63-100). None of them takes the WM lock (no lock() call on wm in those files). They do synchronous AX IPC to other applications: the window observer reads the window id, title and, for moves, the frame (M/window.rs:294, 330), the tap reads the foreground window (M/input_event_listener.rs:88-100). The handlers hand off by try_send only (M/window_manager_event_listener.rs:54, M/monitor_reconciliator.rs:63-67, M/reaper.rs, M/border_manager/mod.rs:168-178). That NSWorkspace posts on main, and where exactly the CG callback is delivered, is Apple behaviour and not verified.
AppKit objects are created or touched on main only: NSScreen::screens(MainThreadMarker::new().unwrap()) panics off main (M/macos_api.rs:213; the same in M/splash.rs:166); closures dispatched to main use MainThreadMarker::new_unchecked() (M/border_manager/ns_window.rs:55, M/animation/ghost.rs:142, M/macos_api.rs:66).
Other threads and their locks
| Thread | Started at | WM lock | Platform calls |
|---|---|---|---|
| event consumer | M/process_event.rs:70-86 |
held for the whole event (:74) |
every Window method (AX), CoreGraphicsApi, activate_finder (:791), border_manager::destroy_all_borders (:128, 139) |
| "mdm" poller | M/process_event.rs:52-67 |
none | spawns komorebic splash |
| command accept + per-client threads | M/process_command.rs:79-110 |
try_lock_for(1 s) (:1882) |
everything, incl. destroy_all_borders (:294, 303, 1776, 1802, 1806), window.observe(&run_loop) (:1758) |
| monitor reconciliator | M/monitor_reconciliator.rs:71-90 |
per notification: a short lock for a state snapshot (:96-99, released), then see the dispatch table |
exec_sync to main (below) |
| reaper | M/reaper.rs:41-70 |
held per notification (:61) |
Window::is_valid (AX round trip per window) |
| settle | M/settle.rs:78-88 |
wm.lock() (:88) |
AX reads with a 0.25 s timeout (LOOK_TIMEOUT :39) |
| workspace reconciliator | M/workspace_reconciliator.rs:72-98 |
held per notification (:98) |
focus and workspace switches |
| border thread | M/border_manager/mod.rs:323-338, 353 |
WM lock for a snapshot, released at :381 (drop(state)), then BORDER_STATE and WINDOWS_BORDERS locked (:462-463) |
creates borders: exec_sync (M/border_manager/border.rs:153) and a blocking rx.recv() waiting for the main queue to build the NSWindow (M/border_manager/ns_window.rs:52-135, no timeout) |
| theme manager | M/theme_manager.rs:53 |
not taken | destroy_all_borders (:227) |
| animator frame thread | M/animation/animator.rs:122-126 |
never: "Nothing in here holds the window manager's lock" (:15-16) |
none itself; it commits to lanes |
| one lane thread per pid | M/animation/animator.rs:597-600 |
never | AX writes and verifies (:819-850, 902, 925), and border_manager::follow (an exec_async, below) |
| ctrl-c waiter | M/main.rs:295-310 |
none | sets a flag the main loop reads |
| deadlock detector (feature) | M/main.rs:151-174 |
none |
Every dispatch to the main queue
| Site | Kind | Closure | Takes WM lock on main? | Calling threads, and are they holding the WM lock? |
|---|---|---|---|---|
M/monitor_reconciliator.rs:106 |
exec_sync |
wm.lock() then update_monitor_work_areas (NSScreen) (:107-110) |
Yes | reconciliator thread, not holding it (the snapshot lock is released) |
M/monitor_reconciliator.rs:117 |
exec_sync |
latest_monitor_information (:118) |
No | reconciliator thread |
M/monitor_reconciliator.rs:383 |
exec_sync |
arc.lock() then load_monitor_information (:384-386) |
Yes | reconciliator thread, which first drop(wm) "because the main thread needs the lock" (:379-381) |
M/border_manager/mod.rs:157 (hide_borders_on) |
exec_sync |
hide NSWindows |
No | M/window_manager.rs:1385: under the WM lock |
M/border_manager/mod.rs:307 (destroy_border) |
exec_sync |
invalidate the border's AX observer | No | destroy_all_borders (while holding BORDER_STATE, :240) from the event thread (M/process_event.rs:128, 139), command threads (M/process_command.rs:294, 303, 1776, 1802, 1806), retile_all (M/window_manager.rs:2286): all under the WM lock; and the theme manager thread (M/theme_manager.rs:227), whose handle_notifications() takes no wm argument, so it does not hold the WM lock |
M/border_manager/border.rs:153 |
exec_sync |
add AX observer sources to the run loop | No | border thread, holding BORDER_STATE/WINDOWS_BORDERS, not the WM lock |
M/border_manager/border.rs:244 (Border::destroy) |
exec_sync |
NSWindow::close |
No | via destroy_border (:313), same callers as above |
M/macos_api.rs:492 (activate_finder) |
exec_sync |
activate Finder | No | event thread (M/process_event.rs:791): under the WM lock |
M/border_manager/ns_window.rs:53 |
exec_async + blocking rx.recv() (:135) |
build the border NSWindow |
No | border thread; blocks without timeout |
M/animation/ghost.rs:125 (on_main) |
exec_async + recv_timeout(750 ms) (:49, 129) |
build ghosts | No | the animating thread; bounded wait |
M/animation/ghost.rs:322, 365 |
exec_async |
animate and close ghosts | No | animator |
M/border_manager/mod.rs:191, 217 |
exec_async |
apply_follows (borders follow the animator) |
No (it uses try_lock on BORDER_STATE and WINDOWS_BORDERS, :200-205, so it never waits for the border thread) |
animator lanes (follow, :185-193) |
M/macos_api.rs:62 (set_wallpaper) |
exec_async |
setDesktopImageURL |
No | command thread |
exec_sync is dispatch_sync_f (dispatch2-0.3.1/src/queue.rs:118-130): the caller blocks until the main queue has run the closure. Calling it from the main thread itself deadlocks in libdispatch (Apple documents this; not verified here). main reaches destroy_all_borders once, through retile_all at M/main.rs:280, and that is safe only because the border thread, the sole creator of borders, is started afterwards (:282), so BORDER_STATE is empty and destroy_border is never called. This is true by reading only.
The possible deadlock, stated precisely (by reading, not reproduced). Three things must coincide:
- A thread E (the event consumer, or a command thread) holds the WM lock for the whole of a message (
M/process_event.rs:74;M/process_command.rs:1882). - While holding it, E calls a blocking main-queue dispatch from the table above:
destroy_all_borders(space change,M/process_event.rs:128, 139;retile_all; several commands),hide_borders_on(workspace switch with ghosts,M/window_manager.rs:1385) oractivate_finder(M/process_event.rs:791). - The monitor reconciliator thread R has dispatched
exec_sync(closure)for aResizeorDisplayConnectionChange(M/monitor_reconciliator.rs:106, 383) and the main thread has started that closure, which callswm.lock()and therefore waits for E.
Then E waits for the main queue (which is inside R's closure), the main thread waits for E's lock, and neither can proceed. The existing mitigations protect only their own pair of threads: R drops the lock before its second dispatch (:379-381), and apply_follows uses try_lock (M/border_manager/mod.rs:196-205). deadlock_detection (parking_lot::deadlock::check_deadlock, M/main.rs:151-174) would show main blocked on a parking_lot mutex but cannot see that E is waiting on a dispatch semaphore, so no cycle appears in its lock graph. The exposure window is small (a display change at the instant of one of those events). The structural fix, outside this work, is that no closure run on the main thread takes the WM lock. Relevance for the seam: this is why consequence 4 (no Backend method blocks on main while the WM lock is held) is a rule the extraction must not violate; today the rule is already violated at the sites above.
AX observers and the run loop. Window::observe adds an observer source to the main run loop from whichever thread calls it: the event thread (M/process_event.rs:614), a command thread (M/process_command.rs:1758) and main at start-up (M/macos_api.rs:341, 349). Border observers are the opposite: added inside an exec_sync on main (M/border_manager/border.rs:153-170). Whether adding a source to a run loop from a thread that does not own it is safe is a CoreFoundation property that I did not verify (Apple's threading guide lists CFRunLoopRef as thread-safe, from memory, not checked). The code asserts it for windows and avoids it for borders.
Thread-safety assertions in the code. AccessibilityUiElement and AccessibilityObserver are unsafe impl Send + Sync (M/lib.rs:282-284, 300-302), NsWindow is Send (M/border_manager/ns_window.rs:38), Still (a CGImage) is Send + Sync (M/animation/capture.rs:19-20), and the animator clones WindowHandle { element, app } into lane threads (M/animation/animator.rs:90-93). So AX elements already travel between threads; that this is allowed by the AX API is not verified.
4. Identity
| Window id | Monitor id | What the platform needs to act on a window | |
|---|---|---|---|
| KDE | u32, Copy: Window { id } (K/window.rs:78-86) |
u32 (K/monitor.rs:42, 64) |
the KWin UUID string, found in the backend's own table: by_uuid (K/kwin/state.rs:153-154), ids assigned by a counter (id_for :603-611), resolved for every command (KWinBackend::command K/kwin/mod.rs:797-806) |
| Windows | isize HWND, Copy: Window { hwnd } (W/window.rs:68-72) |
isize HMONITOR (W/monitor.rs:33, 57) |
nothing else: the HWND is the OS handle; HWND(as_ptr!(hwnd)) is a cast (W/windows_api.rs:177-181) |
| macOS | u32 CGWindowID, but Window is not a handle: it owns element: AccessibilityUiElement, application: Application, observer: AccessibilityObserver, details and is Clone, not Copy (M/window.rs:417-427) |
u32 CGDirectDisplayID (M/monitor.rs:38, 60) |
the AXUIElement (every read and write), the application's AXUIElement and pid (focus, tabbed apps, the animator's enhanced-UI guard), the AXObserver (observation only) |
4.1 Windows: does an HWND fit in 32 bits?
The code never narrows an HWND: it is an isize end to end, cast to a pointer with as_ptr! (W/windows_api.rs:177-181); the socket type is DebugWindow(isize) (W/core/mod.rs:273), later i64 by decision D15; grep finds no hwnd as u32/i32/u64 except an animation delay jitter start + Duration::from_millis(hwnd as u64) (W/animation/snapshot.rs:920) that does not depend on the width. So the Windows code assumes nothing about 32-bit significance and does not need it. For a shared WindowId(u64): a positive isize converts losslessly; a negative one (a sign-extended 32-bit value) would not map to a small u64.
Observation, not a guarantee: the Windows guest logs and state dumps from the user's earlier e2e runs (~/e2e-w0, read only) contain 440 distinct HWND values from 12345 to 7,804,828; none negative, none above 2^32 (grep -rhoaE 'hwnd[\\": =]+-?[0-9]{4,}'; 12345 looks like a test constant). Microsoft documents that handles are 32-bit significant for interoperability between 32- and 64-bit processes; I did not check that text here, so treat "HWND < 2^32" as likely but unproven (section 8).
4.2 macOS: acting on a window from a bare id
Today nothing on macOS acts from an id: callers hold a Window (cloned freely) or a bare element. AdhocWindow acts on an AXUIElement with no Window (M/window.rs:1436-1530: minimize, close, hide, restore, raise, used from M/process_event.rs:594 and M/process_command.rs:327-332), and the animator keeps WindowHandle { element, app } per id (M/animation/animator.rs:90-93). A Backend that takes only a WindowId therefore needs a registry that owns, per id:
- the window
AXUIElement(set frame, minimize, raise, close, alive check); - the application
AXUIElementand pid (focus uses the pid and the id for SkyLight,M/window.rs:1089, and the application's name andmain_window()for tabbed apps,:1105-1129; the animator uses the app element,M/window.rs:1065-1067); - the
AXObserver(only to keep or release observation;Window::newcreates one per window,M/window.rs:554-570); - a way to learn it is dead: an AX error on any call (
M/reaper.rs:164-173), a destroyed notification, orWindow::is_valid(M/window.rs:572).
Where macOS builds the id-to-element link today: at start-up by matching the CoreGraphics list to AX windows by title (M/macos_api.rs:326-349, M/application.rs:195-208), and afterwards from each AX notification, where the id comes from the private _AXUIElementGetWindow (M/accessibility/mod.rs:31-39, M/accessibility/private.rs:12-16), which can fail (window_id(..).ok() at M/window.rs:294). The registry therefore has to be fed from both, and an id the registry has never seen is not actionable.
KDE is the precedent: its Backend also takes only ids and keeps an id-to-UUID table (above).
4.3 Where per-window platform data lives today
- KDE:
kwin/state.rsTable(entries,by_uuid,next_id, park desktop) for backend data;HIDDEN_WINDOWS(K/window_manager.rs:52, 1401) in the core for what komorebi has hidden. - Windows: nothing in
Window; globals keyed by HWND:HIDDEN_HWNDS(W/lib.rs:89),known_hwnds(W/window_manager.rs:103), border state keyed by container id withwindow_border(hwnd)(W/border_manager/mod.rs), the cloak record (W/cloaked.rs), the live-resize and animation tables. - macOS: in
WindowandApplicationthemselves (element, observer, details), plus globals keyed by id:WINDOW_RESTORE_POSITIONS(M/lib.rs:121),PARKED_WINDOWS(M/window.rs:121),ARRIVALS(M/arrival.rs:40-41), the animator'sTracks(M/animation/animator.rs:95-98), the border map; andWindowManager::applications: HashMap<i32, Application>keyed by pid (M/window_manager.rs:122) andknown_window_ids(:146).
5. Coordinates
The shared rectangle. komorebi_layouts::Rect { left, top, right, bottom }: i32 where left, top are the origin and right, bottom are the width and height (doc comments, komorebi-layouts/src/rect.rs:19-28; add_padding shrinks right/bottom by twice the padding, :113-120). All three trees re-export it (W/core/mod.rs:43, K/core/mod.rs:46, M/core/mod.rs:33). Consequence: the name right is a trap, and From<Rect> for RECT (:43-51) copies the fields unchanged, so the result is not a valid Win32 RECT (right/bottom are a width and height); it exists only to feed SetWindowPos.
| Origin and direction | Unit | Frame measured | Where conversions live | |
|---|---|---|---|---|
| Windows | virtual screen, top-left origin, y down (secondary monitors may have negative coordinates: not verified) | physical pixels: the process is PerMonitorV2 (W/main.rs:223, W/windows_api.rs:1169-1172) |
window_rect = visible bounds (DWMWA_EXTENDED_FRAME_BOUNDS, no shadow), falling back to GetWindowRect (W/windows_api.rs:789-802); RECT to Rect subtracts (komorebi-layouts/src/rect.rs:32-41) |
shadow compensation in position_window (W/windows_api.rs:534-540) and move_window (:658-664), offset computed per call by shadow_rect (two syscalls, :808-821); monitor rects from win32_display_data (:1151-1152); the DPI scaling that window_rect would need is a TODO comment (:793-796) |
| KDE | KWin's global logical space, top-left, y down | logical (device-independent) pixels (MonitorInfo doc K/backend/mod.rs:93) |
KWin frameGeometry both ways (K/kwin/bridge.js:217, 378-381); rounded to integers in the script (:162-164) |
the script (bridge.js) and K/kwin/state.rs; the output's devicePixelRatio is sent (bridge.js:239) but the daemon never reads it (no scale in K/kwin/state.rs or K/kwin/mod.rs) |
| macOS | Quartz global display space: top-left origin of the main display, y down (AX position, CGDisplayBounds) |
points, never pixels (below) | the AX position and size of the window (M/macos_api.rs:385-427) |
CGRect to Rect truncates each f64 with as i32 (komorebi-layouts/src/rect.rs:67-76), so fractional points are lost; NSScreen.visibleFrame (AppKit, bottom-left origin) is flipped into Quartz with the main display's height (M/macos_api.rs:275-291); the border and ghost NSWindows flip back the same way (M/border_manager/border.rs:42-55, M/animation/ghost.rs:143-150); WindowBounds to Rect (M/core/rect_ext.rs:20-28) |
macOS Retina: points versus pixels (previously UNVERIFIED, now read). There is no backingScaleFactor, scale_factor, CGDisplayPixels* or contentsScale anywhere in komorebi-for-mac/komorebi/src (grep -rni). Monitor sizes are CGDisplayBounds (M/core_graphics/mod.rs:56-58) and NSScreen.visibleFrame (points), windows are read and written through AX (points), and the whole layout therefore runs in points. The only place pixels appear is ghost capture: the CGImage width and height "in pixels, which is more than the size of the window in points on a retina display" (M/animation/capture.rs:19-24). Not verified: how AX reports on scaled (non-native) resolutions or on mixed-DPI arrangements, which needs a Mac.
Two small traps for a shared geometry layer.
CoreGraphicsApi::contains_rect(smaller, bigger)isCGRectIntersectsRect(M/core_graphics/mod.rs:33-35), so macOSRect::contains(M/core/rect_ext.rs:13-17) means intersects; it is what assigns a window to a monitor at start-up (M/macos_api.rs:331).- A window's visible frame is not the frame the OS lets you set on Windows; set and get agree only because
window_rectreturns the visible bounds andposition_windowadds the shadow offset back (W/windows_api.rs:534-540). The Windowsset_positioncompareswindow_rectto the layout before writing (W/window.rs:394-396), so the two must stay consistent. KDE and macOS have no such offset in the code; whether macOS AX frames include a shadow is not verified.
The shared fake would have to model a frame offset to exercise the Windows case (plan 7b, "frame offsets"); FakeBackend has none today (section 3.1, finding 4).
6. Events
The channel facts (capacities, try_send versus blocking send, ResizeStep coalescing, the missing Changed reconcile, the two latent bugs) were established in the step 5 prep entries of the progress log and are not repeated. What matters for the seam is where events originate and on which thread.
| Event sources | Reach the window manager how | Thread of the producer | |
|---|---|---|---|
| Windows | SetWinEventHook (W/winevent_listener.rs:28-36); WTS session, power and device notifications on the hidden window (W/monitor_reconciliator/hidden.rs:60-110); a 20 ms IsWindow poll for dead windows (W/reaper.rs:173-210); a winput mouse hook (W/process_movement.rs:14-20) |
win_event_hook builds a WindowManagerEvent and does a blocking send into bounded(20) (W/winevent_listener.rs:59, W/windows_callbacks.rs:212, 233); other sources have their own channels |
the WinEvent message-pump thread; the hidden-window thread; the reaper poller |
| KDE | the KWin script, reporting over D-Bus (K/kwin/mod.rs:228-255); logind and screen-lock (K/kwin/power.rs:60-72); the compositor's presence on the bus |
Table::handle turns each report into Output::Event/Monitor/Command/Cursor (K/kwin/mod.rs:191-206); try_send into bounded(20) with ResizeStep coalescing (K/window_manager_event_listener.rs:22, 36, 51-56) |
the tokio runtime of the bridge |
| macOS | AX observers per application and per window (M/application.rs:54, M/window.rs:288-350); NSWorkspace (M/notification_center_listener.rs:183-229); CG display callback (M/display_reconfiguration_listener.rs:12-30); CGEventTap (M/input_event_listener.rs:63-100) |
try_send into bounded(20) (M/window_manager_event_listener.rs:15, 54) and the reaper, monitor and border channels |
the main thread, from the run loop (section 3.3) |
Every consumer is a single thread that takes the WM lock for the whole event (K/process_event.rs:132-149, M/process_event.rs:70-86, W/process_event.rs:117-134).
6.1 Evidence for "a frame request returns once made; landing is observed later"
The three platforms disagree about whether a frame write has landed when the call returns, and each tree has grown a way to find out afterwards:
- KDE: the command is queued and goes out as the reply to a held
Poll(K/kwin/mod.rs:185-188, 268-300). The trait says so: commands "return once the compositor has accepted them, not once the client has acted on them" (K/backend/mod.rs:21-22) and a client "may refuse the size, so the frame the window ends up with iswindow_rect, notrect" (:319-320).window_rectis optimistic until thegeometryevent arrives (K/kwin/state.rs:594-600). - macOS: "An app is free to refuse a size, and many do" (
M/arrival.rs:1-12): after a direct write the layout reads the frame back (M/window.rs:944-972) and, with animation on, the animator verifies each journey (M/animation/animator.rs:6-14, 902); both report toarrival, which says "Nothing in this module waits for anything or takes the lock of the window manager, so it can be called from any thread. The layout picks up what has been said the next time that it runs" (:8-10).settledoes it again after a drag: "A window can be told where to go and not get there ... A quarter of a second after the drag ... the windows are looked at and any that are not where they are to be are sent there again, and looked at once more after that" (M/settle.rs:1-9, 31). The animator's own moves come back as AX notifications and "must not be mistaken for somebody moving a window" (M/window.rs:300-301). - Windows:
SetWindowPosis synchronous unlessASYNC_WINDOW_POS(W/windows_api.rs:518-528), and the same two ideas exist:set_positioncompares the frame to the layout before writing (W/window.rs:394-396), and after a drag the windows are looked at again after 250 ms, "a window that is busy with what an earlier move made it do is seen to be left where that put it" (W/live_resize.rs:713-724).
So the facts support: a frame request may complete before, after or never at the requested size; the achieved frame is learned from events or from a separate, possibly slow, query. They do not support "never read back": section 7, consequence 2.
7. Consequences for the seam
The seven consequences of plan step 7, each against the facts above. Status: CONFIRMED (the facts support it as written), CORRECTED (the plan was wrong or too strong), TARGET (not true of any current code; a rule the seam would have to make true).
(1) The macOS backend owns an id -> {AX element, pid, observer} registry. CONFIRMED, with additions. Window carries element, application and observer and is cloned into other threads (M/window.rs:417-427, M/animation/animator.rs:90-93), nothing acts from an id today (section 4.2), and KDE already has the id-to-UUID table behind its backend (K/kwin/state.rs:153-154, 603-611). Additions: the registry also needs the application element (focus for tabbed apps, the animator's enhanced-UI guard); the per-id bookkeeping that is global today (WINDOW_RESTORE_POSITIONS, PARKED_WINDOWS, ARRIVALS) belongs behind it or beside it; it has to be fed both by start-up enumeration (a title match, M/macos_api.rs:326-349) and by AX events (_AXUIElementGetWindow can fail, M/window.rs:294); an id it does not know must give an error, as KDE's command does (K/kwin/mod.rs:797-806). The per-window AXObserver can stay a platform subsystem (observation only) if the registry does not need to release it.
(2) The contract is "set_rect returns once requested, never read back to confirm; landing is observed through events". CORRECTED. Not true as stated: macOS reads back (M/window.rs:944-972, M/arrival.rs, M/animation/animator.rs:902), Windows compares before writing (W/window.rs:394-396), KDE's window_rect returns the request until the event replaces it (K/kwin/state.rs:594-600). What the facts support (section 6.1): set_rect is a request. It returns when the request has been made (queued on KDE, issued on Windows and macOS), says nothing about the frame the window ends up with, and may fail only for an unknown or dead window. The achieved frame is reported by events and by window_rect, which a backend may serve from a cache that holds the request until an event corrects it; a backend may offer an explicit, possibly slow, read-back, which the layout calls only where it needs the truth (a refused width, the end of a drag). The shared FakeBackend should be able to refuse a size (it already has min_sizes, K/backend/fake.rs:90) and to delay or drop an event, which it cannot today (no failure injection, section 3.1).
(3) Every method is callable from any non-main thread; main-thread work hides inside the macOS implementation. CONFIRMED, qualified. KDE: every call comes from a thread holding the WM lock or from start-up/shutdown, never from the bridge runtime, whose block_on would panic (section 3.1). Windows: stateless, callable from any thread; only window creation and message loops are thread-affine (section 3.2). macOS: the platform calls are already made from the event, command, reaper, settle, reconciler and animator threads (section 3.3). Qualifications: (a) "hides inside the implementation" must not mean exec_sync: four of the current exec_sync sites (M/border_manager/mod.rs:157, 307, M/border_manager/border.rs:244, M/macos_api.rs:492) are reached under the WM lock and are the ingredients of the possible deadlock; the hidden main-thread work should be exec_async with a bounded wait or data the implementation already holds (consequence 4). (b) Five Backend methods exist only because KDE delegates workspaces to the compositor (ensure_workspaces, switch_workspace, set_window_workspace, window_workspace, current_workspaces, K/backend/mod.rs:408-432; table row 18). Windows and macOS keep workspaces in the core and implement them with hide and restore (row 6). The trait cannot require them of all three: use a capability query, or split them into a sub-trait that only KDE implements and a core path for the rest. (c) maximize and set_opacity are provided no-ops today and are real on KDE and Windows only; macOS has neither (rows 16, 17), so those need the same capability treatment.
(4) No Backend method blocks on the main thread; on macOS monitors() reads a cache refreshed by the display callback; rendering uses exec_async. TARGET, not current behaviour. Current macOS: latest_monitor_information is a live NSScreen query that panics off main (M/macos_api.rs:212-213) and its callers run on main through exec_sync (M/monitor_reconciliator.rs:106, 117, 383). Blocking main dispatches under the WM lock exist at M/border_manager/mod.rs:157, 307, M/border_manager/border.rs:153, 244, M/macos_api.rs:492, plus the unbounded rx.recv() for border windows (M/border_manager/ns_window.rs:135); the bounded ones are on_main (M/animation/ghost.rs:125-129, 750 ms). The CG display callback and NSWorkspace handlers run on main (section 3.3), so a cache refreshed there is feasible (a try_send today, M/display_reconfiguration_listener.rs:12-30), but nothing does it yet. Corrections to the rule's wording: KDE's own contract says "Queries never block on the compositor" (K/backend/mod.rs:179) and is also violated, by cursor_pos (up to 250 ms, K/kwin/mod.rs:846) and by the six block_on methods (section 3.1); so the seam rule should be "no Backend method waits for the main thread of its own platform, and any method that waits for anything is listed and bounded", which Windows (SetWindowPos waiting for the target, unless async, W/windows_api.rs:518-520) and KDE also need.
(5) Borders, stackbars, ghost/DWM, focus-follows-mouse, AX observers, the event tap, SkyLight batching and pan stay in platform crates, feeding the core through the thin event vocabulary. CONFIRMED for most; three corrections.
- Focus-follows-mouse is not platform-only: KDE and Windows both implement it as a setting (
K/kwin/mod.rs:1024-1048,W/windows_api.rs:1224-1257); macOS has none (correction 9). It belongs in the trait as an optional setting. - Borders on KDE are drawn by the compositor and reached through
Backend::apply_lookandset_border_colours(K/backend/mod.rs:473, 504; callersK/border_manager/mod.rs:199, 289), so for KDE they are Backend operations, while on Windows and macOS they are the platform's own windows. The split is therefore "a look request that a backend may ignore" (KDE'sOk(false)) versus platform crates that draw. - SkyLight batching is not confined to the platform layer.
BatchedUpdates::begin()brackets groups of window operations fromM/monitor.rs:246, 289,M/window_manager.rs:1383andM/workspace.rs:1470-1519(SLSDisableUpdate...SLSReenableUpdate): "everything that changes on screen is held back until this is dropped" (M/skylight.rs:36-45). Any shared code that does a layout then needs a way to say "these writes appear together" (a batch or transaction scope with a default no-op). KDE already batches by construction (thePollreply,K/kwin/mod.rs:285-290); Windows does not.
(6) WindowInfo has optional metadata fields. CONFIRMED. KDE's WindowInfo has class, exe, path, fullscreen, maximized, normal as plain fields (K/backend/mod.rs:63-91). macOS has no class, only a bundle identifier, role and subrole (M/window.rs:842-905), and asks the app per field over AX; Windows asks per field too (W/windows_api.rs:866-1052). Only id, pid, rect and alive are available everywhere cheaply; the rest should be Option or fetched on demand. normal, fullscreen, maximized have no macOS source.
(7) macOS setup takes the main run loop from the platform's main. CONFIRMED. macOS main owns the NSApplication, the run loop, the event tap and the observers and spins the loop itself (M/main.rs:226-349), and the shared WindowManager carries the run loop as a field (WindowManager::new(&run_loop, ..), StaticConfig::preload(.., &run_loop), M/main.rs:260-271). Windows and KDE main have no loop to run: they block on a ctrl-c channel (W/main.rs:350-357, K/main.rs:372-379). So main stays per platform. A shared run has to accept a "what to do with the calling thread" hook (park for KDE and Windows, spin the loop for macOS), and the run loop must stop being a field of the shared WindowManager.
Findings the plan did not list
- (8) Batch/transaction scope: see (5).
- (9) Error types differ: KDE
eyre::Result(K/backend/mod.rs:325), Windowseyre::Resultfor some calls and()forShowWindow-based ones (rows 5, 6,W/windows_api.rs:668-684), macOSAccessibilityErrorandLibraryError(M/window.rs:794-915, 1084). A shared error type must carry "the window is gone", because macOS uses a failed write as its signal for that (M/reaper.rs:164-173). - (10)
center_cursor_in(&Rect)is only honest on Windows and macOS; KDE ignores the rect (row 14). Either the method becomes "centre on this window" or KDE's contract gets a caveat. - (11) Hide is observable on macOS and Windows-
Hide/Minimize, because it changes ordinary window state (a corner position,SW_HIDE, minimized), whereas KDE's park desktop and Windows'Cloakdo not. The core's habit of keeping its own table of what it hid (HIDDEN_WINDOWS,HIDDEN_HWNDS,PARKED_WINDOWS) exists for that reason and the seam must keep the distinction ("hidden by komorebi" versus "minimized by the user") visible to the core.
8. Unverified list
Everything I could not confirm by reading, and how to confirm it.
| # | Item | How to confirm |
|---|---|---|
| 1 | NSWorkspace notifications are posted on the main thread, and the CG display callback runs on main (the code assumes both: M/notification_center_listener.rs:183-229, M/display_reconfiguration_listener.rs:76, M/main.rs:333-334) |
On a Mac: log pthread_main_np() (or std::thread::current().name()) in both handlers during a display change, a space change and an app launch; run with the main loop paused for a second to see whether delivery waits |
| 2 | Adding AX observer sources to the main run loop from other threads is safe (M/process_event.rs:614, M/process_command.rs:1758) |
Apple's Thread Safety Summary for CFRunLoopRef; on a Mac, a stress test creating windows while a drag is in progress, watching for missed or duplicate notifications |
| 3 | AXUIElement/AXObserver can be used from several threads (the code asserts Send + Sync, M/lib.rs:282-284, 300-302) |
Apple's Accessibility API documentation; a Mac run with a thread sanitizer on the animator lanes |
| 4 | The possible deadlock in 3.3 | On a Mac: a test that holds the WM lock in a thread that calls destroy_all_borders while a Resize notification is posted for the reconciliator (inject through monitor_reconciliator::send_notification); watch for a hang. Not reproducible on Linux |
| 5 | dispatch_sync to the main queue from the main thread deadlocks (relevant to M/main.rs:280) |
Apple's libdispatch documentation; a Mac test |
| 6 | HWND values always fit in 32 bits on 64-bit Windows | Microsoft's "Interprocess Communication Between 32-bit and 64-bit Applications" (handles are 32-bit significant); on a Windows guest, dump komorebic state for a long session and check 0 <= hwnd < 2^32 (440 values from the e2e logs passed, section 4.1) |
| 7 | WINEVENT_OUTOFCONTEXT callbacks arrive on the thread that called SetWinEventHook; cross-thread SetWindowPos waits for the target's thread |
Microsoft documentation for SetWinEventHook and SetWindowPos; log GetCurrentThreadId in win_event_hook on a guest |
| 8 | Secondary monitors can have negative origins on Windows; AX values on scaled resolutions and mixed-DPI arrangements on macOS | Windows guest with two virtual monitors (the monitors scenario); a Mac with a scaled display and one with a different scale factor: compare CGDisplayBounds, NSScreen.frame and the AX frame of a known window |
| 9 | KWin frameGeometry includes the server-side decoration and excludes the shadow; whether a macOS AX frame includes a shadow |
KWin scripting documentation; compare AX frame with CGWindowListCopyWindowInfo bounds for the same window on a Mac |
| 10 | Whether a macOS window created by an app is always reachable by kAXWindowsAttribute and _AXUIElementGetWindow so that a registry can be fed (title matching at start-up, M/application.rs:195-208) |
A Mac run over a set of apps (Electron, Java, tabbed terminals) comparing CGWindowListCopyWindowInfo ids with the AX window ids |
| 11 | (closed) Whether theme_manager.rs:227 calls destroy_all_borders while holding the WM lock |
Read M/theme_manager.rs:53-75, 227: handle_notifications() takes no window manager, so it does not |
| 12 | Whether the border thread's unbounded rx.recv() (M/border_manager/ns_window.rs:135) can hang if the main queue never runs the closure (for example while main is blocked) |
Read together with item 4; same Mac test |
| 13 | The Windows paths of every WindowsApi call: section 3.2 lists thread entry points and examples, not every call chain |
cargo call graph or a tracing run that logs the thread name per WindowsApi call in a scenario |
| 14 | The KDE block_on panics inside a runtime thread |
Tokio documentation; a unit test calling a block_on method from Handle::spawn |
| 15 | Anything about runtime behaviour: this document is built from reading only; nothing was compiled or run | Run the three gates of the progress log (KDE fast tier, Windows scenarios, macOS by hand) when the trait exists |