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<u32, Window>
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.
This commit is contained in:
LGUG2Z committed 2026-10-31 10:10:15 -07:00
1 parent 4c9094d776
commit 386c5a79c6
4 files changed
+132 -13

No files matched your search

@@ -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<Registry<AxEntry>> = 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<AxEntry> {
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<Window> {
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<Window> {
WINDOWS
.snapshot()
.into_iter()
.filter_map(|(id, entry)| Some(Window::from_entry(id.to_u32()?, entry)))
.collect()
}
+1
View File
@@ -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;
+7 -13
View File
@@ -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<HashMap<u32, Window>>,
monitors: Mutex<Vec<MonitorInfo>>,
hiding_position: WindowHidingPosition,
batches: Mutex<Vec<BatchedUpdates>>,
@@ -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<Window> {
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
+20
View File
@@ -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<CGRect, AccessibilityError> {
MacosApi::window_rect(&self.element)