Skip to content

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:

  1. Stateless layouts: Layout algorithms are pure functions that compute window positions from parameters. They don’t modify global state.
  2. Event-driven updates: Window changes emit engine events, with scheduling to coalesce rapid updates.
  3. 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 Shoji class 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:

ModulePurpose
engine.luaEvent handling and intent execution
engine_scheduler.luaTiling coalescing and scheduling
engine_types.luaEmmyLua type definitions
event_intents.luaEvent-to-intent translation
intent_executor.luaIntent execution via handlers
intent_registry.luaIntent 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:

HandlerDomain
layout.luaLayout control intents
window.luaWindow lifecycle intents
window_movement.luaWindow movement intents
window_state.luaWindow state intents
system.luaSystem-level intents
alerts.luaAlert display intents

Layout system (layouts/)

Pure geometry computation, completely separated from window management:

ModulePurpose
protocol.luaType definitions and layout validation
init.luaLayout registry, cycling, enabling
geometry.luaFrame math operations (inset, clamp)
master_stack.luaFactory 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 vertically
  • Centered — Centers content with margins
  • Gaps — Adds additional gaps to inner windows

Combinators (layouts/combinators/):

  • Partition — Splits screen into regions with different layouts
  • IfMax — 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:

EventHandler
windowCreatedDelays tiling to allow init
windowDestroyedRemoves window, retiles spaces
windowFocusedUpdates focus tracking
windowMovedInvalidates nav cache, retiles
windowVisible/HiddenRetiles affected space
screenChangedClears caches, retiles all
spaceChangedClears 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

ModulePurpose
cheatsheet/Canvas-based hotkey overlay
chooser.luaCommand palette for layouts/actions
alerts.luaToast notifications for layout changes
theme.luaColor palette and styling constants
keybinding_format.luaModifier/key symbol formatting

Supporting modules

ModulePurpose
bootstrap.luaStartup and shutdown wiring
space_index.luaBidirectional window-to-space index
persistence.luaSave/restore state across restarts
focus_history.luaTrack focus for focus_last action
navigator.luaDirectional navigation and adjacency
adjacency.luaNeighbor graphs from window geometry
hooks.luaUser-defined lifecycle callbacks
hotkey.luaParse hotkey specs (standard/flat)
hotkey_binding.luaHotkey binding and unbinding
ipc.luaExternal scripting via hs -c commands
hs_safe.luaSafe pcall wrappers for HS APIs
table_utils.luaMerge, deepCopy utilities
constants.luaTiming 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 hook

Action 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 executes

Timing constants

These values balance responsiveness with stability:

ConstantValuePurpose
System coalescing50msCoalesces bursty system events
Debounce delay100msCoalesces rapid events
Window created delay100msAllows window init
Event suppression150msIgnores self-triggered events
Space switch retile300msDelay before retile on space switch
Max debounce wait500msPrevents starvation
Recently tiled TTL500msPer-window tile tracking window
Event suppression max2sMaximum 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:

  • pcall wraps all APIs that might fail (window operations, space queries)
  • Safe wrappers in hs_safe.lua return nil on 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:

Terminal window
hs -c 'spoon.Shoji:cmd("set-layout", "bsp")'
hs -c 'spoon.Shoji:cmd("status")'

See IPC for available commands.