Skip to content
Logo

Architecture

Stack

  • Swift (macOS 15+) with AppKit for terminal UI and SwiftUI for sidebar/settings
  • GhosttyKit — Metal GPU-accelerated terminal emulation via C API (ghostty-org/ghostty)
  • PTY — owned by Ghostty: each GhosttyView surface spawns and drives its own shell process

Project Structure

Boo/                Library target (all app code)
  App/              AppDelegate, AppStore, WindowManager, MainWindowController (+extensions),
                    SidebarController, WindowStateCoordinator
  Content/          Tab content: terminal, browser, editor (Monaco), image viewer, markdown, plugin tabs
  Ghostty/          GhosttyRuntime (singleton), GhosttyView (Metal surface)
  Models/           Workspace, Pane (+ TabState), SplitTree, AppSettings (Settings.swift), Theme
  Plugin/           Core framework (protocol, registry, runtime, ViewDSL)
  Plugins/          Built-in plugins (Agents, FileTree, RemoteExplorer, Git, Docker, Bookmarks,
                    Snippets, SystemInfo, Debug)
  Views/            PaneView, SplitContainerView, StatusBarView, ToolbarView, SettingsWindow
  Services/         TerminalBridge, RemoteExplorer, SessionStore, BooSocketServer, BooMCPServer,
                    RemoteControlServer, AgentInbox, SparkleUpdater
  Resources/        MonacoSource → MonacoBundle, RemoteControl web UI (ghostty-vt.wasm)
BooApp/             Executable entry point (calls BooMain.run())
CGhostty/           C module wrapping ghostty.h
CIronmark/          C module wrapping ironmark (Rust markdown parser)
CObjCSafe/          Obj-C shim that catches AppKit exceptions around string drawing
Vendor/             ghostty + ironmark submodules, patches/ (local Ghostty patches)

Plugin code is colocated — all files for a plugin live in one directory under Boo/Plugins/. Tests mirror this layout under Tests/BooTests/Plugins/.

State Model

Global — AppSettings (singleton)

Theme, font, layout, status bar toggles, and per-plugin settings. Changes broadcast via NotificationCenter (.settingsChanged).

Per-Tab — TabState

Each Pane.Tab owns a TabState — the single source of truth for that tab (excerpt from Pane.swift):

struct TabState {
    var contentType: ContentType = .terminal
    var contentState: ContentState = .terminal(TerminalContentState())
 
    var workingDirectory: String
    var remoteSession: RemoteSessionType?
    var remoteWorkingDirectory: String?
    var shellPID: pid_t = 0
    var title: String
    var foregroundProcess: String = ""
    var foregroundProcessPID: pid_t?
    var foregroundProcessCategory: String?
    var foregroundProcessMetadata: [String: String] = [:]
    var agentSessionID: String?
    var hasActivity: Bool = false
    var isCommandRunning: Bool = false
 
    // Plugin UI
    var expandedPluginIDs: Set<String> = []
    var userCollapsedSectionIDs: Set<String> = []
    var sidebarSectionHeights: [String: CGFloat] = [:]
    var sidebarScrollOffsets: [String: CGPoint] = [:]
    var sidebarSectionOrder: [String: [String]] = [:]
    var selectedPluginTabID: String? = nil
}

Observable — AppStore

AppStore.shared projects the current TerminalContext, theme snapshot, and sidebar visibility for SwiftUI views. Updated at the end of each plugin cycle.

Data Flow

Ghostty OSC event → PaneView delegate → MainWindowController
  → TerminalBridge (heuristics, remote detection)
  → WindowStateCoordinator.syncBridgeToTab() → writes to TabState
  → runPluginCycle() → TerminalContext (frozen snapshot)
  → PluginRuntime (enrich → freeze → react) → sidebar/status bar

Key components:

  • WindowStateCoordinator — centralized state manager between bridge, tab model, and plugin registry
  • TerminalBridge — event parser and heuristic engine; its BridgeState is transient, TabState is authoritative
  • MainWindowController — delegates state management to the coordinator

Hierarchy

Workspace → SplitTree → Pane[] → Tab[] → TabState

Each split pane owns its own tab bar and terminal sessions.

Windows

WindowManager owns every MainWindowController. Each window has its own workspaces, sidebar, and plugin registry; settings, themes, and bookmarks stay global. App-wide entry points (Unix socket, remote control, MCP) go through WindowManager.dispatchCommand, which routes a command carrying pane_id to the window owning that pane, otherwise to the active window.

Build

make setup         # First time: clone Ghostty + build GhosttyKit + ironmark + Boo
make run           # Build and launch
make test          # Run tests
make clean-ghostty # Rebuild GhosttyKit after updating Vendor/ghostty

Development note: make run produces a debug app bundle with Sparkle feed keys removed, so local builds cannot check the production update feed. Release-oriented targets (make app, make zip, make dmg, make dist) keep the Sparkle configuration and bundle Sparkle.framework inside Contents/Frameworks.

State Contracts

Single Source of Truth

DataSource of TruthPersistenceSync Rule
Working DirectoryBridgeState (live)TabStateSync to TabState on tab switch
Remote SessionBridgeState (live)NOT persistedRe-detected on restore via process tree
Remote CWDBridgeState (live)TabStateSync to TabState on tab switch
Foreground ProcessBridgeState (live)TabStateSync to TabState on tab switch
Shell PIDTabStateSessionStoreSet once on terminal creation
Terminal TitleBridgeState (live)TabStateSync to TabState on tab switch
Agent Session IDTabStateSessionStoreUpdated by plugin detection
Plugin Expanded IDsWindowStateCoordinatorTabStateSync on tab switch
Section HeightsSidebarPanelViewTabState + AppSettings.__sidebarSync on tab switch, app quit
Section OrderWindowStateCoordinatorAppSettings.__sidebarSync on reorder, app quit
Scroll OffsetsSidebarPanelViewTabStateSync on tab switch
Active Plugin TabWindowStateCoordinatorTabStateSync on tab switch
ThemeAppSettingsUserDefaults + fileImmediate
Sidebar VisibleMainWindowControllerNOT persistedRuntime only

Key Invariants

  1. BridgeState must be synced to TabState via syncBridgeToTab() before any tab switch.
  2. Remote session is intentionally not persisted — always re-detected on restore.
  3. Plugin settings use namespace: pluginSettings[pluginID][key].
  4. Changes to AppSettings emit .settingsChanged via NotificationCenter.

Sync Points

Tab Switch:

  1. syncBridgeToTab(previousPane, previousTabIndex) — save bridge → tab
  2. savePluginState() — save plugin UI → tab
  3. restorePluginState(newTab) — restore plugin UI ← tab
  4. bridge.restoreTabState(...) — restore bridge ← tab

App Quit:

WindowManager.saveSessionForTermination():

  1. For each window: coordinator.saveSidebarStateToSettings() (heights, order, and global sidebar state → AppSettings.__sidebar) and prepareSessionSave() (persist pane content, sidebar, and plugin state into the model)
  2. SessionStore.save(snapshot) — a version 2 SessionSnapshot with one SessionWindow per window plus keyWindowIndex

Controller Responsibilities

ControllerResponsibility
WindowManagerAll windows, command routing by pane_id, session save
MainWindowControllerWindow lifecycle, menu actions, high-level coordination
SidebarControllerPanel visibility, tab selection, heights sync
WindowStateCoordinatorBridge↔Tab sync, plugin state save/restore
TerminalBridgeEvent parsing, heuristics, remote detection