This document is for contributors who want to understand OmniWM’s internals. It is not a user guide (see Documentation Home) or IPC/CLI reference (see IPC-CLI.md). For contribution process, see the Contribution Guide.
Prerequisites: Familiarity with Swift, macOS development concepts (AppKit, AXUIElement, CGWindowID), and basic tiling window manager concepts.
OmniWM is built with Swift Package Manager (Swift 6.2, strict concurrency). There are four targets with a clear dependency graph:
OmniWMIPC (zero dependencies — shared IPC protocol models)
^ ^
| \
OmniWMCtl OmniWM + GhosttyKit (CLI tool) (main library)
^
|
OmniWMApp (@main entry point)
| Target | Purpose | Dependencies |
|---|---|---|
OmniWMIPC |
Shared IPC data models and wire format | None |
OmniWMCtl |
CLI tool (omniwmctl) |
OmniWMIPC |
OmniWM |
Core window manager library | OmniWMIPC, GhosttyKit, system frameworks |
OmniWMApp |
Executable wrapper with SwiftUI scene | OmniWM |
Sources/
├── OmniWM/ Main library (~38K LOC)
│ ├── App/ Application bootstrap, delegate, updater,
│ │ and owned-window registry (5 files)
│ ├── Core/
│ │ ├── AppInfoCache.swift App icon/name cache
│ │ ├── CommandPaletteMode.swift Command palette mode enum
│ │ ├── PrivateAPIs.swift Private API declarations via @_silgen_name
│ │ ├── Animation/ Spring, cubic & workspace-switch animations (6 files)
│ │ ├── Ax/ Accessibility wrappers, DefaultFloatingApps (10 files)
│ │ ├── Border/ Focused window border rendering (3 files)
│ │ ├── Config/ Settings store, migrations, export, per-monitor settings (16 files)
│ │ ├── Controller/ WMController, event handlers, refresh pipeline (17 files)
│ │ ├── Input/ Hotkey action catalog, binding persistence,
│ │ │ and secure input monitoring (7 files)
│ │ ├── Layout/
│ │ │ ├── DNode.swift Shared types: WindowToken, WindowHandle
│ │ │ ├── LayoutBoundary.swift Layout snapshots & workspace geometry
│ │ │ ├── SideHiding.swift Side-hiding edge types
│ │ │ ├── Niri/ Scrolling columns layout engine (28 files)
│ │ │ └── Dwindle/ Binary space partition layout engine (5 files)
│ │ ├── LockScreen/ Lock screen detection (1 file)
│ │ ├── Menu/ Menu extraction for MenuAnywhere (3 files)
│ │ ├── Monitor/ Display detection, OutputId, restore assignments (5 files)
│ │ ├── Overview/ Bird's-eye workspace overview mode (9 files)
│ │ ├── Reconcile/ Runtime snapshot/trace, restore planning,
│ │ │ and persisted restore models (14 files)
│ │ ├── Rules/ Window rule evaluation engine (1 file)
│ │ ├── SkyLight/ Private macOS API wrappers (2 files)
│ │ ├── Sleep/ Sleep prevention manager (1 file)
│ │ ├── Support/ Utility types & extensions (3 files)
│ │ ├── Surface/ Shared surface policy, hit-testing,
│ │ │ and capture eligibility (2 files)
│ │ └── Workspace/ Workspace model, session state,
│ │ and runtime coordination (6 files)
│ ├── IPC/ IPC server, connections, routing (9 files)
│ ├── QuakeTerminal/ Drop-down terminal, Ghostty integration (9 files)
│ └── UI/ SwiftUI settings, status bar, workspace bar,
│ command palette, hidden bar, updater popup
│ (34 files)
├── OmniWMApp/ 2 files: @main entry + settings redirect
├── OmniWMCtl/ 7 files: CLI parser, IPC client, renderer
└── OmniWMIPC/ 5 files: models, wire format, socket path
OmniWM has zero third-party package dependencies. All functionality is built on:
-framework SkyLight unsafe flagFrameworks/GhosttyKit.xcframework prepared outside git, providing terminal emulation for the Quake Terminal feature# Debug build
swift build
# Run tests
swift test
# Code quality
make lint # SwiftLint check
make format # SwiftFormat
make check # Verify formatting
# Create distributable app bundle
./Scripts/package-app.sh release true # Build, sign, notarize
./Scripts/package-app.sh debug false # Debug build only
The application starts in Sources/OmniWMApp/OmniWMApp.swift:
@main OmniWMApp (SwiftUI App)
└─ @NSApplicationDelegateAdaptor → AppDelegate
└─ applicationDidFinishLaunching()
└─ bootstrapApplication()
AppBootstrapPlanner.decision() evaluates two preconditions before booting:
┌─────────────────────────┐
│ AppBootstrapPlanner │
│ .decision() │
└────────┬────────────────┘
│
┌────────────┴────────────┐
│ "Displays have separate │
│ Spaces" disabled? │
└────────┬───────────┬─────┘
NO │ │ YES
│ │
┌──────────────┘ ┌────┴────────────┐
│ Show modal: │ Settings epoch │
│ .requireDisplays... │ matches? │
└─────────────────┘ └──┬──────────┬───┘
NO │ │ YES
│ │
┌────────────┘ ┌────┴────┐
│ Show modal: │ .boot │
│ .requireSettings │ (normal)│
│ Reset └─────────┘
└─────────────────┘
When the decision is .boot, finishBootstrap() runs:
applyPersistedSettings() — creates both layout engines, registers hotkeys, configures borders, workspaces, gaps, etc.SettingsStore, WMController, and UpdateCoordinator with SwiftUI redirect flowsCheck for Updates... actionThe updater is intentionally bootstrap-gated. Release polling and popup presentation do not run during the settings-reset gate or the Displays Have Separate Spaces gate.
WMController.setEnabled(true) triggers ServiceLifecycleManager.start():
startServices() connects all event plumbing:
LayoutRefreshController.setup() — display links, refresh schedulingAXEventHandler.setup() — SkyLight event observationHotkeyCenterMouseEventHandler.setup() — CGEvent tapsOmniWM is fundamentally reactive. It responds to two categories of events, processes them through a pipeline, and applies the resulting window frames:
┌──────────────────────────────────────────────────────────────────┐
│ EVENT SOURCES │
├──────────────────────────┬───────────────────────────────────────┤
│ System Events │ User Input │
│ (SkyLight/CGS) │ (Carbon/CGEvent) │
│ - Window created │ - Hotkey pressed │
│ - Window destroyed │ - Mouse moved/dragged │
│ - Frame changed │ - Scroll wheel (gestures) │
│ - Front app changed │ - IPC command (omniwmctl) │
│ - Title changed │ │
└──────────┬───────────────┴──────────┬───────────────────────────┘
│ │
v v
┌──────────────────┐ ┌────────────────────────┐
│ CGSEventObserver │ │ HotkeyCenter / │
│ │ │ MouseEventHandler / │
│ │ │ IPCCommandRouter │
└────────┬─────────┘ └──────────┬─────────────┘
│ │
v v
┌──────────────────┐ ┌──────────────────┐
│ AXEventHandler │ │ CommandHandler │
│ (window lifecycle│ │ (command routing │
│ & focus) │ │ & execution) │
└────────┬─────────┘ └────────┬─────────┘
│ │
└───────────┬───────────┘
v
┌───────────────────────┐
│LayoutRefreshController│
│ (scheduling, │
│ coalescing, │
│ debouncing) │
└───────────┬───────────┘
v
┌───────────────────────┐
│ Layout Engine │
│ (Niri or Dwindle) │
│ │
│ Input: window list, │
│ workspace geometry │
│ Output: [WindowToken: │
│ CGRect] frame map │
└───────────┬───────────┘
v
┌───────────────────────┐
│ AXManager │
│ .applyFramesParallel()│
│ │
│ Writes frames to │
│ windows via AX APIs │
└───────────────────────┘
Windows are identified at three levels, each serving a different purpose:
// 1. WindowToken — value type, used as dictionary keys everywhere
struct WindowToken: Hashable, Sendable {
let pid: pid_t // Process ID
let windowId: Int // SkyLight/CGS window ID
}
// 2. WindowHandle — reference type, identity-compared (===)
final class WindowHandle: Hashable {
var id: WindowToken
// hash/equality use ObjectIdentifier (reference identity)
}
// 3. AXWindowRef — accessibility bridge to the actual window
struct AXWindowRef: Hashable, @unchecked Sendable {
let element: AXUIElement // Accessibility handle for read/write
let windowId: Int // SkyLight window ID
}
Why three layers?
WindowToken is a lightweight value type that survives across relayouts, is Sendable, and works as a dictionary key without holding any reference to the accessibility system.WindowHandle provides reference identity for layout engine tree nodes — two handles wrapping the same token are NOT equal unless they are the same object.AXWindowRef is the bridge to macOS accessibility APIs for actually reading/writing window attributes (position, size, title). It holds the AXUIElement which is a heavyweight system resource.From creation to destruction, a window passes through these stages:
Creation:
CGSEventObserver receives .created(windowId, spaceId) from SkyLightAXEventHandler queries window attributes via accessibility APIs (role, subrole, title, size, buttons)WindowRuleEngine.evaluate() produces a WindowDecision:
.managed — tiled in the layout engine.floating — tracked but positioned independently.unmanaged — ignored entirely (e.g., system UI, panels)WindowModel creates an Entry, layout engine inserts a nodeLayoutRefreshController schedules a refresh to compute and apply framesDestruction:
CGSEventObserver receives .destroyed(windowId, spaceId)WindowModel removes the entryLayoutRefreshController schedules a windowRemoval refreshManaged Replacement:
Some apps (Ghostty, Safari, browsers) destroy and recreate windows during internal operations. AXEventHandler detects these patterns via ManagedReplacementMetadata correlation — matching a destroy+create pair within a 150ms grace period to preserve the window’s workspace assignment and position.
LayoutRefreshController is the central coordination point between events and window frame application. It manages scheduling, debouncing, and coalescing of layout refreshes.
Five Refresh Routes:
| Route | When Used | What It Does |
|---|---|---|
fullRescan |
Startup, app launch/termination, space change, display change | Full window enumeration + relayout |
relayout |
Config change, window created, window frame changed | Recompute layout from current state |
immediateRelayout |
User commands, gestures, workspace switch | Synchronous immediate layout |
visibilityRefresh |
App hidden/unhidden | Show/hide windows, no relayout |
windowRemoval |
Window destroyed | Remove from layout + relayout + focus recovery |
RefreshReason → Route Mapping:
Each RefreshReason maps to a route and a scheduling policy:
RefreshReason → Route → Scheduling
────────────────────────────────────────────────────────────
.startup → fullRescan → plain
.appLaunched → fullRescan → plain
.activeSpaceChanged → fullRescan → plain
.layoutCommand → immediateRelayout → plain
.interactiveGesture → immediateRelayout → plain
.workspaceTransition → immediateRelayout → plain
.axWindowCreated → relayout → debounced(4ms)
.axWindowChanged → relayout → debounced(8ms, dropWhileBusy)
.windowDestroyed → windowRemoval → plain
.appHidden / .appUnhidden → visibilityRefresh → plain
Coalescing: If a refresh is already in progress, incoming requests are merged into a pendingRefresh. When the active refresh completes, the pending refresh fires. This prevents redundant layout calculations during bursts of events.
DisplayLink Integration: When animations are active (spring-based viewport scrolling, workspace switch effects), a CADisplayLink per display fires at the native refresh rate, driving per-frame layout recalculation.
Both layout engines follow the same contract:
[WindowToken: CGRect] frame dictionary as outputThis separation means layout logic can be unit-tested without any macOS UI or accessibility infrastructure. The LayoutRefreshController feeds workspace snapshots to the active engine and collects frame outputs, then AXManager.applyFramesParallel() writes the frames to actual windows.
@MainActor everywhere. Nearly all code in OmniWM runs on the main thread, including:
Exceptions:
AppAXContext runs a dedicated thread per application for accessibility observer callbacks. These callbacks post back to the main actor.IPCApplicationBridge and IPCEventBroker are Swift actors handling concurrent client connections. They dispatch to @MainActor for any window management operations.CGSEventObserver uses OSAllocatedUnfairLock for the pending event buffer that bridges between the SkyLight callback thread and the main thread.File: Sources/OmniWM/Core/Controller/WMController.swift
WMController is the central object that owns or references every major subsystem. It does NOT contain business logic itself — it delegates to specialized handlers.
Handler constellation (all lazy-initialized, all hold weak var controller: WMController?):
| Handler | Responsibility |
|---|---|
commandHandler |
Routes HotkeyCommand cases to appropriate handler methods |
axEventHandler |
Processes window create/destroy events, manages replacement correlation |
mouseEventHandler |
CGEvent tap for mouse events, gestures, focus-follows-mouse |
mouseWarpHandler |
Warps cursor to focused window when configured |
layoutRefreshController |
Refresh scheduling, DisplayLink animation, frame application |
workspaceNavigationHandler |
Workspace switching, window-to-workspace moves |
windowActionHandler |
Window close, fullscreen toggle, float toggle |
serviceLifecycleManager |
App lifecycle, observer setup, permission polling |
borderCoordinator |
Orchestrates border updates after layout/focus changes |
focusNotificationDispatcher |
Publishes focus change events to IPC subscribers |
Core managers (owned directly):
| Manager | Purpose |
|---|---|
settings: SettingsStore |
Persisted user configuration |
workspaceManager: WorkspaceManager |
Workspace definitions, window tracking, session state |
axManager: AXManager |
Per-app accessibility contexts, frame application |
focusBridge: FocusBridgeCoordinator |
Focus state machine with retry logic |
windowRuleEngine: WindowRuleEngine |
Window rule evaluation |
hotkeys: HotkeyCenter |
Global hotkey registration via Carbon |
borderManager: BorderManager |
Focus border window management |
niriEngine: NiriLayoutEngine? |
Niri layout state (nil if not in use) |
dwindleEngine: DwindleLayoutEngine? |
Dwindle layout state (nil if not in use) |
animationClock: AnimationClock |
Monotonic time source for animations |
WorkspaceManager (Sources/OmniWM/Core/Workspace/WorkspaceManager.swift)
Owns workspace definitions, the window model, session state, monitor tracking, and the reconcile runtime used for debugging and relaunch restore behavior.
WorkspaceManager
├── monitors: [Monitor] Display geometry
├── workspacesById: [ID: WorkspaceDescriptor] Workspace names & monitor assignments
├── windows: WindowModel All tracked windows
├── reconcileTrace / runtimeStore Replayed runtime snapshot and trace state
├── restorePlanner Restore and rescue planning
├── bootPersistedWindowRestoreCatalog Relaunch restore intents loaded from settings
├── session: SessionState Ephemeral runtime state
│ ├── monitorSessions: [MonitorID: MonitorSession]
│ │ ├── visibleWorkspaceId
│ │ └── previousVisibleWorkspaceId
│ ├── workspaceSessions: [WorkspaceID: WorkspaceSession]
│ │ └── niriViewportState: ViewportState?
│ ├── focus: FocusSession
│ │ ├── focusedToken: WindowToken?
│ │ ├── pendingManagedFocus
│ │ ├── lastTiledFocusedByWorkspace
│ │ ├── lastFloatingFocusedByWorkspace
│ │ ├── isNonManagedFocusActive
│ │ └── isAppFullscreenActive
│ ├── scratchpadToken: WindowToken?
│ └── interactionMonitorId: Monitor.ID?
└── nativeFullscreenRecords Fullscreen transition tracking
Post-v0.4.5, WorkspaceManager also owns the reconcile runtime. RuntimeStore and ReconcileTraceRecorder capture normalized window-management events into a replayable snapshot, exposed through reconcileSnapshotDump() and reconcileTraceDump() for IPC diagnostics. PersistedWindowRestoreCatalog stores relaunch restore intent such as workspace target, preferred monitor, and floating geometry so managed floating windows can be restored or rescued across launches.
WindowModel (Sources/OmniWM/Core/Workspace/WindowModel.swift)
The single source of truth for all tracked windows. Each Entry contains:
struct Entry {
let handle: WindowHandle
let axRef: AXWindowRef
var workspaceId: WorkspaceDescriptor.ID
var mode: TrackedWindowMode // .tiling or .floating
var ruleEffects: ManagedWindowRuleEffects
var floatingState: FloatingState? // Last frame, normalized position
var hiddenReason: HiddenReason? // .workspaceInactive, .layoutTransient, .scratchpad
var manualLayoutOverride: ManualWindowOverride?
// ... constraints, parent kind, layout reason
}
Entries are indexed by both WindowToken and raw windowId for fast lookup from different event sources.
Directory: Sources/OmniWM/Core/Layout/Niri/
Niri arranges windows in vertical columns that scroll horizontally, inspired by the Niri Wayland compositor.
Node Tree:
NiriRoot (per workspace)
├── NiriContainer (column 1)
│ ├── NiriWindow (window A)
│ └── NiriWindow (window B) ← stacked vertically
├── NiriContainer (column 2)
│ └── NiriWindow (window C)
└── NiriContainer (column 3) ← can be tabbed
├── NiriWindow (window D) ← active tab
└── NiriWindow (window E) ← hidden tab
All three types inherit from NiriNode (base class with id: NodeId, parent, children, size, frame).
Key types:
| Type | Purpose |
|---|---|
NiriRoot |
Per-workspace container. Owns column list and node index. |
NiriContainer |
A column. Has displayMode (.normal or .tabbed), width: ProportionalSize, activeTileIdx. |
NiriWindow |
Leaf node. Has token: WindowToken, height: WeightedSize, constraints. |
ProportionalSize |
.proportion(CGFloat) or .fixed(CGFloat) — column width relative to monitor |
WeightedSize |
.auto(weight:) or .fixed(CGFloat) — window height within column |
ViewportState |
Horizontal scroll offset: .static, .gesture(ViewGesture), or .spring(SpringAnimation) |
NodeId |
UUID-based identifier for tree nodes |
Column width presets cycle through configurable proportions (default: 1/3, 1/2, 2/3). Full-width mode expands a column to fill the monitor.
Viewport scrolling: The viewport tracks which columns are visible. User gestures (trackpad swipe) drive the viewport via ViewGesture → SwipeTracker, which accumulates deltas and produces spring animations that snap to column boundaries.
File Organization (28 files):
The Niri directory is the largest subsystem. Files are organized by responsibility:
| Category | Files | Purpose |
|---|---|---|
| Core engine | NiriLayoutEngine.swift, NiriNode.swift, NiriLayout.swift |
Engine class, node tree (Root/Container/Window), pixel-rounding utilities |
| Navigation | NiriNavigation.swift |
Focus movement between columns and windows |
| Constraint solving | NiriConstraintSolver.swift |
NiriAxisSolver distributes space among windows respecting min/max size constraints |
| Monitor model | NiriMonitor.swift |
Per-monitor state: geometry, workspace roots, workspace switch animation |
| Viewport | ViewportState.swift, +Animation, +ColumnTransitions, +Geometry, +Gestures |
Horizontal scroll offset, spring physics, gesture tracking |
| Interactive move | InteractiveMove.swift, +InteractiveMove, DragGhostController.swift, DragGhostWindow.swift, SwapTargetOverlay.swift |
Mouse-driven window dragging with ghost thumbnail and swap target indicators |
| Interactive resize | InteractiveResize.swift, +InteractiveResize |
Mouse-driven edge resizing with ResizeEdge option set |
| Engine extensions | +Animation, +ColumnOps, +Monitors, +Sizing, +TabbedMode, +WindowOps, +Windows, +WorkspaceOps |
Modular engine operations (see 6.4) |
| UI overlays | TabbedColumnOverlay.swift |
Visual indicator for tabbed columns |
| Overview bridge | NiriOverviewSnapshot.swift |
Produces layout snapshots for the Overview renderer |
Interactive Move/Resize: Users can drag windows between columns using Option+Shift+click. InteractiveMove tracks the drag state (origin column, hover target). DragGhostController captures a ScreenCaptureKit thumbnail of the dragged window and displays it as a semi-transparent ghost. SwapTargetOverlay highlights the drop target. On release, the engine performs a column insertion or window swap. Interactive resize (InteractiveResize) allows edge-dragging to change column widths or window heights.
Constraint Solving: NiriAxisSolver (in NiriConstraintSolver.swift) distributes available space among windows in a column while respecting per-window min/max size constraints. Windows with isConstraintFixed get exact sizes; remaining space is distributed by weight. This runs during every layout calculation and handles edge cases like tabbed columns (all windows share the same height).
Directory: Sources/OmniWM/Core/Layout/Dwindle/
Dwindle recursively divides screen space using binary splits, similar to bspwm.
BSP Tree:
DwindleNode (split: horizontal, ratio: 0.5)
├── DwindleNode (leaf: window A)
└── DwindleNode (split: vertical, ratio: 0.5)
├── DwindleNode (leaf: window B)
└── DwindleNode (leaf: window C)
Key types:
final class DwindleNode {
let id: DwindleNodeId // UUID
var kind: DwindleNodeKind
var parent: DwindleNode?
var children: [DwindleNode] // 0 (leaf) or 2 (split)
// Animation properties for smooth transitions
}
enum DwindleNodeKind {
case split(orientation: DwindleOrientation, ratio: CGFloat)
case leaf(handle: WindowToken?, fullscreen: Bool)
}
enum DwindleOrientation {
case horizontal // Left/right split
case vertical // Top/bottom split
}
Smart split chooses orientation based on the available space dimensions. Preselection lets users choose where the next window will be inserted.
File: Sources/OmniWM/Core/Controller/KeyboardFocusLifecycleCoordinator.swift
Focus management is complex because OmniWM must coordinate its intent with what macOS actually does. The FocusBridgeCoordinator manages this:
The Deferred Focus Pattern:
1. User presses focus-left
2. CommandHandler identifies target window
3. FocusBridgeCoordinator.beginManagedRequest(token, workspaceId)
→ Creates ManagedFocusRequest with status = .pending
4. Private APIs activate the target app + window
(_SLPSSetFrontProcessWithOptions, makeKeyWindow)
5. macOS confirms focus via AX callback
6. FocusBridgeCoordinator.confirmManagedRequest(token, source)
→ Marks request as .confirmed
→ If no confirmation within retries, re-attempts activation
Key types:
| Type | Purpose |
|---|---|
KeyboardFocusTarget |
Resolved focus: token, axRef, workspaceId, isManaged |
ManagedFocusRequest |
In-flight request with requestId, retryCount, status (.pending/.confirmed) |
ActivationEventSource |
How focus was confirmed: .focusedWindowChanged (authoritative), .workspaceDidActivateApplication, .cgsFrontAppChanged |
Focus serialization: focusWindow(_:performFocus:onDeferredFocus:) serializes focus operations. If a focus request arrives while one is in-flight, it queues as pendingFocusToken and fires after the current request completes or times out.
Hotkeys (Sources/OmniWM/Core/Input/)
ActionCatalog is the source of truth for the 67 hotkey-triggerable actions. It defines each action’s title, category, layout compatibility, search terms, default and alternate bindings, and optional IPC command linkage. HotkeyBinding persists a bindings array per action, and HotkeyBindingRegistry canonicalizes both legacy single-binding payloads and newer multi-binding settings data.
HotkeyCenter flattens those action bindings and registers each key+modifiers combination via Carbon’s RegisterEventHotKey API, so a single action can be triggered by multiple shortcuts. Actions are still tagged with layout compatibility:
.shared — works with any layout (focus, move, workspace switch, float, scratchpad, UI toggles).niri — Niri-only (moveColumn, toggleColumnTabbed, focusPrevious, cycleColumnWidth).dwindle — Dwindle-only (moveToRoot, toggleSplit, swapSplit, preselect, resizeInDirection)Command routing (Sources/OmniWM/Core/Controller/CommandHandler.swift)
CommandHandler.performCommand() is a switch statement over all 67 HotkeyCommand cases, delegating to the appropriate handler. It first checks layout compatibility — a Niri command is ignored when Dwindle is active, and vice versa.
Mouse events (Sources/OmniWM/Core/Controller/MouseEventHandler.swift)
Uses CGEventTap for system-wide mouse event interception:
idle → armed → committed) for workspace switching via swipeSkyLight events (Sources/OmniWM/Core/SkyLight/CGSEventObserver.swift)
Registers for window server notifications via private APIs:
enum CGSWindowEvent {
case created(windowId, spaceId)
case destroyed(windowId, spaceId)
case frameChanged(windowId)
case closed(windowId)
case frontAppChanged(pid)
case titleChanged(windowId)
}
Events are buffered in a lock-protected PendingCGSEventState and drained on the main run loop via CFRunLoopPerformBlock. Frame change events are coalesced by windowId.
File: Sources/OmniWM/Core/Rules/WindowRuleEngine.swift
Evaluates windows against rules to produce a WindowDecision. Evaluation order (first match wins):
Key types:
struct WindowDecision {
let disposition: WindowDecisionDisposition // .managed, .floating, .unmanaged, .undecided
let source: WindowDecisionSource // .manualOverride, .userRule(UUID), .builtInRule, .heuristic
let workspaceName: String? // Target workspace (if rule specifies)
let ruleEffects: ManagedWindowRuleEffects // minWidth, minHeight constraints
}
struct WindowRuleFacts {
let appName: String?
let ax: AXWindowFacts // role, subrole, title, buttons
let sizeConstraints: WindowSizeConstraints?
let windowServer: WindowServerInfo?
}
For the protocol specification, wire format, and CLI command reference, see IPC-CLI.md. This section covers the internal code architecture.
omniwmctl OmniWM process
───────── ──────────────
CLIParser IPCServer
│ │
CLIRuntime acceptConnections() on DispatchQueue
│ │
IPCClient ──── Unix Socket ────► IPCConnection (per client)
(NDJSON) │
IPCApplicationBridge (actor)
│ auth check, protocol version
│
┌───────┼───────┐
│ │ │
IPCCommand IPCQuery IPCRule
Router Router Router
│ │ │
└───────┼───────┘
│ @MainActor
v
CommandHandler /
WorkspaceManager /
WindowRuleEngine
Key actors:
IPCApplicationBridge — Swift actor that receives deserialized requests, checks authorization, and dispatches to the appropriate router on @MainActorIPCEventBroker — Swift actor managing event subscriptions. Uses AsyncStream with continuations per channel per connection. IPCEventDemandTracker tracks whether any client is subscribed to a channel (so events aren’t computed when nobody is listening)Public surface registry: IPCAutomationManifest is the source of truth for public IPC commands, queries, rule actions, subscriptions, and CLI discoverability metadata (including completion/help surfaces). The routers execute the behavior; the manifest defines what is exposed.
Security: The trust boundary is the local macOS user account, not individual client processes. Each request carries a per-session authorization token stored in plaintext at <socket-path>.secret; the server also enforces socket permissions 0o600, creates new socket directories with 0o700, and verifies peer UID via getpeereid(). If OMNIWM_SOCKET points into an existing directory, OmniWM reuses that directory as-is instead of re-permissioning it, so custom socket paths should live in a private directory owned by the same user.
File: Sources/OmniWM/Core/Ax/AXManager.swift
Per-app threading model: AXManager maintains an AppAXContext per process. Each context runs an AX observer on a dedicated thread to receive accessibility callbacks (focused-window-changed, window-destroyed).
Frame application pipeline (applyFramesParallel()):
[WindowToken: CGRect]lastAppliedFrames — skip windows whose frame hasn’t changedframesByPidBufferAXUIElementAXFrameWriteResult with any errorsrecentFrameWriteFailures for retry budgetingInactive workspace suppression: Windows on non-visible workspaces are tracked in inactiveWorkspaceWindowIds. Frame writes to these windows are skipped, preventing unnecessary AX API calls and visual glitches.
Directory: Sources/OmniWM/Core/Animation/
SpringAnimation — critically-damped spring physics for smooth, responsive motion:
struct SpringConfig {
// Presets:
static let snappy = SpringConfig(response: 0.22, dampingFraction: 0.95)
static let balanced = SpringConfig(response: 0.30, dampingFraction: 0.88)
static let gentle = SpringConfig(response: 0.45, dampingFraction: 0.78)
static let reducedMotion = SpringConfig(response: 0.18, dampingFraction: 0.98)
}
Used for: viewport scrolling (Niri), workspace switch transitions, window movement animations.
CubicAnimation — cubic easing for Dwindle node transitions (position and size).
AnimationClock — monotonic time wrapper around CACurrentMediaTime().
DisplayLink integration: LayoutRefreshController manages a CADisplayLink per display. On each frame tick, it recalculates animated layouts and applies frames, producing 60/120Hz smooth animations.
Accessibility: All animation configs support resolvedForReduceMotion(), which returns the reducedMotion preset when the user has enabled “Reduce Motion” in macOS accessibility settings.
Files: Sources/OmniWM/Core/Border/BorderManager.swift, BorderWindow.swift
A lightweight NSWindow overlay that draws a rounded rectangle around the focused window:
BorderManager tracks the current focused window’s frame and windowIdBorderWindow renders the border using SkyLight private APIs for window ordering (stays above managed windows but below floating panels)| Feature | Key Files | Description |
|---|---|---|
| Overview | Core/Overview/OverviewController.swift |
Bird’s-eye view of all workspaces with window thumbnails (ScreenCaptureKit), search, drag-to-reorganize |
| Quake Terminal | QuakeTerminal/QuakeTerminalController.swift |
Drop-down terminal using GhosttyKit. Supports tabs and split panes. Toggles with hotkey. |
| Command Palette | UI/CommandPalette/CommandPaletteController.swift |
Fuzzy-search interface for windows, commands, and menu items |
| Menu Anywhere | UI/MenuAnywhere/MenuAnywhereController.swift |
UI controller that uses the Core menu extraction layer to display any app’s menu at cursor position |
| Workspace Bar | UI/WorkspaceBar/WorkspaceBarManager.swift |
Visual workspace indicators with window icons per workspace |
| Hidden Bar | UI/HiddenBar/HiddenBarController.swift |
Collapsible menu bar icon management |
| Scratchpad | Core/Workspace/WorkspaceManager.swift |
Tracks the transient scratchpad window via scratchpadToken(). Show/hide and focus recovery are coordinated by WMController. |
| Status Bar | UI/StatusBar/StatusBarController.swift |
Menu bar icon with settings access, manual update checks, and workspace summary |
| Release Updater | App/UpdateCoordinator.swift, UI/UpdateWindowController.swift |
Polls the latest GitHub release once per day on launch, supports manual checks from Settings and the status bar, and shows a manual-action popup with release notes |
OmniWM utility windows such as Settings, App Rules, Sponsors, and the updater popup still register through OwnedWindowRegistry, but that type now acts as a facade over SurfaceCoordinator and SurfaceScene. The shared surface system assigns each owned UI surface a SurfaceKind and SurfacePolicy, centralizing hit-testing, screen-capture inclusion, and managed-focus-recovery suppression across overview, workspace bar, border, quake, and utility windows.
User presses a hotkey (e.g., Option+Left to focus left):
Carbon EventHandler callback
│
v
HotkeyCenter.dispatch(id)
│ lookup HotkeyCommand by registration ID
v
CommandHandler.handleCommand(.focus(.left))
│ check: isEnabled? layout compatible? overview open?
v
layoutHandler(as: LayoutFocusable.self)?.focusNeighbor(direction: .left)
│ e.g., NiriLayoutHandler.focusNeighbor()
│ determines target window in the Niri tree
v
FocusBridgeCoordinator.focusWindow(targetToken)
│ activates app + window via private APIs
v
LayoutRefreshController.scheduleRefresh(.immediateRelayout, reason: .layoutCommand)
│
v
NiriLayoutEngine.calculateLayout(...)
│ produces [WindowToken: CGRect]
v
AXManager.applyFramesParallel(frames)
│ writes new positions to windows
v
BorderCoordinator.updateBorder(for: targetToken)
│ moves border to newly focused window
v
FocusNotificationDispatcher.publish(focusEvent)
│ notifies IPC subscribers
v
Done
An application opens a new window:
macOS window server creates window
│
v
CGSEventObserver receives .created(windowId, spaceId)
│ buffered in PendingCGSEventState (lock-protected)
│ drained via CFRunLoopPerformBlock on main thread
v
AXEventHandler.handleWindowCreated(windowId)
│ creates AXWindowRef from AXUIElement
│ queries: role, subrole, title, buttons, size
v
WindowRuleEngine.evaluate(facts)
│ returns WindowDecision (.managed / .floating / .unmanaged)
v
WindowModel.track(handle, axRef, workspaceId, mode)
│ creates Entry, indexes by token and windowId
v
NiriLayoutEngine.insertWindow(token, into: workspaceRoot)
│ creates NiriWindow node, appends to active column or new column
v
LayoutRefreshController.scheduleRefresh(.relayout, reason: .axWindowCreated)
│ debounced: 4ms
v
Layout calculation → AXManager.applyFramesParallel()
│
v
All windows repositioned to accommodate the new one
User runs omniwmctl command focus left:
CLIParser.parse(["command", "focus", "left"])
│ produces IPCRequest { kind: .command, payload: .command(.focus(direction: .left)) }
v
IPCClient connects to Unix socket (~/.../ipc.sock)
│ sends NDJSON: {"version":3,"id":"...","kind":"command","authorizationToken":"...","payload":{"name":"focus","arguments":{"direction":"left"}}}\n
v
IPCServer accepts connection → IPCConnection reads line
│ deserializes to IPCRequest
v
IPCApplicationBridge.response(request) [actor]
│ verifies authorization token
│ checks protocol version
v
IPCCommandRouter.handle(.focus(direction: .left)) [@MainActor]
│ maps to HotkeyCommand.focus(.left)
v
CommandHandler.performCommand(.focus(.left))
│ (same flow as hotkey from here — see 5.1)
│ returns ExternalCommandResult
v
IPCResponse { ok: true } → serialized as NDJSON → sent to client
v
CLIRenderer displays result
Sources/OmniWM/Core/Input/HotkeyCommand.swift:
case myNewCommand
Set layoutCompatibility (.shared, .niri, or .dwindle).
Sources/OmniWM/Core/Controller/CommandHandler.swift:
case .myNewCommand:
// implementation or delegation to a handler
Add the action spec in Sources/OmniWM/Core/Input/ActionCatalog.swift so the command has its title, category, search metadata, and default or alternate bindings. DefaultHotkeyBindings.swift is only a thin wrapper over this catalog.
Expose via IPC in Sources/OmniWM/IPC/IPCCommandRouter.swift — add the routing to the new command when it should be scriptable.
Add CLI support in Sources/OmniWMCtl/CLIParser.swift — add the command name.
Sources/OmniWMIPC/IPCAutomationManifest.swift — add the command description.Actions can carry multiple persisted bindings, so any extra default shortcuts should be modeled in ActionCatalog rather than as separate commands.
Define the response model in Sources/OmniWMIPC/IPCModels.swift.
Sources/OmniWM/IPC/IPCQueryRouter.swift:
case "my-query":
let result = // gather data from WorkspaceManager, etc.
return .success(result)
Add CLI rendering in Sources/OmniWMCtl/CLIRenderer.swift — format the response for terminal output.
Add CLI parsing in Sources/OmniWMCtl/CLIParser.swift — add the query name.
Sources/OmniWMIPC/IPCAutomationManifest.swift.Add the property to Sources/OmniWM/Core/Config/SettingsStore.swift with a UserDefaults key.
Wire the runtime behavior in WMController.applyPersistedSettings() or the relevant handler that consumes the setting.
Add UI in the appropriate settings tab under Sources/OmniWM/UI/.
Update config export/import in Sources/OmniWM/Core/Config/SettingsExport.swift for persisted user preferences that belong in editable config so full export, compact backup, and import all round-trip correctly. Do not export remote payloads or operational cache state such as updater release notes, release URLs, last-check timestamps, or skipped-release markers.
Check config-file touchpoints when the change affects config discoverability or UX. Sources/OmniWM/UI/ConfigFileWorkflow.swift is the generic workflow layer, and the Config File section in Sources/OmniWM/UI/SettingsView.swift is the main user-facing entry point; most new settings do not need workflow code changes, but contributor-facing config behavior and copy should remain accurate.
Handle migration if needed in Sources/OmniWM/Core/Config/SettingsMigration.swift.
Add round-trip coverage in tests: verify the setting survives store load/save and config export/import so it cannot silently disappear from ~/.config/omniwm/settings.json.
Identify the engine: Niri code is in Sources/OmniWM/Core/Layout/Niri/, Dwindle in Sources/OmniWM/Core/Layout/Dwindle/.
NiriLayoutEngine+Animation.swift — animation tick and spring updatesNiriLayoutEngine+ColumnOps.swift — column add/remove/reorderNiriLayoutEngine+InteractiveMove.swift — mouse-driven window movingNiriLayoutEngine+InteractiveResize.swift — mouse-driven edge resizingNiriLayoutEngine+Monitors.swift — multi-monitor layoutNiriLayoutEngine+Sizing.swift — width/height calculationNiriLayoutEngine+TabbedMode.swift — tabbed column logicNiriLayoutEngine+WindowOps.swift — window insert/remove/reorderNiriLayoutEngine+Windows.swift — window query and lookupNiriLayoutEngine+WorkspaceOps.swift — workspace-level operationsFocus navigation lives in NiriNavigation.swift. Constraint solving lives in NiriConstraintSolver.swift.
calculateLayout(), assert frame positions.OmniWM uses SkyLight (private macOS framework) for low-latency window operations. The wrapper pattern is:
@_silgen_name in Sources/OmniWM/Core/PrivateAPIs.swiftdlopen/dlsym in Sources/OmniWM/Core/SkyLight/SkyLight.swift for functions that can’t use @_silgen_nameRisk model: Private APIs can break across macOS versions. When adding new private API usage, provide a fallback path using public APIs where possible, and test across macOS versions.
Runner: swift test via SwiftPM. Requires macOS 15+.
Test directory: Tests/OmniWMTests/ (55 files: 52 test files + 3 support files)
Test patterns:
| Pattern | Used For | Example |
|---|---|---|
| Direct unit tests | Layout engines, animation math, rule evaluation | Create nodes, call calculateLayout(), assert frames |
| DI via closures | Controllers, handlers | nativeFullscreenStateProvider, frameApplyOverrideForTests |
| Debug hooks | Refresh pipeline | RefreshDebugHooks.onFullRescan, onRelayout |
| In-process IPC | IPC protocol, routing | Create socket pair, send/receive in-process |
Key test support files:
TestSharedStateSupport.swift — shared test fixturesTokenCompatibilityTestSupport.swift — window token creation helpersLayoutPlanTestSupport.swift — layout test utilitiesWhat’s hard to test: Anything requiring live accessibility permissions or actual window manipulation. These are covered by the override/hook pattern — production code checks for test overrides (closures/hooks) and uses them instead of real system calls.
| Term | Definition |
|---|---|
WindowToken |
Value type (pid + windowId) identifying a window. Used as dictionary keys throughout. |
WindowHandle |
Reference-type wrapper around WindowToken. Identity-compared (===). Used in layout trees. |
AXWindowRef |
Accessibility bridge (AXUIElement + windowId) for reading/writing window properties. |
TrackedWindowMode |
.tiling or .floating — whether a window is managed by the layout engine. |
WorkspaceDescriptor |
A workspace definition: id (UUID), name, optional assignedMonitorPoint. |
SessionState |
Ephemeral runtime state in WorkspaceManager: focused window, visible workspace per monitor, viewport states. |
NiriRoot / NiriContainer / NiriWindow |
The three-level Niri layout tree: root → columns → windows. |
DwindleNode |
BSP tree node. Kind is either .split(orientation, ratio) or .leaf(handle, fullscreen). |
ViewportState |
Niri’s horizontal scroll state: .static, .gesture, or .spring. |
LayoutRefreshController |
Central refresh coordinator. Schedules, debounces, and coalesces layout recalculations. |
RefreshReason |
Why a refresh was requested (e.g., .axWindowCreated, .layoutCommand). Maps to a refresh route. |
RefreshRoute |
How the refresh executes: fullRescan, relayout, immediateRelayout, visibilityRefresh, windowRemoval. |
ManagedFocusRequest |
In-flight focus request with status (.pending/.confirmed) and retry tracking. |
FocusBridgeCoordinator |
Focus state machine coordinating OmniWM’s focus intent with macOS confirmation. |
CGSEventObserver |
SkyLight event listener for window create/destroy/frame-change/front-app-change. |
HotkeyCommand |
Enum of all 67 commands that can be triggered by hotkeys or IPC. |
IPCApplicationBridge |
Swift actor routing IPC requests to @MainActor command/query/rule handlers. |
IPCEventBroker |
Swift actor managing real-time event subscriptions for IPC clients. |
ProportionalSize |
.proportion(CGFloat) or .fixed(CGFloat) — Niri column width specification. |
WeightedSize |
.auto(weight:) or .fixed(CGFloat) — Niri window height within a column. |
NodeId |
UUID-based identifier for Niri layout tree nodes. |
SpringConfig |
Animation parameters: response, dampingFraction. Presets: .snappy, .balanced, .gentle. |
WindowDecision |
Result of rule evaluation: disposition, source, workspaceName, ruleEffects. |
WindowRuleFacts |
Input for rule evaluation: app name, AX facts (role, subrole, title), size constraints. |
LayoutType |
.defaultLayout, .niri, or .dwindle — per-workspace layout selection. |
Scratchpad |
A special slot for a single transient window that can be toggled in/out of view. |