Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 26 additions & 100 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -593,60 +593,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
MacSentryStartupPolicy.isRunningUnderXCTest(environment: env)
}

@MainActor
final class MainWindowContext {
let windowId: UUID
let tabManager: TabManager
let sidebarState: SidebarState
let sidebarSelectionState: SidebarSelectionState
var fileExplorerState: FileExplorerState?
let keyboardFocusCoordinator: MainWindowFocusController
var cmuxConfigStore: CmuxConfigStore?
var closeObserver: WindowCloseObserver?
weak var window: NSWindow?
/// Per-window Dock owned by this context and torn down with it.
var windowDock: DockSplitStore?
private let workspaceTerminalFontSizeArbiter:
WorkspaceTerminalFontSizeArbiter
/// Window-scoped font-size queue. Requests contain stable workspace ids;
/// teardown cancels the queue before any surface owner is released.
lazy var workspaceTerminalFontSizeCoordinator =
WorkspaceTerminalFontSizeCoordinator(
tabManager: tabManager,
arbiter: workspaceTerminalFontSizeArbiter
)
#if DEBUG
var debugWorkspaceTerminalFontSizeEnqueueResultOverride: Bool?
#endif

init(
windowId: UUID,
tabManager: TabManager,
sidebarState: SidebarState,
sidebarSelectionState: SidebarSelectionState,
fileExplorerState: FileExplorerState?,
cmuxConfigStore: CmuxConfigStore?,
window: NSWindow?,
workspaceTerminalFontSizeArbiter:
WorkspaceTerminalFontSizeArbiter
) {
self.windowId = windowId
self.tabManager = tabManager
self.sidebarState = sidebarState
self.sidebarSelectionState = sidebarSelectionState
self.fileExplorerState = fileExplorerState
self.cmuxConfigStore = cmuxConfigStore
self.window = window
self.workspaceTerminalFontSizeArbiter =
workspaceTerminalFontSizeArbiter
self.keyboardFocusCoordinator = MainWindowFocusController(
windowId: windowId,
window: window,
tabManager: tabManager,
fileExplorerState: fileExplorerState
)
}
}
/// Compatibility name for the independently owned main-window model.
typealias MainWindowContext = CmuxMainWindowContext

struct ScriptableMainWindowState {
let windowId: UUID
Expand Down Expand Up @@ -1074,7 +1022,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
#endif

var mainWindowContexts: [ObjectIdentifier: MainWindowContext] = [:]
private let mainWindowRegistry = MainWindowRegistry()

/// Compatibility view for existing routing and lifecycle callers.
var mainWindowContexts: [ObjectIdentifier: MainWindowContext] {
mainWindowRegistry.contexts
}
private var mainWindowControllers: [MainWindowController] = []

/// Tracks the cascade point for new windows, matching Ghostty's upstream algorithm.
Expand Down Expand Up @@ -4875,6 +4828,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
NotificationCenter.default.post(name: .mainWindowContextsDidChange, object: self)
}

#if DEBUG
/// Inserts a windowless test context without exposing the registry storage.
func insertMainWindowContextForTesting(_ context: MainWindowContext, key: ObjectIdentifier) {
mainWindowRegistry.insert(context, for: key)
}
#endif

func ensureMobileWorkspaceListObserver(for tabManager: TabManager) {
let id = ObjectIdentifier(tabManager)
let configStore = mainWindowContext(for: tabManager)?.cmuxConfigStore
Expand Down Expand Up @@ -4907,12 +4867,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
fileExplorerState: FileExplorerState? = nil,
cmuxConfigStore: CmuxConfigStore? = nil
) {
let key = ObjectIdentifier(window)
forgetRecoverableMainWindowRoute(windowId: windowId)
#if DEBUG
let priorManagerToken = debugManagerToken(self.tabManager)
#endif
if let existing = mainWindowContexts[key] {
if let existing = mainWindowRegistry.context(for: window) {
tabManager.window = window
tabManager.windowId = existing.windowId
existing.window = window
Expand Down Expand Up @@ -4981,7 +4940,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
workspaceTerminalFontSizeArbiter:
workspaceTerminalFontSizeArbiter
)
mainWindowContexts[key] = context
mainWindowRegistry.insert(context, for: window)
context.closeObserver = WindowCloseObserver(window: window) { [weak self] in self?.unregisterMainWindow($0) }
}
commandPaletteWindowStore.registerWindow(windowId)
Expand Down Expand Up @@ -6213,33 +6172,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}

