Files
komorebi/docs/dev/seam.md
T
LGUG2Z 2f1fb7826c docs(seam): window identity and coordinate conventions
Adds sections 4 (identity: id types per platform, what acting on a macOS window from a bare id would need, where each tree keeps per-window platform data, HWND width including an observation of 440 distinct HWNDs from the Windows guest logs) and 5 (coordinates: Rect convention, origin and y direction, DPI and scale handling, shadow and frame compensation, and where each conversion lives). Closes two plan items: Windows HWND width and macOS Retina points versus pixels. Verified against wip-main aa52fdc4d9e5: komorebi-layouts/src/rect.rs, W/window.rs, W/windows_api.rs, W/lib.rs, K/kwin/state.rs, K/kwin/bridge.js, M/window.rs, M/application.rs, M/macos_api.rs, M/core_graphics/mod.rs, M/core/rect_ext.rs, M/border_manager/border.rs, M/animation/ghost.rs, M/animation/capture.rs, and ~/e2e-w0 logs read-only for the HWND range. Not verified: HWND width as a Microsoft guarantee, AX values on scaled-resolution or mixed-DPI displays, negative monitor origins on Windows.
2026-10-31 01:37:53 -07:00

59 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: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/
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.

  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 only enumerate at startup and learn the rest 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 theme_manager.rs:227 (lock state not examined)
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).