Architecture
This page describes Shoji’s internal architecture for contributors and users who want to understand how the window manager works.
Design principles
Shoji follows these core principles:
- Stateless layouts: Layout algorithms are pure functions that compute window positions from parameters. They don’t modify global state.
- Event-driven updates: Window changes emit engine events, with scheduling to coalesce rapid updates.
- Intent-driven core: Actions and events become intents, and the engine executes them consistently.
Module dependency graph
┌─────────────────────┐ │ init.lua │ │ (entry point) │ └──────────┬──────────┘ │ ┌──────────▼──────────┐ │ bootstrap.lua │ │ (startup/shutdown) │ └──────────┬──────────┘ │ ┌──────────────────────────┼───────────────────┐ │ │ │ ▼ ▼ ▼┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐│ actions.lua │ │ events.lua │ │ engine.lua ││ (user intent) │ │ (system events) │ │ (dispatch/exec) │└──────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ │ └──────────────────────┴──────────┬──────────┘ ▼ ┌─────────────────┐ │ tiling.lua │ │ (orchestration) │ └─────────────────┘Key pattern: init.lua is the Hammerspoon bridge and
public API. bootstrap.lua handles the actual start/stop
lifecycle, initializing all subsystems via .init(shoji) and
creating the engine.
Subsystems
Core (init.lua, bootstrap.lua)
init.lua is the entry point and Hammerspoon bridge:
- Defines the
Shojiclass and public API (start(),stop(),configure(),cmd()) - Stores instance state (hotkeys, window focus tracking)
- Delegates lifecycle to
bootstrap.lua
bootstrap.lua handles startup and shutdown wiring:
- Initializes all modules via
.init(shoji)pattern - Creates the engine after module initialization
- Registers hooks, extensions, and hotkey bindings
- Validates configuration and layout registration
- Tears down extensions and modules on stop
Engine subsystem
The engine centralizes event-to-intent dispatch and execution:
| Module | Purpose |
|---|---|
engine.lua | Event handling and intent execution |
engine_scheduler.lua | Tiling coalescing and scheduling |
engine_types.lua | EmmyLua type definitions |
event_intents.lua | Event-to-intent translation |
intent_executor.lua | Intent execution via handlers |
intent_registry.lua | Intent type registry and validation |
engine.lua is the core: it receives EngineEvent
payloads, translates them to intents via event_intents.lua,
runs extension event handlers and merges their intents, then
executes the resulting intent queue. Extension intent handlers
can intercept, replace, or emit additional intents before the
core executor runs.
engine_scheduler.lua manages tiling coalescing with
priority-based delays. Immediate-priority tiles run
synchronously; debounced tiles coalesce into a single timer.
Starvation protection flushes spaces that have waited beyond
DEBOUNCE_MAX_WAIT.
intent_handlers/ contains handler implementations
grouped by domain:
| Handler | Domain |
|---|---|
layout.lua | Layout control intents |
window.lua | Window lifecycle intents |
window_movement.lua | Window movement intents |
window_state.lua | Window state intents |
system.lua | System-level intents |
alerts.lua | Alert display intents |
Layout system (layouts/)
Pure geometry computation, completely separated from window management:
| Module | Purpose |
|---|---|
protocol.lua | Type definitions and layout validation |
init.lua | Layout registry, cycling, enabling |
geometry.lua | Frame math operations (inset, clamp) |
master_stack.lua | Factory for main-stack layouts |
Built-in layouts: Tall, Wide, BSP, Column, Grid, Monocle, Floating, ThreeColumns.
Modifiers (layouts/modifiers/): Transform layouts
without rewriting them:
Mirror— Flips layout horizontally or verticallyCentered— Centers content with marginsGaps— Adds additional gaps to inner windows
Combinators (layouts/combinators/):
Partition— Splits screen into regions with different layoutsIfMax— Switches between two layouts based on window count
Action system (actions.lua, actions_window.lua)
Maps user intent to tiling operations:
- Focus navigation: Directional (left/right/up/down), sequential (next/prev), history-based (last)
- Swap operations: Same patterns as focus navigation
- Float toggle: Per-window state with frame memory
- Layout control: Cycling, ratio/count adjustment, reset
- Hotkey binding: Maps action names to Hammerspoon hotkeys
actions_window.lua contains window-centric behavior
extracted from actions.lua: directional and sequential
focus/swap, float toggle, and zoom toggle. These actions
dispatch intents through the engine.
Event system (events.lua)
Handles window and system events:
| Event | Handler |
|---|---|
windowCreated | Delays tiling to allow init |
windowDestroyed | Removes window, retiles spaces |
windowFocused | Updates focus tracking |
windowMoved | Invalidates nav cache, retiles |
windowVisible/Hidden | Retiles affected space |
screenChanged | Clears caches, retiles all |
spaceChanged | Clears directional context, retiles |
Event suppression: During programmatic frame changes, events are suppressed for 0.5 seconds to prevent feedback loops.
State management (state.lua)
Maintains per-space and per-window state:
- Per-space: Layout name, main ratio, main count, window order, layout state
- Per-window: Floating status, frame cache, space assignment
- Change tracking: Observer pattern for persistence hooks
- MRU tracking: Most recently used layouts (30-item limit)
State invariants
- Window order: Unique window IDs, preserving first-seen order.
- Layout state: Create-on-read table, never
nil. - Recently tiled TTL: Uses monotonic time; expiry at TTL is not recent.
Window utilities (windows.lua)
Window operations and filtering:
- Filtering: Create window filter based on config (blocklist/allowlist, minimum size)
- Floating detection: Check if window should skip tiling
- Canvas calculation: Get usable screen area (accounting for menu bar, dock)
- Screen mapping: Cache space-to-screen lookups
Tiling orchestration (tiling.lua)
Coordinates layout execution:
- Batch frame updates: Disables animations during retile for smooth updates
- Event coalescing: Delegates to the engine scheduler for debouncing
- Layout execution: Parameter validation, geometry computation, state persistence
- Insertion tracking: Maintains natural window ordering
UI modules
| Module | Purpose |
|---|---|
cheatsheet/ | Canvas-based hotkey overlay |
chooser.lua | Command palette for layouts/actions |
alerts.lua | Toast notifications for layout changes |
theme.lua | Color palette and styling constants |
keybinding_format.lua | Modifier/key symbol formatting |
Supporting modules
| Module | Purpose |
|---|---|
bootstrap.lua | Startup and shutdown wiring |
space_index.lua | Bidirectional window-to-space index |
persistence.lua | Save/restore state across restarts |
focus_history.lua | Track focus for focus_last action |
navigator.lua | Directional navigation and adjacency |
adjacency.lua | Neighbor graphs from window geometry |
hooks.lua | User-defined lifecycle callbacks |
hotkey.lua | Parse hotkey specs (standard/flat) |
hotkey_binding.lua | Hotkey binding and unbinding |
ipc.lua | External scripting via hs -c commands |
hs_safe.lua | Safe pcall wrappers for HS APIs |
table_utils.lua | Merge, deepCopy utilities |
constants.lua | Timing values, limits, layout names |
Data flow
Retiling flow
1. Event triggers (window created, space changed, etc.) │ ▼2. Event handler dispatches an engine event │ ▼3. Engine emits a `tile_space` intent │ ▼4. Scheduler coalesces and runs tiling │ ▼5. tileSpace() executes: a. Get tiling windows for space (filtered, ordered) b. Get layout for space c. Build LayoutParams (windowIDs, workarea, etc.) d. Call layout.arrange(params) → geometries e. Disable animations f. Apply geometries to windows g. Restore animations h. Trigger after_tile hookAction flow
1. User presses hotkey │ ▼2. Hammerspoon calls bound action function │ ▼3. Action determines affected window(s) and space │ ▼4. Action dispatches intent via the engine │ ▼5. Engine executes intent │ ▼6. Retiling flow executesTiming constants
These values balance responsiveness with stability:
| Constant | Value | Purpose |
|---|---|---|
| System coalescing | 50ms | Coalesces bursty system events |
| Debounce delay | 100ms | Coalesces rapid events |
| Window created delay | 100ms | Allows window init |
| Event suppression | 150ms | Ignores self-triggered events |
| Space switch retile | 300ms | Delay before retile on space switch |
| Max debounce wait | 500ms | Prevents starvation |
| Recently tiled TTL | 500ms | Per-window tile tracking window |
| Event suppression max | 2s | Maximum suppression |
Caching strategy
Shoji caches expensive operations to maintain performance:
- Window-space mapping: Cached per window, invalidated on move events
- Screen lookups: Cached per space, cleared on screen configuration changes
- Index cache: Window indices within space, cleared on order changes
Cache validation runs periodically (every 10 seconds) to detect windows moved between spaces by other means (Mission Control, etc.).
Error handling
Shoji uses defensive programming for Hammerspoon API calls:
pcallwraps all APIs that might fail (window operations, space queries)- Safe wrappers in
hs_safe.luareturnnilon failure - Errors are logged but don’t crash the window manager
- Layouts with invalid geometries are rejected with error messages
Extension points
Custom layouts
Register custom layouts via configuration:
spoon.Shoji:configure({ layouts = { require("my_layout") }, enabled_layouts = { "tall", "my_layout" },})spoon.Shoji:start()See Custom layouts for the protocol specification.
Hooks
Register callbacks for lifecycle events:
spoon.Shoji:configure({ hooks = { after_tile = function(spaceID) -- React to retiling end, },})spoon.Shoji:start()See Hooks for available events.
IPC
Control Shoji from external scripts:
hs -c 'spoon.Shoji:cmd("set-layout", "bsp")'hs -c 'spoon.Shoji:cmd("status")'See IPC for available commands.