Files
LGUG2Z e0e0f49714 docs(dev): old paths in the analysis notes carry the new ones
seam, core-model, settings, rule-matching, bar-unification, e2e-to-unit and known-issues are
measurements and decision logs written for the old directories. They keep their history; each
gets a "Paths (cutover stack)" note under the title that maps the old names (komorebi/,
komorebi-for-kde/komorebi/, komorebi-for-mac/komorebi/, nix/, docs/, schema files) to the new
ones and says the 0.1.x numbers are history. The seam.md tree table, the test file paths in
e2e-to-unit.md and known-issues.md (files that exist and are named as current) and the
bar-e2e.md schema path now name the files where they are.

Not rewritten: the line numbers, measurements and "at the time" statements. Not touched:
backend-switch.md (no old paths), build-and-test.md and flake.md (flake-one).

Verified: the files named as current exist (komorebi-linux/tests/*.rs); relative links resolve.
2026-11-05 15:03:27 -08:00

89 KiB

The platform seam: evidence

Status (wip-cand4): the evidence for crates/komorebi-backend, which has landed (trait, FakeBackend, contract tests); the Windows daemon goes through it for the clusters in backend-switch.md; read section 8 as the list of what was never verified.

Paths (cutover stack): this document was written before the directories were renamed and keeps the paths of its time. Now: komorebi/ is komorebi-windows/, komorebi-for-kde/komorebi/ is komorebi-linux/, komorebi-for-mac/komorebi/ is komorebi-macos/, komorebi-for-kde/komorebi-hotkeys/ is komorebi-hotkeys/, komorebi-for-kde/nix/ and komorebi-for-mac/nix/ are nix/linux/ and nix/macos/, komorebi-for-kde/docs/ and komorebi-for-mac/docs/ are docs/linux/ and docs/macos/, schema*.json are in schemas/{windows,linux,macos}/, and every package is at version 0.2.0 (the 0.1.x numbers below are history).

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:line citation 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/ (now komorebi-windows/src/)
KDE K/ komorebi-for-kde/komorebi/src/ (now komorebi-linux/src/)
macOS M/ komorebi-for-mac/komorebi/src/ (now komorebi-macos/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.

  1. KDE Backend method 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 helpers is_window, window_rect, monitor_from_point (:612-628, written in terms of window and monitors) are left out. Use "28 primitives plus 3 derived".
  2. macOS monitors() is not a cache (already corrected by the kfm A0 report, confirmed). MacosApi::latest_monitor_information queries NSScreen live and does MainThreadMarker::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).
  3. Consequence 2 is too strong as written. "set_rect is 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_rect writes the requested rect into its own table at once (K/kwin/state.rs:596-600, called from K/kwin/mod.rs:855). The defensible contract is in section 7.
  4. The KDE u32 id 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, and KWinBackend::command resolves every command through it (K/kwin/mod.rs:797-806).
  5. The macOS deadlock has more entry points than the plan lists. The plan names destroy_all_borders from 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 at M/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.
  6. 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.
  7. Counts (re-measured; method in section 2.3). KDE trait: 31 (not 28, see 1). Windows WindowsApi: 71 distinct functions used outside windows_api.rs (matches), 280 call sites in W/ plus 3 in komorebi-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: 14 MacosApi functions, 5 of 8 CoreGraphicsApi functions, 13 AccessibilityApi functions are used outside their API files; the earlier "52 raw AX sites, 33 in window.rs" does not reproduce for the total: 67 AccessibilityApi:: sites in 12 files, 33 of them in M/window.rs (that part agrees).
  8. 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".
  9. macOS has no focus-follows-mouse implementation, only config/state fields (grep finds FocusFollowsMouse in M/static_config.rs, M/state.rs, M/core/mod.rs and 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 WindowInfo or Backend could 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).

  1. Every Backend call 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 a Backend method may block the WM lock but never the bridge.
  2. Two kinds of backend methods block their caller, which is why finding 1 matters. cursor_pos blocks up to 250 ms on a reply that arrives on the bridge runtime (K/kwin/mod.rs:841-849). ensure_workspaces, hide when the park desktop does not exist yet, apply_look, set_border_colours, set_focus_follows_mouse and shutdown use Handle::block_on (Shared::block_on :178-183, uses at :901, 931, 1007, 1027, 1097, 1114). block_on from inside a tokio worker panics (tokio's documented behaviour, not re-verified here), so the code relies on no runtime thread calling backend(); finding 1 shows that holds today.
  3. Tests. with_backend sets a thread-local override (:633-635, 697-707), so a thread spawned inside it does not see the test's fake: it falls through to BACKEND (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 with cfg(test), so test_default returns None (:670-673) and a test must install a backend. 54 of the 60 files in komorebi-for-kde/komorebi/tests/ mention with_backend, set_backend or FakeBackend; the six that do not are power.rs, config_parity.rs, hot_reload.rs, no_kwin.rs, version.rs, socket.rs. Production code that spawns a thread which calls backend() would work under set_backend but 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).
  4. FakeBackend records 18 command kinds (K/backend/fake.rs:43-81), has a min_sizes table so a window can refuse a size (:90), and has no failure injection: its only error is no 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).

  1. 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 as WM_UPDATE_BRUSHES (W/border_manager/border.rs:75-79, 457-459); the Border is 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). That WINEVENT_OUTOFCONTEXT callbacks are delivered on the installing thread is Microsoft's documented behaviour, not verified here.
  2. No WindowsApi function that acts on a managed window is documented thread-affine, and none is called from a single thread. position_window alone 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: SetWindowPos waits for the target window's own thread unless ASYNC_WINDOW_POS is used (W/windows_api.rs:518-520), which is a property of the target app, not of komorebi's threads. SetCloak initialises COM per thread through a thread-local (W/com/mod.rs:40-45, 68-72), so it is callable from any thread too.
  3. Consequence for a Windows Backend adapter: it can be a stateless wrapper over WindowsApi, 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.
  4. Hazard that exists today: the hook callback blocks in send when 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:

  1. 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).
  2. 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) or activate_finder (M/process_event.rs:791).
  3. The monitor reconciliator thread R has dispatched exec_sync(closure) for a Resize or DisplayConnectionChange (M/monitor_reconciliator.rs:106, 383) and the main thread has started that closure, which calls wm.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 AXUIElement and pid (focus uses the pid and the id for SkyLight, M/window.rs:1089, and the application's name and main_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::new creates 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, or Window::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.rs Table (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 with window_border(hwnd) (W/border_manager/mod.rs), the cloak record (W/cloaked.rs), the live-resize and animation tables.
  • macOS: in Window and Application themselves (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's Tracks (M/animation/animator.rs:95-98), the border map; and WindowManager::applications: HashMap<i32, Application> keyed by pid (M/window_manager.rs:122) and known_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.

  1. CoreGraphicsApi::contains_rect(smaller, bigger) is CGRectIntersectsRect (M/core_graphics/mod.rs:33-35), so macOS Rect::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).
  2. A window's visible frame is not the frame the OS lets you set on Windows; set and get agree only because window_rect returns the visible bounds and position_window adds the shadow offset back (W/windows_api.rs:534-540). The Windows set_position compares window_rect to 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 is window_rect, not rect" (:319-320). window_rect is optimistic until the geometry event 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 to arrival, 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). settle does 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: SetWindowPos is synchronous unless ASYNC_WINDOW_POS (W/windows_api.rs:518-528), and the same two ideas exist: set_position compares 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_look and set_border_colours (K/backend/mod.rs:473, 504; callers K/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's Ok(false)) versus platform crates that draw.
  • SkyLight batching is not confined to the platform layer. BatchedUpdates::begin() brackets groups of window operations from M/monitor.rs:246, 289, M/window_manager.rs:1383 and M/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 (the Poll reply, 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), Windows eyre::Result for some calls and () for ShowWindow-based ones (rows 5, 6, W/windows_api.rs:668-684), macOS AccessibilityError and LibraryError (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' Cloak do 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

9. Findings from the 7c adapters

Step 7c wrote WindowsBackend (komorebi/src/windows_backend.rs) and MacosBackend (komorebi-for-mac/komorebi/src/macos_backend.rs) over the existing static functions, to learn which methods of the shared trait (crates/komorebi-backend) cannot be implemented without changing what the daemon does. Both compile (Windows: clippy -D warnings clean; macOS: fake-CC cargo check 0 errors) and neither is used by its daemon. They have never run: everything below comes from reading the code while writing the adapter, and "not verified" means exactly that. K/ is komorebi-for-kde/komorebi/src/; W/ and M/ as in section 1.

Verdict. All 14 required methods and the 4 capabilities could be written on both platforms, so the trait did not have to change. Eleven of them carry a semantic difference that the doc comments of the trait already allow (a request, a platform-defined read) or that the adapter had to choose; those are listed here so steps 8 and 11 decide them knowingly. Nothing in this section is a reason to change the trait yet; the three candidates are marked PROPOSAL.

9.1 Windows (WindowsBackend)

Method What the adapter does What differs from the daemon's own call
windows() WindowsApi::alt_tab_windows() (W/windows_api.rs:763), each one read with info Cannot be written faithfully. The candidate list the daemon uses is load_workspace_information (:437), which filters inside the enum_window callback while filling the containers of a WindowManager. The adapter has no manager, so it lists alt-tab windows, a superset (no should_manage, no ignore or float rules).
window(id) IsWindow, then one call per field (process_id, title, class, exe, path, is_iconic, is_zoomed) fullscreen and normal are None: komorebi decides both in the core, there is no OS query. An error reading the frame leaves rect at the default, indistinguishable from a window at the origin (PROPOSAL: rect: Option<Rect> or an error). Five or six kernel calls per call, where KDE answers from a table: do not call it per frame.
monitors() valid_hmonitors() then WindowsApi::monitor(hmonitor) for each monitor re-enumerates every display for each handle (quadratic) and reads the global DUPLICATE_MONITOR_SERIAL_IDS (a duplicated serial becomes None). Physical pixels, the Monitor.id is an isize carried bit for bit through MonitorId::from_hmonitor.
cursor_pos() GetCursorPos, an error is None The error is dropped.
set_rect Window::set_position(rect, false) top (raise to the front in the same SetWindowPos) has no place in the trait: it is false, a caller raises separately, so one SetWindowPos becomes two. Window::set_position also keeps its slide-in animation, the live-resize guard (returns Ok while the user holds the window) and the early return when the frame is already right (W/window.rs:329-370): the call can return Ok without writing, which fits "a request" but not "the window will be there". Synchronous by default (waits for the target's thread; see unverified 7).
focus Window::focus(mouse_follows_focus) Matches the trait, including the cursor centring. When the window is still sliding in (transition::is_entering) the focus is deferred and Ok is returned.
raise Window::raise (raises without activating, and the border) Matches.
hide, restore Window::hide, Window::restore They return (): ShowWindow and the cloak result are ignored, so WindowGone is told only by an IsWindow check before the call (a race). They also hide or show the window's border and write HIDDEN_HWNDS, and are skipped while a workspace slide owns the window (is_exiting, is_entering). The default hiding behaviour Cloak is not observable by the user; Hide and Minimize are (finding 11).
minimize Window::minimize Silently does nothing for an exe that contains komorebi-bar, and returns Ok. No error is possible (()).
unminimize WindowsApi::restore_window (SW_SHOWNOACTIVATE) Windows has no unminimize: this is the restore the hiding behaviours use, without the HIDDEN_HWNDS and border bookkeeping of Window::restore. It does not activate.
close Window::close (PostMessage WM_CLOSE) Matches; the only call with a real error (a failed post), turned into WindowGone when the window is gone.
center_cursor_in WindowsApi::center_cursor_in_rect Matches (and the one place the rect is honoured, finding 10).
monitor_from_point WindowsApi::monitor_from_point Overrides the trait default because Windows uses MONITOR_DEFAULTTONEAREST: never None. The trait documents this; shared code must not use None to detect "off screen".
maximize Window::maximize / unmaximize ()-returning; removes the window from HIDDEN_HWNDS as the daemon does.
set_opacity LAYERED extended style, then WindowsApi::set_transparent(hwnd, alpha) Not what the daemon calls. Window::transparent/opaque take the alpha from transparency_manager::TRANSPARENCY_ALPHA (a global, not an argument) and may animate (TransparencyRenderDispatcher). The adapter writes the plain value both end in, with no animation and no manager bookkeeping.
set_focus_follows_mouse enable_/disable_focus_follows_mouse (the system setting) The komorebi-implemented focus follows mouse (W/process_movement.rs) is a different mechanism with its own thread and is not started or stopped by this; the daemon today picks one by configuration. Both are "focus follows mouse"; the trait method means the OS setting.
Workspaces (5) Unsupported By design: komorebi's own workspaces.
Batch default no-op Windows does not batch.
Thread any thread The calls are stateless except for the statics named above (HIDDEN_HWNDS, DUPLICATE_MONITOR_SERIAL_IDS, the transition and live-resize tables), which are behind locks. set_rect and raise wait for the target window's thread unless WINDOW_HANDLING_BEHAVIOUR is Async: the bounded-wait rule of section 7(4) is not met by Windows today.

9.2 macOS (MacosBackend)

Method What the adapter does What differs from the daemon's own call
(registry) Mutex<HashMap<u32, Window>> with register(Window) and forget(id); every method clones the Window out The trait needs a feeder the trait does not have. The daemon must call register wherever it creates a Window (start-up enumeration by title, AX events) and forget on destroy. Window carries the element, application and observer, and the adapter keeps the whole clone, so the registry holds the observer too (consequence 1 said the observer could stay outside: it cannot while Window is the unit). An id not in the registry is WindowGone, which is also what a window the daemon never registered looks like.
windows() the registry's windows that are still valid Cannot be written faithfully. The CoreGraphics list joined by title with the AX elements (M/macos_api.rs:312-374) lives in load_workspace_information, which fills a WindowManager. The list is only as complete as the registry.
window(id) AX round trips: is_valid (attribute names), frame (two reads), title, exe, path, AXMinimized There is no existing "is minimized" query, so the adapter reads AXMinimized itself. A dead window gives None, so "never known" and "dead" cannot be told apart (the trait allows both). class, fullscreen, maximized, normal are None. A frame read error leaves rect at the default (same PROPOSAL as Windows). Four or more IPC calls to the app per call: slow and blocking on a hung app.
monitors() a cache; refresh_monitors(MainThreadMarker) fills it from latest_monitor_information A change in how the daemon works, not just an adapter. Today every caller runs on main (exec_sync). The cache has to be refreshed by the display callback on main (a try_send today, M/display_reconfiguration_listener.rs:12-30); before the first refresh monitors() is an Err. The MainThreadMarker argument makes a call from another thread a compile error. This is the TARGET rule of consequence 4 made real.
foreground_window MacosApi::foreground_window_id Matches (None when any AX step fails).
cursor_pos MacosApi::cursor_pos Matches, always Some; floats are truncated to i32.
set_rect Window::set_position May write directly or hand the move to the animator, and honours the drag Snap state (M/window.rs:915-940): a request, as the contract says. A failed write asks is_valid to decide between WindowGone and a platform error (the reaper's trick, a second round trip inside the error path).
focus Window::focus Matches (SkyLight set_front_process, then AXMain; tabbed apps use main_window). Error is LibraryError.
raise Window::raise Also focuses: macOS has no raise without focus (AXMain and AXFocused).
hide Window::hide(hiding_position) The trait has no place for two inputs hide needs. The corner (WindowHidingPosition) is configuration of the monitor; MacosBackend::new takes one for all windows. And hide_on(position, screen) wants the display the window's monitor owns, "since a window that hangs over the edge is not always on the display the lookup by position finds" (M/window.rs:610-615); the adapter passes none and takes the lookup. PROPOSAL (step 11, not now): hide stays as it is and MacosBackend learns each window's monitor at register.
restore Window::restore Matches. A window that was not hidden is left alone, as the contract says.
minimize, unminimize Window::minimize/unminimize (AXMinimized) Match (both need &mut self; the adapter clones).
close AdhocWindow::close(element) (presses the close button) The daemon calls it on the foreground element; here by id. Ok when the window has no close button.
center_cursor_in MacosApi::center_cursor_in_rect Matches.
monitor_from_point MacosApi::monitor_from_point (CoreGraphics) Matches: None outside every display.
Capabilities none By design (rows 16 to 19).
Batch a stack of SkyLight BatchedUpdates guards Matches the daemon's own brackets. Not verified: that nested SLSDisableUpdate calls count (the daemon nests none today). The guard holds the main connection id, so it is Send; SLSReenableUpdate from another thread than the one that disabled is not verified.
Thread any thread except main for everything but refresh_monitors Not verified: whether hide_on(.., None)'s lookup by position (M/window.rs:616-676) reaches an NSScreen and so needs main; if it does, hide breaks the rule until the screen is passed (see hide).

9.3 What this means for the trait

  1. No change was needed to write both adapters. The capability query, the batch scope, WindowGone and the optional fields were all used, and each was necessary: macOS answers Unsupported for all four capabilities, Windows for one; WindowGone is built from a pre-check on Windows (results ignored) and from a re-check on macOS and KDE; None fields are used on both.
  2. PROPOSAL A: WindowInfo::rect can fail on both platforms (a frame read is an API call). If the core must tell "at the origin" from "unknown", the field becomes Option<Rect>. Decided not to change it now: KDE cannot fail, and the shared core has no consumer yet.
  3. PROPOSAL B: hide has hidden inputs on macOS (corner, monitor). Left to step 11, where the registry learns the monitor at registration.
  4. PROPOSAL C: windows() is a start-up operation on Windows and macOS (the filtered list needs the core's rules), and the trait documents it as slow. If the core wants one candidate list, the filter has to move into shared code with the window rules as an argument.
  5. The adapters add non-trait API that the daemons must call (MacosBackend::register, forget, refresh_monitors; MacosBackend::new(hiding_position)). That is the cost of keeping Window as it is until step 11(ii).