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
GhosttyViewsurface 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
BridgeStateis transient,TabStateis 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/ghosttyDevelopment 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
| Data | Source of Truth | Persistence | Sync Rule |
|---|---|---|---|
| Working Directory | BridgeState (live) | TabState | Sync to TabState on tab switch |
| Remote Session | BridgeState (live) | NOT persisted | Re-detected on restore via process tree |
| Remote CWD | BridgeState (live) | TabState | Sync to TabState on tab switch |
| Foreground Process | BridgeState (live) | TabState | Sync to TabState on tab switch |
| Shell PID | TabState | SessionStore | Set once on terminal creation |
| Terminal Title | BridgeState (live) | TabState | Sync to TabState on tab switch |
| Agent Session ID | TabState | SessionStore | Updated by plugin detection |
| Plugin Expanded IDs | WindowStateCoordinator | TabState | Sync on tab switch |
| Section Heights | SidebarPanelView | TabState + AppSettings.__sidebar | Sync on tab switch, app quit |
| Section Order | WindowStateCoordinator | AppSettings.__sidebar | Sync on reorder, app quit |
| Scroll Offsets | SidebarPanelView | TabState | Sync on tab switch |
| Active Plugin Tab | WindowStateCoordinator | TabState | Sync on tab switch |
| Theme | AppSettings | UserDefaults + file | Immediate |
| Sidebar Visible | MainWindowController | NOT persisted | Runtime only |
Key Invariants
BridgeStatemust be synced toTabStateviasyncBridgeToTab()before any tab switch.- Remote session is intentionally not persisted — always re-detected on restore.
- Plugin settings use namespace:
pluginSettings[pluginID][key]. - Changes to
AppSettingsemit.settingsChangedviaNotificationCenter.
Sync Points
Tab Switch:
syncBridgeToTab(previousPane, previousTabIndex)— save bridge → tabsavePluginState()— save plugin UI → tabrestorePluginState(newTab)— restore plugin UI ← tabbridge.restoreTabState(...)— restore bridge ← tab
App Quit:
WindowManager.saveSessionForTermination():
- For each window:
coordinator.saveSidebarStateToSettings()(heights, order, and global sidebar state →AppSettings.__sidebar) andprepareSessionSave()(persist pane content, sidebar, and plugin state into the model) SessionStore.save(snapshot)— a version 2SessionSnapshotwith oneSessionWindowper window pluskeyWindowIndex
Controller Responsibilities
| Controller | Responsibility |
|---|---|
| WindowManager | All windows, command routing by pane_id, session save |
| MainWindowController | Window lifecycle, menu actions, high-level coordination |
| SidebarController | Panel visibility, tab selection, heights sync |
| WindowStateCoordinator | Bridge↔Tab sync, plugin state save/restore |
| TerminalBridge | Event parsing, heuristics, remote detection |
