Skip to content

Architecture

This page describes Shoji’s internal architecture for contributors and users who want to understand how the window manager works.

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.
┌─────────────────────┐
│ 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.

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

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

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 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)

Section titled “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.

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.

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
  • 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 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

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

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

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.).

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

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.

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.

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.