private func reindexMainWindowContextIfNeeded(_ context: MainWindowContext, for window: NSWindow) {
let desiredKey = ObjectIdentifier(window)
if mainWindowContexts[desiredKey] === context {
context.window = window
return
}

let contextKeys = mainWindowContexts.compactMap { key, value in
value === context ? key : nil
}
for key in contextKeys {
mainWindowContexts.removeValue(forKey: key)
}

if let conflicting = mainWindowContexts[desiredKey], conflicting !== context {
context.window = window
return
if mainWindowRegistry.reindex(context, for: window) {
notifyMainWindowContextsDidChange()
}

mainWindowContexts[desiredKey] = context
context.window = window
notifyMainWindowContextsDidChange()
}

func contextForMainTerminalWindow(_ window: NSWindow, reindex: Bool = true) -> MainWindowContext? {
guard isMainTerminalWindow(window) else { return nil }

if let context = mainWindowContexts[ObjectIdentifier(window)] {
if let context = mainWindowRegistry.context(for: window) {
context.window = window
return context
}
Expand Down Expand Up @@ -6274,12 +6215,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
private func unregisterMainWindowContext(for window: NSWindow) -> MainWindowContext? {
guard let removed = contextForMainTerminalWindow(window, reindex: false) else { return nil }
removed.teardownWindowDock()
let removedKeys = mainWindowContexts.compactMap { key, value in
value === removed ? key : nil
}
for key in removedKeys {
mainWindowContexts.removeValue(forKey: key)
}
mainWindowRegistry.removeReferences(to: removed)
rememberRecoverableMainWindowRoute(windowId: removed.windowId, tabManager: removed.tabManager, window: removed.window)
removeMobileWorkspaceListObserverIfUnused(for: removed.tabManager)
notifyMainWindowContextsDidChange()
Expand All @@ -6289,12 +6225,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
// Internal (not private): see notifyMainWindowContextsDidChange.
func discardOrphanedMainWindowContext(_ context: MainWindowContext, allowWindowlessFallback: Bool = false) {
context.teardownWindowDock()
let contextKeys = mainWindowContexts.compactMap { key, value in
value === context ? key : nil
}
for key in contextKeys {
mainWindowContexts.removeValue(forKey: key)
}
mainWindowRegistry.removeReferences(to: context)
rememberRecoverableMainWindowRoute(windowId: context.windowId, tabManager: context.tabManager, window: context.window)
removeMobileWorkspaceListObserverIfUnused(for: context.tabManager)
notifyMainWindowContextsDidChange()
Expand All @@ -6320,7 +6251,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}

private func mainWindowId(for window: NSWindow) -> UUID? {
if let context = mainWindowContexts[ObjectIdentifier(window)] {
if let context = mainWindowRegistry.context(for: window) {
return context.windowId
}
guard let rawIdentifier = window.identifier?.rawValue,
Expand Down Expand Up @@ -16064,13 +15995,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
)
debugDetachedContextWindows.append(detachedWindow)

let contextKeys = mainWindowContexts.compactMap { key, value in
value === context ? key : nil
}
for key in contextKeys {
mainWindowContexts.removeValue(forKey: key)
}
mainWindowContexts[ObjectIdentifier(detachedWindow)] = context
mainWindowRegistry.removeReferences(to: context)
mainWindowRegistry.insert(context, for: detachedWindow)
context.window = window
return true
}
Expand Down Expand Up @@ -17393,7 +17319,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
#endif

func isMainTerminalWindow(_ window: NSWindow) -> Bool {
if mainWindowContexts[ObjectIdentifier(window)] != nil {
if mainWindowRegistry.context(for: window) != nil {
return true
}
guard let raw = window.identifier?.rawValue else { return false }
Expand Down
57 changes: 57 additions & 0 deletions Sources/CmuxMainWindowContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import AppKit

/// Owns the models and resource handles scoped to a single main window.
@MainActor
final class CmuxMainWindowContext {
let windowId: UUID
let tabManager: TabManager
let sidebarState: SidebarState
let sidebarSelectionState: SidebarSelectionState
var fileExplorerState: FileExplorerState?
let keyboardFocusCoordinator: MainWindowFocusController
var cmuxConfigStore: CmuxConfigStore?
var closeObserver: WindowCloseObserver?
weak var window: NSWindow?
/// Per-window Dock owned by this context and torn down with it.
var windowDock: DockSplitStore?
private let workspaceTerminalFontSizeArbiter:
WorkspaceTerminalFontSizeArbiter
/// Window-scoped font-size queue. Requests contain stable workspace ids;
/// teardown cancels the queue before any surface owner is released.
lazy var workspaceTerminalFontSizeCoordinator =
WorkspaceTerminalFontSizeCoordinator(
tabManager: tabManager,
arbiter: workspaceTerminalFontSizeArbiter
)
#if DEBUG
var debugWorkspaceTerminalFontSizeEnqueueResultOverride: Bool?
#endif

init(
windowId: UUID,
tabManager: TabManager,
sidebarState: SidebarState,
sidebarSelectionState: SidebarSelectionState,
fileExplorerState: FileExplorerState?,
cmuxConfigStore: CmuxConfigStore?,
window: NSWindow?,
workspaceTerminalFontSizeArbiter:
WorkspaceTerminalFontSizeArbiter
) {
self.windowId = windowId
self.tabManager = tabManager
self.sidebarState = sidebarState
self.sidebarSelectionState = sidebarSelectionState
self.fileExplorerState = fileExplorerState
self.cmuxConfigStore = cmuxConfigStore
self.window = window
self.workspaceTerminalFontSizeArbiter =
workspaceTerminalFontSizeArbiter
self.keyboardFocusCoordinator = MainWindowFocusController(
windowId: windowId,
window: window,
tabManager: tabManager,
fileExplorerState: fileExplorerState
)
}
}
68 changes: 68 additions & 0 deletions Sources/MainWindowRegistry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import AppKit

/// Owns the identity index that maps terminal windows to their main-window contexts.
@MainActor
final class MainWindowRegistry {
typealias Context = CmuxMainWindowContext

private(set) var contexts: [ObjectIdentifier: Context] = [:]

/// Returns the context currently indexed for `window`.
func context(for window: NSWindow) -> Context? {
contexts[ObjectIdentifier(window)]
}

/// Returns the context currently indexed under `key`.
func context(for key: ObjectIdentifier) -> Context? {
contexts[key]
}

/// Inserts `context` under the window's identity key.
func insert(_ context: Context, for window: NSWindow) {
insert(context, for: ObjectIdentifier(window))
}

/// Inserts `context` under an already-resolved identity key.
///
/// The key overload keeps test-only windowless contexts on the same mutation
/// path without exposing the registry's backing dictionary to callers.
func insert(_ context: Context, for key: ObjectIdentifier) {
contexts[key] = context
}

/// Removes every key currently referring to `context`.
@discardableResult
func removeReferences(to context: Context) -> [ObjectIdentifier] {
let keys = contexts.compactMap { key, value in
value === context ? key : nil
}
for key in keys {
contexts.removeValue(forKey: key)
}
return keys
}

/// Rebinds a context to a window while preserving duplicate-window conflict behavior.
///
/// Returns `true` only when a new index entry was installed. A conflicting entry leaves
/// the context unindexed under the requested key, matching the prior AppDelegate logic.
@discardableResult
func reindex(_ context: Context, for window: NSWindow) -> Bool {
let desiredKey = ObjectIdentifier(window)
if contexts[desiredKey] === context {
context.window = window
return false
}

removeReferences(to: context)

if let conflicting = contexts[desiredKey], conflicting !== context {
context.window = window
return false
}

contexts[desiredKey] = context
context.window = window
return true
}
}
Loading