From 386c5a79c619da076ba58034cf7fea652979a4b4 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 31 Oct 2026 10:08:57 -0700 Subject: [PATCH] refactor(mac): ax_registry, the id -> {element, observer, application} table Step 11(ii), first part. New module ax_registry.rs instantiates komorebi_backend::Registry (rules tested natively in 4c9094d7) with AxEntry {element, observer, application with its pid}; lifetime and thread rules are written at the top of the file: entries end on Window::drop of a dead window, reaper reaping and application termination (wired in the next revisions); nothing in it dispatches to main or takes the WM lock; removed entries are dropped after the table's lock is released. Window gets entry() and from_entry() to convert. MacosBackend drops its private HashMap and looks windows up here (register and forget delegate), so the compile-only adapter now resolves an id the same way the daemon will. BEHAVIOUR: none yet, nothing registers (the adapter is still unused by the daemon). Verified: macOS fake-CC type-check (tools/fast-check) PASS. Not verified: cannot run on Linux. --- komorebi-for-mac/komorebi/src/ax_registry.rs | 104 ++++++++++++++++++ komorebi-for-mac/komorebi/src/lib.rs | 1 + .../komorebi/src/macos_backend.rs | 20 ++-- komorebi-for-mac/komorebi/src/window.rs | 20 ++++ 4 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 komorebi-for-mac/komorebi/src/ax_registry.rs diff --git a/komorebi-for-mac/komorebi/src/ax_registry.rs b/komorebi-for-mac/komorebi/src/ax_registry.rs new file mode 100644 index 00000000..b72a13eb --- /dev/null +++ b/komorebi-for-mac/komorebi/src/ax_registry.rs @@ -0,0 +1,104 @@ +//! The id -> {AX element, pid, observer} table of step 11(ii): what lets anything act on a +//! window from nothing but its `WindowId`. +//! +//! It is `komorebi_backend::Registry` (whose rules are tested natively: thread-safe, nothing of +//! the platform runs under its lock, stale ids are `Error::WindowGone`) instantiated with +//! [`AxEntry`]. +//! +//! # Lifetime +//! +//! An entry is created by `Window::new` and by [`sync`] when a window's element or id changes +//! (a native tab). It ends at every place the daemon learns of a death: +//! +//! - `Window::drop`, when the window is no longer alive ([`forget`]); +//! - the reaper, when it reaps a window ([`forget`]); +//! - an application terminating ([`forget_pid`], from `WindowManagerEvent::Destroy`). +//! +//! A window that is alive and never managed stays registered until it dies or its application +//! quits: the entry only holds retained references that the `Window` clones held anyway. +//! +//! # Threads +//! +//! Callable from any thread. Nothing here dispatches to the main queue or takes the window +//! manager's lock (`docs/dev/seam.md` section 3.3); a closure that runs on the main thread may +//! call it. Handles removed from the table are dropped after the table's lock is released. + +use crate::AccessibilityObserver; +use crate::AccessibilityUiElement; +use crate::application::Application; +use crate::window::Window; +use komorebi_backend::Registry; +use komorebi_backend::Result; +use komorebi_backend::WindowId; +use std::sync::LazyLock; + +/// What it takes to act on one window: its element and observer, and its application (whose +/// element and observer are the application's, and whose `process_id` is the pid). +#[derive(Debug, Clone)] +pub struct AxEntry { + /// The window's Accessibility element + pub element: AccessibilityUiElement, + /// The observer that reports the window's events + pub observer: AccessibilityObserver, + /// The application that owns the window + pub application: Application, +} + +impl AxEntry { + /// The pid of the owning application + pub fn pid(&self) -> i32 { + self.application.process_id + } +} + +static WINDOWS: LazyLock> = LazyLock::new(Registry::new); + +/// Registers the window under its id, replacing what was registered under it. +pub fn register(window: &Window) { + let entry = window.entry(); + let pid = entry.pid(); + // the replaced entry is dropped here, with the table's lock released + drop(WINDOWS.insert(WindowId::from_u32(window.id), pid, entry)); +} + +/// A window changed its id or its element in place (a native tab that is now shown by another +/// element): forget the old id and register the window as it is. +pub fn sync(old_id: u32, window: &Window) { + if old_id != window.id { + forget(old_id); + } + + register(window); +} + +/// Forgets a window that is gone. A second call is not an error. +pub fn forget(id: u32) { + drop(WINDOWS.remove(WindowId::from_u32(id))); +} + +/// Forgets every window of a process that quit. +pub fn forget_pid(pid: i32) { + drop(WINDOWS.remove_pid(pid)); +} + +/// The entry of a window, or `Error::WindowGone` if the id is stale. +pub fn get(id: WindowId) -> Result { + WINDOWS.get(id) +} + +/// A `Window` made from the entry of the id, or `Error::WindowGone` if the id is stale. +pub fn window(id: WindowId) -> Result { + let entry = WINDOWS.get(id)?; + // an id that does not fit a `CGWindowID` cannot have been registered + let raw = id.to_u32().ok_or(komorebi_backend::Error::WindowGone(id))?; + Ok(Window::from_entry(raw, entry)) +} + +/// Every registered window, by id. +pub fn windows() -> Vec { + WINDOWS + .snapshot() + .into_iter() + .filter_map(|(id, entry)| Some(Window::from_entry(id.to_u32()?, entry))) + .collect() +} diff --git a/komorebi-for-mac/komorebi/src/lib.rs b/komorebi-for-mac/komorebi/src/lib.rs index c71b0d16..91909a7e 100644 --- a/komorebi-for-mac/komorebi/src/lib.rs +++ b/komorebi-for-mac/komorebi/src/lib.rs @@ -54,6 +54,7 @@ pub mod animation; pub mod app_kit_notification_constants; pub mod application; pub mod arrival; +pub mod ax_registry; pub mod border_manager; pub mod container; pub mod core; diff --git a/komorebi-for-mac/komorebi/src/macos_backend.rs b/komorebi-for-mac/komorebi/src/macos_backend.rs index c6b8e352..ea4096ef 100644 --- a/komorebi-for-mac/komorebi/src/macos_backend.rs +++ b/komorebi-for-mac/komorebi/src/macos_backend.rs @@ -9,7 +9,8 @@ //! `docs/dev/seam.md`, section 9, and commented where they are. //! //! The trait names a window by an id, and nothing on macOS acts from a bare id: a window is its -//! Accessibility element. So the adapter owns the **id registry** of consequence (1): the daemon +//! Accessibility element. So the backend looks windows up in the **id registry** of consequence +//! (1), `ax_registry`: the daemon //! registers each `Window` it learns of ([`MacosBackend::register`]) and forgets it when it is //! gone, and every method looks the element up by id. An id the registry does not hold is a //! window that is gone. @@ -34,7 +35,6 @@ use komorebi_backend::WindowId; use komorebi_backend::WindowInfo; use objc2::MainThreadMarker; use parking_lot::Mutex; -use std::collections::HashMap; /// The shared [`Backend`] for macOS. /// @@ -42,7 +42,6 @@ use std::collections::HashMap; /// opacity and no focus follows mouse (`docs/dev/seam.md` section 2.1), so those methods answer /// `Unsupported`. pub struct MacosBackend { - windows: Mutex>, monitors: Mutex>, hiding_position: WindowHidingPosition, batches: Mutex>, @@ -64,7 +63,6 @@ impl MacosBackend { /// the monitor's configuration, and the trait has no place for it. pub fn new(hiding_position: WindowHidingPosition) -> Self { Self { - windows: Mutex::new(HashMap::new()), monitors: Mutex::new(Vec::new()), hiding_position, batches: Mutex::new(Vec::new()), @@ -73,12 +71,12 @@ impl MacosBackend { /// Makes the window known to the backend, by its `CGWindowID`. pub fn register(&self, window: Window) { - self.windows.lock().insert(window.id, window); + crate::ax_registry::register(&window); } /// Forgets a window that is gone. pub fn forget(&self, id: u32) { - self.windows.lock().remove(&id); + crate::ax_registry::forget(id); } /// Reads the monitors from `NSScreen` into the cache that [`Backend::monitors`] answers @@ -102,9 +100,7 @@ impl MacosBackend { } fn lookup(&self, id: WindowId) -> Result { - id.to_u32() - .and_then(|raw| self.windows.lock().get(&raw).cloned()) - .ok_or(Error::WindowGone(id)) + crate::ax_registry::window(id) } /// Says why a call about a window failed. A failed write is how macOS finds out that a window @@ -131,9 +127,7 @@ impl MacosBackend { id: WindowId::from_u32(window.id), pid: window.application.process_id, // the Accessibility position and size, in points; a failure leaves the default - rect: window.window_rect() - .map(Rect::from) - .unwrap_or_default(), + rect: window.window_rect().map(Rect::from).unwrap_or_default(), minimized: window.minimized(), alive: true, title: window.title(), @@ -153,7 +147,7 @@ impl Backend for MacosBackend { // The registry is the only list there is: the CoreGraphics list joined with the // Accessibility elements by title lives inside `load_workspace_information`, which fills // a `WindowManager`, and the daemon registers what it finds - let registered: Vec<_> = self.windows.lock().values().cloned().collect(); + let registered = crate::ax_registry::windows(); let mut windows: Vec<_> = registered.iter().filter_map(Self::info).collect(); windows.sort_by_key(|w| w.id); windows diff --git a/komorebi-for-mac/komorebi/src/window.rs b/komorebi-for-mac/komorebi/src/window.rs index 83279064..8dc9420e 100644 --- a/komorebi-for-mac/komorebi/src/window.rs +++ b/komorebi-for-mac/komorebi/src/window.rs @@ -569,6 +569,26 @@ impl Window { }) } + /// The handles that act on this window, as the registry keeps them + pub(crate) fn entry(&self) -> crate::ax_registry::AxEntry { + crate::ax_registry::AxEntry { + element: self.element.clone(), + observer: self.observer.clone(), + application: self.application.clone(), + } + } + + /// A window made from a registry entry + pub(crate) fn from_entry(id: u32, entry: crate::ax_registry::AxEntry) -> Self { + Self { + id, + element: entry.element, + application: entry.application, + observer: entry.observer, + details: None, + } + } + /// The frame query (`Backend::window_rect`): the Accessibility position and size, in points. pub fn window_rect(&self) -> Result { MacosApi::window_rect(&self.element)