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.
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:linecitation read on the tips below. Nothing in a table is a proposal. - Proposals are confined to section 7 (consequences). Each consequence is a statement about what the seam must satisfy, with the facts that support it, and is marked CONFIRMED, CORRECTED or TARGET (not true of any current code, but something the seam would have to make true).
- "Not verified" means I read the code and could not settle the point by reading (usually: Apple or Microsoft runtime behaviour, or behaviour that needs a Mac). It does not mean "false".
Tips read: integration line wip-main at aa52fdc4d9e5 (build(nix): relock the nested flakes after the step 1 and crates changes). The three trees live in one checkout:
| Tree | Prefix used below | Path |
|---|---|---|
| Windows (repo root) | W/ |
komorebi/src/ |
| KDE | K/ |
komorebi-for-kde/komorebi/src/ |
| macOS | M/ |
komorebi-for-mac/komorebi/src/ |
Shared crates are cited with their full path (komorebi-layouts/src/rect.rs). Only reading and grep were used; nothing was compiled or run, and no macOS or Windows runtime behaviour was observed.
Corrections to earlier claims
Things in the plan (generic-percolating-matsumoto.md, Step 7 and "Risks") or the progress log that this reading contradicts or refines.
- KDE
Backendmethod count. The trait declares 31 methods: 19 required (K/backend/mod.rs:242-258,325-389,408-432) and 12 provided (:280,:311,:473,:504,:529,:553,:586,:597,:607,:612,:617,:622). The plan's "28 (not 31)" matches only if the three derived helpersis_window,window_rect,monitor_from_point(:612-628, written in terms ofwindowandmonitors) are left out. Use "28 primitives plus 3 derived". - macOS
monitors()is not a cache (already corrected by the kfm A0 report, confirmed).MacosApi::latest_monitor_informationqueriesNSScreenlive and doesMainThreadMarker::new().unwrap()(M/macos_api.rs:212-213), so it panics off the main thread; every caller is already on main (section 3.3). "monitors() reads a cache refreshed by the display callback" is a TARGET (consequence 4). - Consequence 2 is too strong as written. "
set_rectis never read back to confirm" is false of macOS: after a direct write the layout reads the frame back to learn whether the app refused the width (M/window.rs:944-972,M/arrival.rs:1-12), and the animator verifies each journey (M/animation/animator.rs:902). KDE returns an optimistic value:set_rectwrites the requested rect into its own table at once (K/kwin/state.rs:596-600, called fromK/kwin/mod.rs:855). The defensible contract is in section 7. - The KDE
u32id is not "the compositor's". The doc comments say so (K/backend/mod.rs:65,K/window.rs:78-81), but the id is a counter that the daemon assigns to each KWin UUID string (K/kwin/state.rs:153-154,603-611). KDE already has a registry (uuid <-> id) behind its backend, andKWinBackend::commandresolves every command through it (K/kwin/mod.rs:797-806). - The macOS deadlock has more entry points than the plan lists. The plan names
destroy_all_bordersfrom the event thread (M/process_event.rs:128, 139). A blocking main-queue dispatch under the window-manager lock also happens from the event thread atM/process_event.rs:791(activate_finder,M/macos_api.rs:492), and from command threads (M/process_command.rs:294, 303, 1776, 1802, 1806) and workspace switching (M/window_manager.rs:1385,2286). Section 3.3. - Windows HWND width: the code never narrows an HWND, so it assumes nothing about 32-bit significance (section 4). The Microsoft-documented fact that handles are 32-bit significant is not verified here.
- Counts (re-measured; method in section 2.3). KDE trait: 31 (not 28, see 1). Windows
WindowsApi: 71 distinct functions used outsidewindows_api.rs(matches), 280 call sites inW/plus 3 inkomorebi-bar/src/bar.rs= 283 (earlier survey 279). The generic / Windows-only split depends entirely on the rule used; with the rule stated in 2.3 it is 41 / 30 functions and 219 / 61 sites (earlier survey 42 / 29 and 209 / 70). macOS: 14MacosApifunctions, 5 of 8CoreGraphicsApifunctions, 13AccessibilityApifunctions are used outside their API files; the earlier "52 raw AX sites, 33 in window.rs" does not reproduce for the total: 67AccessibilityApi::sites in 12 files, 33 of them inM/window.rs(that part agrees). - The plan's list of macOS main-thread-only work is incomplete in one respect. The AX observer and event-tap callbacks run on main and do synchronous AX IPC to other applications there (
M/window.rs:288-350,M/input_event_listener.rs:96-100): "main thread" is not just "cheap UI work". - macOS has no focus-follows-mouse implementation, only config/state fields (
grepfindsFocusFollowsMouseinM/static_config.rs,M/state.rs,M/core/mod.rsand nowhere else); KDE and Windows both have one. The plan lists focus-follows-mouse among platform subsystems; for macOS it is absent, not platform-specific.
2. The operation table
Conventions. "Rect" is the shared komorebi_layouts::Rect: left, top are the origin and right, bottom hold the width and height, not the far edge (komorebi-layouts/src/rect.rs:19-28; the conversions From<RECT> at :32-41, From<CGRect> at :67-76). "Queued" means the call returns before the platform has acted; "sync" means the calling thread waits for the platform to answer. Row numbers are for reference in later sections.
2.1 Operations present on at least two platforms
| # | Operation | KDE Backend method |
Windows | macOS | Semantic differences |
|---|---|---|---|---|---|
| 1 | Set frame | set_rect(id, &Rect) K/backend/mod.rs:325; KWinBackend::set_rect K/kwin/mod.rs:852-857 |
Window::set_position(&Rect, top) W/window.rs:374; WindowsApi::position_window W/windows_api.rs:501-555 (SetWindowPos :639-652); move_window :655-666 |
Window::set_position(&Rect) M/window.rs:915; direct path set_position_direct = set_point then set_size, two AX writes M/window.rs:1050-1058, 1148-1184; animated path animator::retarget M/window.rs:1061-1082 |
KDE: queued. A geo command is pushed (command_rect K/kwin/mod.rs:786-788) and carried out as the answer to a held Poll (:268-300, 1 ms coalescing :141); the requested rect is written into the table at once (K/kwin/state.rs:596-600). Windows: sync by default (WINDOW_HANDLING_BEHAVIOUR default Sync, W/lib.rs:266-267, W/core/mod.rs:816-822); with Async and with_async_window_pos the call adds ASYNC_WINDOW_POS (W/windows_api.rs:521-528). macOS: sync AX round trips (M/animation/animator.rs:6-7) when animation is off; queued to the animator lane when on. Rect: Windows adds the DWM shadow offset before the call (W/windows_api.rs:534-540, shadow_rect :808-821); KDE sets frameGeometry (K/kwin/bridge.js:378-381); macOS writes AX position and size as written. A client may refuse the size (documented for KDE K/backend/mod.rs:319-320; macOS reads it back, M/arrival.rs:1-12). Errors: KDE only for an unknown or dead id (K/kwin/mod.rs:797-806); Windows returns the SetWindowPos error (.process() :651); macOS returns the AX error and a failed write asks the reaper whether the window is gone (M/window.rs:1155-1162, M/reaper.rs:164-173). |
| 2 | Get frame | window_rect(id) (provided) K/backend/mod.rs:617-619 over window(id) :245 |
WindowsApi::window_rect(hwnd) W/windows_api.rs:789-802 |
MacosApi::window_rect(&AXUIElement) -> CGRect M/macos_api.rs:385-427 |
KDE: from the daemon's table, never waits on KWin; holds the optimistic requested rect until a geometry event replaces it (K/kwin/state.rs:594-600). Windows: visible bounds (DWMWA_EXTENDED_FRAME_BOUNDS, without the shadow) with fallback to GetWindowRect (includes shadow) (:792-801). macOS: AX position and size, two sync IPC calls, errors logged and returned (:386-410). macOS takes an element, not an id. |
| 3 | Focus | focus(id, mouse_follows_focus) K/backend/mod.rs:332; K/kwin/mod.rs:859-880 |
Window::focus W/window.rs:591-622 -> WindowsApi::raise_and_focus_window W/windows_api.rs:722-747 |
Window::focus M/window.rs:1084-1139 |
KDE queues an activate, then (if mouse follows) spawns a task that polls up to 60 x 5 ms for the window to be foreground before moving the pointer (:864-877). Windows: a synthetic SendInput to pass the foreground lock, SetWindowPos (error ignored), then SetForegroundWindow, whose failure is the returned error; skips the work when already foreground (W/window.rs:599-607). macOS: tries the private SkyLight set_front_process, else NSRunningApplication::activateWithOptions (M/window.rs:1089-1102, M/skylight.rs:206), then sets AXMain (:1131); tabbed apps use main_window() (:1105-1129). Pointer centring reads the frame back over AX. |
| 4 | Raise | raise(id) K/backend/mod.rs:339; K/kwin/mod.rs:882-884 |
Window::raise W/window.rs:791-798 -> raise_window W/windows_api.rs:563-583 |
Window::raise M/window.rs:1141-1146 |
Windows raises without activating (SetWindowPos(HWND_TOP, NOACTIVATE)) and also raises the window's border. macOS has no raise-without-focus: it sets AXMain and AXFocused (:1144-1145). |
| 5 | Minimize / unminimize | minimize(id), unminimize(id) K/backend/mod.rs:368, 375; K/kwin/mod.rs:972-978 |
Window::minimize W/window.rs:556-561 (skips an exe containing komorebi-bar) -> minimize_window W/windows_api.rs:686; unminimize is restore_window (SW_SHOWNOACTIVATE) :706-708 |
Window::minimize/unminimize set AXMinimized true/false M/window.rs:794-805 |
Windows has no separate unminimize, and show_window ignores the result (W/windows_api.rs:668-684, "TODO: error handling"; ShowWindowAsync when Async), so these return (). KDE and macOS return Result. |
| 6 | Hide / restore (back of a stack, other workspace) | hide(id), restore(id) K/backend/mod.rs:353, 361; K/kwin/mod.rs:886-925 |
Window::hide_with_border/restore_with_border W/window.rs:488-554 |
Window::hide/hide_on M/window.rs:602-676, restore :807-829 |
Three different mechanisms. KDE: the window is moved to an extra virtual desktop komorebi-park that nobody looks at; it stays alive, minimized-false, on its workspace (K/kwin/mod.rs:55-61 module doc, K/backend/mod.rs:32-38). Windows: by HidingBehaviour: Hide (SW_HIDE), Minimize, Cloak (the undocumented SetCloak through COM, W/com/mod.rs:68-); default Cloak (W/lib.rs:205-206); komorebi keeps HIDDEN_HWNDS (W/lib.rs:89, W/window.rs:495-500) so it does not take its own hiding for the user's. macOS: the window is moved to leave a 1-point sliver in a display corner (M/lib.rs:334-348) after its restore rect is stored in the static WINDOW_RESTORE_POSITIONS (M/lib.rs:121, M/window.rs:616-676); restore rewrites position and size. A hidden macOS window is an ordinary window at another position, so the app and the user see it move (observers report it; PARKED_WINDOWS bookkeeping M/window.rs:1152). |
| 7 | Close | close(id) K/backend/mod.rs:382; K/kwin/mod.rs:980-982 |
Window::close W/window.rs:563-565 -> close_window posts WM_CLOSE W/windows_api.rs:694-700 |
AdhocWindow::close(&AXUIElement) presses the AX close button M/window.rs:1444-1453, called on the foreground element M/process_command.rs:327-332 |
All three are requests, not guarantees. macOS returns Ok(()) when the window has no close button (:1451). macOS closes the foreground element, not a stored Window. |
| 8 | Alive check | is_window(id) (provided, window(id).alive) K/backend/mod.rs:612-614; a destroyed window stays queryable with alive == false for a while :18-22 |
WindowsApi::is_window (IsWindow) W/windows_api.rs:1090; the reaper polls it every 20 ms W/reaper.rs:173-210 |
Window::is_valid = copy_attribute_names returns something M/window.rs:572-574 |
KDE: table lookup, never waits. Windows: kernel call, no app involvement, polled without the WM lock (W/reaper.rs:173-210). macOS: an AX round trip to the owning app, only driven on mouse-up/key-up from the event tap (M/input_event_listener.rs:79-82, M/reaper.rs:84-93) and after failed AX writes (M/reaper.rs:164-173): macOS has no pollable "is this window gone". |
| 9 | Foreground window | foreground_window() -> Option<u32> K/backend/mod.rs:248; K/kwin/mod.rs:818-820 |
WindowsApi::foreground_window() -> Result<isize> W/windows_api.rs:718-720 |
MacosApi::foreground_window_id() -> Option<u32> M/macos_api.rs:429-444; foreground_window() -> Option<AXUIElement> :446-456 |
KDE: table. Windows: GetForegroundWindow; a null handle is an Err. macOS: AX system-wide -> focused application -> focused window, then _AXUIElementGetWindow (private, M/accessibility/private.rs:12-16); None whenever any AX step fails; sync IPC. |
| 10 | Window list | windows() -> Vec<WindowInfo> K/backend/mod.rs:242; K/kwin/mod.rs:810-812 |
WindowsApi::load_workspace_information W/windows_api.rs:437, enum_windows :433 (callbacks W/windows_callbacks.rs) |
MacosApi::load_workspace_information M/macos_api.rs:312-374 over CoreGraphicsApi::window_list_info M/core_graphics/mod.rs:126-133 |
KDE returns every candidate window with all fields (WindowInfo K/backend/mod.rs:63-91). Windows and macOS 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
WindowInfoorBackendcould carry (lower_window,bring_window_to_top,is_window_visible,is_window_cloaked,monitor_from_window,get_transparent,process_handle,close_process,window_at_cursor_pos). - Platform-only (W / P): everything else (class registration, DWM thumbnails, GWL styles, notification registration, raw AX primitives, ...).
| Platform | G1 fns / sites | G2 fns / sites | Platform-only fns / sites |
|---|---|---|---|
| Windows (71 fns, 283 sites) | 32 / 196 | 9 / 23 | 30 / 61 |
macOS (MacosApi + CoreGraphicsApi + AccessibilityApi: 32 fns, 158 sites) |
15 / 83 | 1 / 4 (left_mouse_button_is_pressed) |
16 / 71 (13 raw AX primitives, frontmost_process_id, activate_finder, contains_rect) |
Windows G1 + G2 = 41 fns / 219 sites (earlier survey 42 / 209); platform-only = 30 / 61 (earlier 29 / 70). The per-function counts are in 2.4 so that the split can be re-derived with another rule.
What the macOS count says about the seam: 67 raw AX call sites live outside accessibility/mod.rs, 33 of them in M/window.rs and 11 in M/application.rs; M/macos_api.rs itself has only 6. Most of the macOS seam is therefore the Window and Application types, not the *Api structs, which is why step 6 normalises Window methods first.
2.4 Per-function counts (name:sites)
Windows, WindowsApi:: outside windows_api.rs (class in brackets: G1, G2, W):
- G1: window_rect:39, foreground_window:26, is_window:21, restore_window:16, position_window:15, is_zoomed:9, disable_focus_follows_mouse:6, center_cursor_in_rect:6, real_window_class_w:5, raise_and_focus_window:5, monitor:4, is_iconic:4, focus_follows_mouse:4, enable_focus_follows_mouse:4, window_thread_process_id:3, maximize_window:3, hide_window:3, close_window:3, unmaximize_window:2, set_transparent:2, raise_window:2, minimize_window:2, load_monitor_information:2, cursor_pos:2, window_text_w:1, set_wallpaper:1, move_window:1, monitor_rects:1, monitor_from_point:1, load_workspace_information:1, exe_path:1, exe:1
- G2: is_window_visible:6, is_window_cloaked:4, window_at_cursor_pos:2, process_handle:2, monitor_from_window:2, lower_window:2, get_transparent:2, close_process:2, bring_window_to_top:1
- W: module_handle_w:8, register_class_w:6, dwm_update_thumbnail_properties:5, dwm_unregister_thumbnail:4, desktop_window:4, register_device_notification:3, dwm_register_thumbnail:3, create_solid_brush:3, update_style:2, update_ex_style:2, set_window_accent:2, wts_register_session_notification:1, set_process_dpi_awareness_context:1, set_border_pos:1, register_power_setting_notification:1, process_id_to_session_id:1, monitors_have_same_dpi:1, monitor_info_w:1, left_click:1, invalidate_rect:1, gwl_style:1, gwl_ex_style:1, foreground_lock_timeout:1, enum_windows:1, enum_display_monitors:1, dpi_for_monitor:1, current_process_id:1, create_hidden_window:1, create_border_window:1, allow_set_foreground_window:1
macOS, outside the API files (G1, G2, P):
- G1: MacosApi::window_rect:39, MacosApi::foreground_window_id:14, MacosApi::foreground_window:7, MacosApi::center_cursor_in_rect:3, MacosApi::load_monitor_information:2, MacosApi::cursor_pos:2, MacosApi::update_monitor_work_areas:1, MacosApi::set_wallpaper:1, MacosApi::monitor_from_point:1, MacosApi::load_workspace_information:1, MacosApi::latest_monitor_information:1, CoreGraphicsApi::display_bounds_for_window_rect:5, CoreGraphicsApi::display_bounds:4, CoreGraphicsApi::window_list_info:1, CoreGraphicsApi::connected_display_ids:1
- G2: MacosApi::left_mouse_button_is_pressed:4
- P: MacosApi::frontmost_process_id:1, MacosApi::activate_finder:1, CoreGraphicsApi::contains_rect:2, AccessibilityApi::copy_attribute_value:17, AccessibilityApi::window_id:11, AccessibilityApi::set_attribute_cf_value:9, AccessibilityApi::set_attribute_ax_value:8, AccessibilityApi::set_messaging_timeout:6, AccessibilityApi::invalidate_observer:3, AccessibilityApi::create_observer:3, AccessibilityApi::copy_attribute_names:3, AccessibilityApi::add_observer_to_run_loop:3, AccessibilityApi::remove_notification_from_observer:1, AccessibilityApi::perform_action:1, AccessibilityApi::create_application:1, AccessibilityApi::add_notification_to_observer:1
3. Threads, locks and who calls the platform
Common to all three trees: the window manager is Arc<parking_lot::Mutex<WindowManager>>; the event consumer thread takes wm.lock() and holds it for the whole event; command connections take it with try_lock_for(1 s) on UDS (K/process_command.rs:2125, M/process_command.rs:1882, W/process_command.rs:2280; the Windows TCP path uses a blocking lock(), :2333). The event channel producers use try_send on KDE and macOS (K/window_manager_event_listener.rs:55, M/window_manager_event_listener.rs:54) and a blocking send on Windows (W/windows_callbacks.rs:212, 233); see the step 5 prep entries for the channel facts, not repeated here.
3.1 KDE: who calls backend()
backend() returns OVERRIDE (thread-local) else the process-wide BACKEND (RwLock<Option<Arc<dyn Backend>>>) else, in the library's own unit tests only, a permissive fake installed in the calling thread's thread-local (K/backend/mod.rs:631-657, 661-673). 65 call sites outside K/backend/ (window.rs 17, window_manager.rs 15, workspace.rs 5, transparency_manager.rs 5, process_command.rs 5, process_event.rs 4, monitor.rs 4, desktop.rs 4, border_manager/mod.rs 3, static_config.rs 2, main.rs 1; non-comment lines, counted by grep -rn "backend()"); all of them are reached from window-manager code.
| Thread | Started at | Locks it takes | Calls backend()? |
|---|---|---|---|
| main | process entry K/main.rs:271 |
WM lock for init, sync_workspace_desktops, retile_all, colour/transparency setup (:320-342); again for the restore at stop (:382-386) |
Yes: indirectly through those WM methods, and directly at K/main.rs:387 (backend().shutdown(), WM lock released). Installs the backend first (:281/:284). Then blocks on quit_receiver.recv() (:379), so it is idle while running. |
kwin-bridge-main plus two tokio workers kwin-bridge |
K/kwin/mod.rs:366-412 |
Shared::table, Shared::queue (own mutexes); never the WM lock |
No (no backend() in K/kwin/*.rs outside tests, grep). The D-Bus handlers Events::event (:230-255) and Poll::poll (:268-300) only update the table, try_send window and monitor notifications (Shared::dispatch :191-206), push commands, answer cursor waiters and call hotkeys::focus. |
| tokio tasks spawned by the backend on that runtime | K/kwin/mod.rs:865, 995, 1081; K/kwin/power.rs:66-70 |
Shared mutexes |
No: they move the pointer, ask Plasma for a wallpaper, watch logind (they send monitor notifications). |
kwin-hotkeys (current-thread runtime) |
K/kwin/hotkeys.rs:599-617 |
its own | No: a key runs komorebic as a child process (K/kwin/hotkeys.rs:356-360), which reaches the daemon like any other client, i.e. through a command thread. |
| event consumer | K/process_event.rs:132-149 |
WM lock held for each event (:136) |
Yes: backend().window, foreground_window, drag_origin, drag_end directly (:460, 521, 796, 864) and everything in Window methods. |
| command accept thread, restarted on panic, and one thread per client | K/process_command.rs:296-330 |
WM lock for try_clone of the listener (:303-306); then try_lock_for(1 s) per connection (:2125) |
Yes: every command is processed under the WM lock. |
| monitor reconciliator | K/monitor_reconciliator.rs:155- |
WM lock per notification (:538, 545, 552, 559, 567) |
Yes: desktop::latest_monitor_information = backend().monitors() (K/desktop.rs:83-89) under the lock. |
| settle thread (spawned once per burst) | K/monitor_reconciliator.rs:123-135 |
WM lock after 1.5 s (:127) |
Yes: follow_windows_to_their_monitors. |
deadlock detector (feature deadlock_detection) |
K/main.rs:136-159 |
none | No. |
| tests | with_backend K/backend/mod.rs:697-707 |
n/a | see below |
Findings (KDE).
- Every
Backendcall from non-test code happens either at start-up/shutdown on the main thread, or from a thread that holds the WM lock. The KWin bridge threads never call it. So aBackendmethod may block the WM lock but never the bridge. - Two kinds of backend methods block their caller, which is why finding 1 matters.
cursor_posblocks up to 250 ms on a reply that arrives on the bridge runtime (K/kwin/mod.rs:841-849).ensure_workspaces,hidewhen the park desktop does not exist yet,apply_look,set_border_colours,set_focus_follows_mouseandshutdownuseHandle::block_on(Shared::block_on:178-183, uses at:901, 931, 1007, 1027, 1097, 1114).block_onfrom inside a tokio worker panics (tokio's documented behaviour, not re-verified here), so the code relies on no runtime thread callingbackend(); finding 1 shows that holds today. - Tests.
with_backendsets a thread-local override (:633-635, 697-707), so a thread spawned inside it does not see the test's fake: it falls through toBACKEND(unset in a test), then to a fresh permissive fake of its own in unit tests, or panics with "no backend installed" elsewhere (:649-657). In integration tests (komorebi/tests/*.rs) the library is not built withcfg(test), sotest_defaultreturnsNone(:670-673) and a test must install a backend. 54 of the 60 files inkomorebi-for-kde/komorebi/tests/mentionwith_backend,set_backendorFakeBackend; the six that do not arepower.rs,config_parity.rs,hot_reload.rs,no_kwin.rs,version.rs,socket.rs. Production code that spawns a thread which callsbackend()would work underset_backendbut silently get a different (permissive) fake in a unit test: nothing today does (the only test-spawned thread,K/process_command.rs:2229, only reads a socket). FakeBackendrecords 18 command kinds (K/backend/fake.rs:43-81), has amin_sizestable so a window can refuse a size (:90), and has no failure injection: its only error isno such window(:509). It has no frame offset (shadow) model either.
3.2 Windows: who calls WindowsApi
WindowsApi is a unit struct of associated functions (W/windows_api.rs:252-254), so there is no state and no handle: any thread may call any function. Threads, from W/main.rs and the listen_for_* functions:
| Thread | Started at | WM lock | WindowsApi calls (examples) |
|---|---|---|---|
| main | W/main.rs |
before the loop: init, retile_all (:327); after ctrl-c: restore_all_windows (:368) |
start-up (:205-223: allow_set_foreground_window, set_process_dpi_awareness_context, foreground_lock_timeout), shutdown disable_focus_follows_mouse (:372). Otherwise blocked in ctrlc_receiver.recv() (:357): there is no message pump on the main thread. |
| WinEvent pump | W/winevent_listener.rs:24-56 |
none | Calls SetWinEventHook and then runs GetMessageW / DispatchMessageW on the same thread (:28-50). The hook callback win_event_hook (W/windows_callbacks.rs:144-236) runs here: window.exe(), title(), class() (:224-230), SendNotifyMessageW to a border, and a blocking event_tx().send(..) (:212, 233). |
| event consumer | W/process_event.rs:117-134 |
held per event (:121) |
every Window method: position_window, restore_window, foreground_window, ... |
| command threads (UDS accept + one per client, and TCP) | W/process_command.rs:105-130, :151 |
try_lock_for(1 s) (:2280); TCP blocking lock() (:2333) |
the same, via process_command |
| reaper (two threads) | W/reaper.rs:58, 155 |
handler: WM lock (:79); poller: none |
poller calls Window::is_window/is_visible every 20 ms (:173-203) |
| border manager | W/border_manager/mod.rs:390 (+ watcher :365, helper threads :1068-1194) |
WM lock per notification (:421) |
window_rect, set_border_pos, border window create/destroy |
| one thread per border window | W/border_manager/border.rs:304-340 |
none | creates the border window (create_border_window) and runs its own GetMessageW loop; the Border is owned by that thread (W/border_manager/mod.rs:1054-1056) |
| one thread per stackbar window | W/stackbar_manager/stackbar.rs:117-150 |
none | CreateWindowExW and its own message loop; position_window is called from another thread under a documented ordering constraint (:178-193) |
| stackbar manager | W/stackbar_manager/mod.rs:74-98 |
WM lock (:98) |
stackbar updates |
| hidden window thread | W/monitor_reconciliator/hidden.rs:82-100 |
none | create_hidden_window, message loop; registers WTS session, power and device notifications |
| monitor reconciliator | W/monitor_reconciliator/mod.rs:226- |
WM lock (:276) |
load_monitor_information, monitor |
| transparency, focus manager, theme manager | W/transparency_manager.rs:52, W/focus_manager.rs:47, W/theme_manager.rs:57 |
WM lock (:87, :67; theme: none) |
set_transparent; Window::focus (W/focus_manager.rs:68) |
| live resize | W/live_resize.rs:707-775, :442, :533, :760 |
WM lock per step (:732, 767) |
position_window (:522, 548, 1044) |
| animation engine | W/animation/engine.rs:100-130, 217-235 (frame thread, a scoped thread per animation, a landing thread per animation) |
none; ANIMATION_MANAGER lock only |
position_window through the dispatchers (W/animation/ghost_dispatcher.rs:376, 513, 599, 673), DWM thumbnails |
| ghost owner | W/animation/ghost.rs:154-156 |
none | creates the host window, owns the thumbnails (:10-14) |
winput mouse thread |
W/process_movement.rs:14-20 |
WM lock per move (:20, 32) |
raise_window_at_cursor_pos |
| config watcher and loader | W/window_manager.rs:348, 465, W/main.rs:304 |
WM lock inside | everything |
Thread-affinity findings (Windows).
- Documented or evident thread-affine operations are all window creation and message handling, not window manipulation. A window must be created by a thread that pumps its messages (
W/animation/ghost.rs:10-14); the border's Direct2D brush update must run on the window's own thread, which is why it is posted asWM_UPDATE_BRUSHES(W/border_manager/border.rs:75-79, 457-459); theBorderis owned by the thread that created its window (W/border_manager/mod.rs:1054-1056); the WinEvent hook is installed on the thread that pumps (W/winevent_listener.rs:26-53). ThatWINEVENT_OUTOFCONTEXTcallbacks are delivered on the installing thread is Microsoft's documented behaviour, not verified here. - No
WindowsApifunction that acts on a managed window is documented thread-affine, and none is called from a single thread.position_windowalone is called from the event thread, command threads, animation threads and live-resize threads (see the list above). The code comments explain the one cross-thread cost:SetWindowPoswaits for the target window's own thread unlessASYNC_WINDOW_POSis used (W/windows_api.rs:518-520), which is a property of the target app, not of komorebi's threads.SetCloakinitialises COM per thread through a thread-local (W/com/mod.rs:40-45, 68-72), so it is callable from any thread too. - Consequence for a Windows
Backendadapter: it can be a stateless wrapper overWindowsApi, callable from any thread, except for the pieces that create windows (borders, stackbars, ghosts, the hidden window and the WinEvent hook), which stay with their own message-loop threads and are exactly the "platform subsystems" of section 2.2. - Hazard that exists today: the hook callback blocks in
sendwhen the 20-slot channel is full (W/winevent_listener.rs:59,W/windows_callbacks.rs:212, 233), while the consumer needs the WM lock that a command thread may hold while itself sending into the same channel (W/window_manager.rs:802, 809, 865). Reading only; not reproduced (this is the plan's step 5 latent bug 2).
3.3 macOS: the main thread, the dispatches and the possible deadlock
The main thread (M/main.rs:202-367) is the only thread that may touch AppKit. main creates NSApplication, calls finishLaunching rather than run (:226-230), registers the NSWorkspace observer and the CoreGraphics display callback (:232-233), takes CFRunLoop::current() and installs the event tap on it (:235-236, 322-330), builds the window manager, starts all other threads (:282-290), and then spins in a loop: nextEventMatchingMask(.., 0.1 s) plus CFRunLoop::run_in_mode(default, 0.1 s) (:314-349; the comment at :333-334 says the explicit NSApp pumping is what makes display callbacks fire on macOS Tahoe). Because of the 0.1 s bound, anything added to that run loop from another thread is noticed within 0.1 s even without a wake-up call. The main thread takes the WM lock only before the loop (:274-280) and after it (:354).
What runs on main, from callbacks: AX observer callbacks for windows (M/window.rs:288-350) and applications (M/application.rs:54), the NSWorkspace handler (M/notification_center_listener.rs:183-229, selector based, no queue argument), the CG display reconfiguration callback (M/display_reconfiguration_listener.rs:12-30, registered :76), the event tap callback (M/input_event_listener.rs:63-100). None of them takes the WM lock (no lock() call on wm in those files). They do synchronous AX IPC to other applications: the window observer reads the window id, title and, for moves, the frame (M/window.rs:294, 330), the tap reads the foreground window (M/input_event_listener.rs:88-100). The handlers hand off by try_send only (M/window_manager_event_listener.rs:54, M/monitor_reconciliator.rs:63-67, M/reaper.rs, M/border_manager/mod.rs:168-178). That NSWorkspace posts on main, and where exactly the CG callback is delivered, is Apple behaviour and not verified.
AppKit objects are created or touched on main only: NSScreen::screens(MainThreadMarker::new().unwrap()) panics off main (M/macos_api.rs:213; the same in M/splash.rs:166); closures dispatched to main use MainThreadMarker::new_unchecked() (M/border_manager/ns_window.rs:55, M/animation/ghost.rs:142, M/macos_api.rs:66).
Other threads and their locks
| Thread | Started at | WM lock | Platform calls |
|---|---|---|---|
| event consumer | M/process_event.rs:70-86 |
held for the whole event (:74) |
every Window method (AX), CoreGraphicsApi, activate_finder (:791), border_manager::destroy_all_borders (:128, 139) |
| "mdm" poller | M/process_event.rs:52-67 |
none | spawns komorebic splash |
| command accept + per-client threads | M/process_command.rs:79-110 |
try_lock_for(1 s) (:1882) |
everything, incl. destroy_all_borders (:294, 303, 1776, 1802, 1806), window.observe(&run_loop) (:1758) |
| monitor reconciliator | M/monitor_reconciliator.rs:71-90 |
per notification: a short lock for a state snapshot (:96-99, released), then see the dispatch table |
exec_sync to main (below) |
| reaper | M/reaper.rs:41-70 |
held per notification (:61) |
Window::is_valid (AX round trip per window) |
| settle | M/settle.rs:78-88 |
wm.lock() (:88) |
AX reads with a 0.25 s timeout (LOOK_TIMEOUT :39) |
| workspace reconciliator | M/workspace_reconciliator.rs:72-98 |
held per notification (:98) |
focus and workspace switches |
| border thread | M/border_manager/mod.rs:323-338, 353 |
WM lock for a snapshot, released at :381 (drop(state)), then BORDER_STATE and WINDOWS_BORDERS locked (:462-463) |
creates borders: exec_sync (M/border_manager/border.rs:153) and a blocking rx.recv() waiting for the main queue to build the NSWindow (M/border_manager/ns_window.rs:52-135, no timeout) |
| theme manager | M/theme_manager.rs:53 |
not taken | destroy_all_borders (:227) |
| animator frame thread | M/animation/animator.rs:122-126 |
never: "Nothing in here holds the window manager's lock" (:15-16) |
none itself; it commits to lanes |
| one lane thread per pid | M/animation/animator.rs:597-600 |
never | AX writes and verifies (:819-850, 902, 925), and border_manager::follow (an exec_async, below) |
| ctrl-c waiter | M/main.rs:295-310 |
none | sets a flag the main loop reads |
| deadlock detector (feature) | M/main.rs:151-174 |
none |
Every dispatch to the main queue
| Site | Kind | Closure | Takes WM lock on main? | Calling threads, and are they holding the WM lock? |
|---|---|---|---|---|
M/monitor_reconciliator.rs:106 |
exec_sync |
wm.lock() then update_monitor_work_areas (NSScreen) (:107-110) |
Yes | reconciliator thread, not holding it (the snapshot lock is released) |
M/monitor_reconciliator.rs:117 |
exec_sync |
latest_monitor_information (:118) |
No | reconciliator thread |
M/monitor_reconciliator.rs:383 |
exec_sync |
arc.lock() then load_monitor_information (:384-386) |
Yes | reconciliator thread, which first drop(wm) "because the main thread needs the lock" (:379-381) |
M/border_manager/mod.rs:157 (hide_borders_on) |
exec_sync |
hide NSWindows |
No | M/window_manager.rs:1385: under the WM lock |
M/border_manager/mod.rs:307 (destroy_border) |
exec_sync |
invalidate the border's AX observer | No | destroy_all_borders (while holding BORDER_STATE, :240) from the event thread (M/process_event.rs:128, 139), command threads (M/process_command.rs:294, 303, 1776, 1802, 1806), retile_all (M/window_manager.rs:2286): all under the WM lock; and 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:
- A thread E (the event consumer, or a command thread) holds the WM lock for the whole of a message (
M/process_event.rs:74;M/process_command.rs:1882). - While holding it, E calls a blocking main-queue dispatch from the table above:
destroy_all_borders(space change,M/process_event.rs:128, 139;retile_all; several commands),hide_borders_on(workspace switch with ghosts,M/window_manager.rs:1385) oractivate_finder(M/process_event.rs:791). - The monitor reconciliator thread R has dispatched
exec_sync(closure)for aResizeorDisplayConnectionChange(M/monitor_reconciliator.rs:106, 383) and the main thread has started that closure, which callswm.lock()and therefore waits for E.
Then E waits for the main queue (which is inside R's closure), the main thread waits for E's lock, and neither can proceed. The existing mitigations protect only their own pair of threads: R drops the lock before its second dispatch (:379-381), and apply_follows uses try_lock (M/border_manager/mod.rs:196-205). deadlock_detection (parking_lot::deadlock::check_deadlock, M/main.rs:151-174) would show main blocked on a parking_lot mutex but cannot see that E is waiting on a dispatch semaphore, so no cycle appears in its lock graph. The exposure window is small (a display change at the instant of one of those events). The structural fix, outside this work, is that no closure run on the main thread takes the WM lock. Relevance for the seam: this is why consequence 4 (no Backend method blocks on main while the WM lock is held) is a rule the extraction must not violate; today the rule is already violated at the sites above.
AX observers and the run loop. Window::observe adds an observer source to the main run loop from whichever thread calls it: the event thread (M/process_event.rs:614), a command thread (M/process_command.rs:1758) and main at start-up (M/macos_api.rs:341, 349). Border observers are the opposite: added inside an exec_sync on main (M/border_manager/border.rs:153-170). Whether adding a source to a run loop from a thread that does not own it is safe is a CoreFoundation property that I did not verify (Apple's threading guide lists CFRunLoopRef as thread-safe, from memory, not checked). The code asserts it for windows and avoids it for borders.
Thread-safety assertions in the code. AccessibilityUiElement and AccessibilityObserver are unsafe impl Send + Sync (M/lib.rs:282-284, 300-302), NsWindow is Send (M/border_manager/ns_window.rs:38), Still (a CGImage) is Send + Sync (M/animation/capture.rs:19-20), and the animator clones WindowHandle { element, app } into lane threads (M/animation/animator.rs:90-93). So AX elements already travel between threads; that this is allowed by the AX API is not verified.
4. Identity
| Window id | Monitor id | What the platform needs to act on a window | |
|---|---|---|---|
| KDE | u32, Copy: Window { id } (K/window.rs:78-86) |
u32 (K/monitor.rs:42, 64) |
the KWin UUID string, found in the backend's own table: by_uuid (K/kwin/state.rs:153-154), ids assigned by a counter (id_for :603-611), resolved for every command (KWinBackend::command K/kwin/mod.rs:797-806) |
| Windows | isize HWND, Copy: Window { hwnd } (W/window.rs:68-72) |
isize HMONITOR (W/monitor.rs:33, 57) |
nothing else: the HWND is the OS handle; HWND(as_ptr!(hwnd)) is a cast (W/windows_api.rs:177-181) |
| macOS | u32 CGWindowID, but Window is not a handle: it owns element: AccessibilityUiElement, application: Application, observer: AccessibilityObserver, details and is Clone, not Copy (M/window.rs:417-427) |
u32 CGDirectDisplayID (M/monitor.rs:38, 60) |
the AXUIElement (every read and write), the application's AXUIElement and pid (focus, tabbed apps, the animator's enhanced-UI guard), the AXObserver (observation only) |
4.1 Windows: does an HWND fit in 32 bits?
The code never narrows an HWND: it is an isize end to end, cast to a pointer with as_ptr! (W/windows_api.rs:177-181); the socket type is DebugWindow(isize) (W/core/mod.rs:273), later i64 by decision D15; grep finds no hwnd as u32/i32/u64 except an animation delay jitter start + Duration::from_millis(hwnd as u64) (W/animation/snapshot.rs:920) that does not depend on the width. So the Windows code assumes nothing about 32-bit significance and does not need it. For a shared WindowId(u64): a positive isize converts losslessly; a negative one (a sign-extended 32-bit value) would not map to a small u64.
Observation, not a guarantee: the Windows guest logs and state dumps from the user's earlier e2e runs (~/e2e-w0, read only) contain 440 distinct HWND values from 12345 to 7,804,828; none negative, none above 2^32 (grep -rhoaE 'hwnd[\\": =]+-?[0-9]{4,}'; 12345 looks like a test constant). Microsoft documents that handles are 32-bit significant for interoperability between 32- and 64-bit processes; I did not check that text here, so treat "HWND < 2^32" as likely but unproven (section 8).
4.2 macOS: acting on a window from a bare id
Today nothing on macOS acts from an id: callers hold a Window (cloned freely) or a bare element. AdhocWindow acts on an AXUIElement with no Window (M/window.rs:1436-1530: minimize, close, hide, restore, raise, used from M/process_event.rs:594 and M/process_command.rs:327-332), and the animator keeps WindowHandle { element, app } per id (M/animation/animator.rs:90-93). A Backend that takes only a WindowId therefore needs a registry that owns, per id:
- the window
AXUIElement(set frame, minimize, raise, close, alive check); - the application
AXUIElementand pid (focus uses the pid and the id for SkyLight,M/window.rs:1089, and the application's name andmain_window()for tabbed apps,:1105-1129; the animator uses the app element,M/window.rs:1065-1067); - the
AXObserver(only to keep or release observation;Window::newcreates one per window,M/window.rs:554-570); - a way to learn it is dead: an AX error on any call (
M/reaper.rs:164-173), a destroyed notification, orWindow::is_valid(M/window.rs:572).
Where macOS builds the id-to-element link today: at start-up by matching the CoreGraphics list to AX windows by title (M/macos_api.rs:326-349, M/application.rs:195-208), and afterwards from each AX notification, where the id comes from the private _AXUIElementGetWindow (M/accessibility/mod.rs:31-39, M/accessibility/private.rs:12-16), which can fail (window_id(..).ok() at M/window.rs:294). The registry therefore has to be fed from both, and an id the registry has never seen is not actionable.
KDE is the precedent: its Backend also takes only ids and keeps an id-to-UUID table (above).
4.3 Where per-window platform data lives today
- KDE:
kwin/state.rsTable(entries,by_uuid,next_id, park desktop) for backend data;HIDDEN_WINDOWS(K/window_manager.rs:52, 1401) in the core for what komorebi has hidden. - Windows: nothing in
Window; globals keyed by HWND:HIDDEN_HWNDS(W/lib.rs:89),known_hwnds(W/window_manager.rs:103), border state keyed by container id withwindow_border(hwnd)(W/border_manager/mod.rs), the cloak record (W/cloaked.rs), the live-resize and animation tables. - macOS: in
WindowandApplicationthemselves (element, observer, details), plus globals keyed by id:WINDOW_RESTORE_POSITIONS(M/lib.rs:121),PARKED_WINDOWS(M/window.rs:121),ARRIVALS(M/arrival.rs:40-41), the animator'sTracks(M/animation/animator.rs:95-98), the border map; andWindowManager::applications: HashMap<i32, Application>keyed by pid (M/window_manager.rs:122) andknown_window_ids(:146).
5. Coordinates
The shared rectangle. komorebi_layouts::Rect { left, top, right, bottom }: i32 where left, top are the origin and right, bottom are the width and height (doc comments, komorebi-layouts/src/rect.rs:19-28; add_padding shrinks right/bottom by twice the padding, :113-120). All three trees re-export it (W/core/mod.rs:43, K/core/mod.rs:46, M/core/mod.rs:33). Consequence: the name right is a trap, and From<Rect> for RECT (:43-51) copies the fields unchanged, so the result is not a valid Win32 RECT (right/bottom are a width and height); it exists only to feed SetWindowPos.
| Origin and direction | Unit | Frame measured | Where conversions live | |
|---|---|---|---|---|
| Windows | virtual screen, top-left origin, y down (secondary monitors may have negative coordinates: not verified) | physical pixels: the process is PerMonitorV2 (W/main.rs:223, W/windows_api.rs:1169-1172) |
window_rect = visible bounds (DWMWA_EXTENDED_FRAME_BOUNDS, no shadow), falling back to GetWindowRect (W/windows_api.rs:789-802); RECT to Rect subtracts (komorebi-layouts/src/rect.rs:32-41) |
shadow compensation in position_window (W/windows_api.rs:534-540) and move_window (:658-664), offset computed per call by shadow_rect (two syscalls, :808-821); monitor rects from win32_display_data (:1151-1152); the DPI scaling that window_rect would need is a TODO comment (:793-796) |
| KDE | KWin's global logical space, top-left, y down | logical (device-independent) pixels (MonitorInfo doc K/backend/mod.rs:93) |
KWin frameGeometry both ways (K/kwin/bridge.js:217, 378-381); rounded to integers in the script (:162-164) |
the script (bridge.js) and K/kwin/state.rs; the output's devicePixelRatio is sent (bridge.js:239) but the daemon never reads it (no scale in K/kwin/state.rs or K/kwin/mod.rs) |
| macOS | Quartz global display space: top-left origin of the main display, y down (AX position, CGDisplayBounds) |
points, never pixels (below) | the AX position and size of the window (M/macos_api.rs:385-427) |
CGRect to Rect truncates each f64 with as i32 (komorebi-layouts/src/rect.rs:67-76), so fractional points are lost; NSScreen.visibleFrame (AppKit, bottom-left origin) is flipped into Quartz with the main display's height (M/macos_api.rs:275-291); the border and ghost NSWindows flip back the same way (M/border_manager/border.rs:42-55, M/animation/ghost.rs:143-150); WindowBounds to Rect (M/core/rect_ext.rs:20-28) |
macOS Retina: points versus pixels (previously UNVERIFIED, now read). There is no backingScaleFactor, scale_factor, CGDisplayPixels* or contentsScale anywhere in komorebi-for-mac/komorebi/src (grep -rni). Monitor sizes are CGDisplayBounds (M/core_graphics/mod.rs:56-58) and NSScreen.visibleFrame (points), windows are read and written through AX (points), and the whole layout therefore runs in points. The only place pixels appear is ghost capture: the CGImage width and height "in pixels, which is more than the size of the window in points on a retina display" (M/animation/capture.rs:19-24). Not verified: how AX reports on scaled (non-native) resolutions or on mixed-DPI arrangements, which needs a Mac.
Two small traps for a shared geometry layer.
CoreGraphicsApi::contains_rect(smaller, bigger)isCGRectIntersectsRect(M/core_graphics/mod.rs:33-35), so macOSRect::contains(M/core/rect_ext.rs:13-17) means intersects; it is what assigns a window to a monitor at start-up (M/macos_api.rs:331).- A window's visible frame is not the frame the OS lets you set on Windows; set and get agree only because
window_rectreturns the visible bounds andposition_windowadds the shadow offset back (W/windows_api.rs:534-540). The Windowsset_positioncompareswindow_rectto the layout before writing (W/window.rs:394-396), so the two must stay consistent. KDE and macOS have no such offset in the code; whether macOS AX frames include a shadow is not verified.
The shared fake would have to model a frame offset to exercise the Windows case (plan 7b, "frame offsets"); FakeBackend has none today (section 3.1, finding 4).