import AppKit import Combine import SwiftUI /// Coordinates screen/workspace state with notch lifecycle and /// delegates raw window work to `WindowCoordinator`. @MainActor final class ScreenManager: ObservableObject { static let shared = ScreenManager() private let screenRegistry = ScreenRegistry.shared private let workspaceRegistry = WorkspaceRegistry.shared private let windowCoordinator = WindowCoordinator() private lazy var orchestrator = NotchOrchestrator(screenRegistry: screenRegistry, host: self) private var cancellables = Set() private init() {} private var showOnAllDisplays: Bool { AppSettingsController.shared.settings.display.showOnAllDisplays } // MARK: - Lifecycle func start() { screenRegistry.refreshConnectedScreens() observeScreenChanges() rebuildWindows() setupHotkeys() } func stop() { cleanupAllWindows() cancellables.removeAll() HotkeyManager.shared.stop() } // MARK: - Hotkey wiring private func setupHotkeys() { let hotkeyManager = HotkeyManager.shared hotkeyManager.onToggle = { [weak self] in MainActor.assumeIsolated { self?.orchestrator.toggleOnActiveScreen() } } hotkeyManager.onNewTab = { [weak self] in MainActor.assumeIsolated { self?.activeWorkspace().newTab() } } hotkeyManager.onCloseTab = { [weak self] in MainActor.assumeIsolated { self?.activeWorkspace().closeActiveTab() } } hotkeyManager.onNextTab = { [weak self] in MainActor.assumeIsolated { self?.activeWorkspace().nextTab() } } hotkeyManager.onPreviousTab = { [weak self] in MainActor.assumeIsolated { self?.activeWorkspace().previousTab() } } hotkeyManager.onNextWorkspace = { [weak self] in MainActor.assumeIsolated { self?.switchWorkspace(offset: 1) } } hotkeyManager.onPreviousWorkspace = { [weak self] in MainActor.assumeIsolated { self?.switchWorkspace(offset: -1) } } hotkeyManager.onDetachTab = { [weak self] in MainActor.assumeIsolated { self?.detachActiveTab() } } hotkeyManager.onApplySizePreset = { [weak self] preset in MainActor.assumeIsolated { self?.applySizePreset(preset) } } hotkeyManager.onSwitchToTab = { [weak self] index in MainActor.assumeIsolated { self?.activeWorkspace().switchToTab(at: index) } } hotkeyManager.onSwitchToWorkspace = { [weak self] workspaceID in MainActor.assumeIsolated { self?.switchActiveScreen(to: workspaceID) } } hotkeyManager.start() } // MARK: - Toggle func toggleNotchOnActiveScreen() { orchestrator.toggleOnActiveScreen() } // MARK: - Open / Close func openNotch(screenID: ScreenID) { orchestrator.open(screenID: screenID) } func closeNotch(screenID: ScreenID) { orchestrator.close(screenID: screenID) } private func detachActiveTab() { if let session = activeWorkspace().detachActiveTab() { DispatchQueue.main.async { PopoutWindowController.shared.popout(session: session) } } } private func switchWorkspace(offset: Int) { guard let screenID = screenRegistry.activeScreenID() else { return } let currentWorkspaceID = screenRegistry.screenContext(for: screenID)?.workspaceID ?? workspaceRegistry.defaultWorkspaceID let nextWorkspaceID = offset >= 0 ? workspaceRegistry.nextWorkspaceID(after: currentWorkspaceID) : workspaceRegistry.previousWorkspaceID(before: currentWorkspaceID) guard let nextWorkspaceID else { return } switchScreen(screenID, to: nextWorkspaceID) } private func switchActiveScreen(to workspaceID: WorkspaceID) { guard let screenID = screenRegistry.activeScreenID() else { return } switchScreen(screenID, to: workspaceID) } private func switchScreen(_ screenID: ScreenID, to workspaceID: WorkspaceID) { screenRegistry.assignWorkspace(workspaceID, to: screenID) guard let context = screenRegistry.screenContext(for: screenID), context.notchState == .open else { return } orchestrator.open(screenID: screenID) } func applySizePreset(_ preset: TerminalSizePreset) { guard let context = screenRegistry.allScreens().first(where: { $0.notchState == .open }) else { AppSettingsController.shared.update { $0.display.openWidth = preset.width $0.display.openHeight = preset.height } return } let startSize = context.notchSize let targetSize = context.setStoredOpenSize(preset.size) windowCoordinator.animatePresetResize( for: context.id, context: context, from: startSize, to: targetSize, duration: context.openAnimationDuration ) } // MARK: - Window creation func rebuildWindows() { cleanupAllWindows() screenRegistry.refreshConnectedScreens() for screen in visibleScreens() { createWindow(for: screen) } } private func createWindow(for screen: NSScreen) { let screenID = screen.displayUUID guard let context = screenRegistry.screenContext(for: screenID) else { return } context.requestOpen = { [weak self] in self?.orchestrator.open(screenID: screenID) } context.requestClose = { [weak self] in self?.orchestrator.close(screenID: screenID) } context.requestWindowResize = { [weak self] in guard let self, let context = self.screenRegistry.screenContext(for: screenID) else { return } self.windowCoordinator.updateWindowFrame( for: screenID, context: context, centerHorizontally: true ) } context.requestTerminalFocus = { [weak self] in guard let self else { return } self.windowCoordinator.focusActiveTerminal(for: screenID) { [weak self] in self?.screenRegistry.workspaceController(for: screenID).activeTab?.terminalView } } let hostingView = NSHostingView( rootView: ContentView( screen: context, orchestrator: orchestrator ) .preferredColorScheme(.dark) ) windowCoordinator.createWindow( on: screen, context: context, contentView: hostingView, onResignKey: { [weak self] in guard !context.suppressCloseOnFocusLoss else { return } self?.orchestrator.close(screenID: screenID) } ) } // MARK: - Repositioning func repositionWindows() { screenRegistry.refreshConnectedScreens() for context in screenRegistry.allScreens() { context.refreshClosedSize() windowCoordinator.repositionWindow( for: context.id, context: context, centerHorizontally: true ) } } // MARK: - Cleanup private func cleanupAllWindows() { orchestrator.cancelAllPendingWork() windowCoordinator.cleanupAllWindows() } // MARK: - Screen observation private func observeScreenChanges() { NotificationCenter.default.publisher(for: NSApplication.didChangeScreenParametersNotification) .debounce(for: .milliseconds(500), scheduler: RunLoop.main) .sink { [weak self] _ in self?.handleScreenConfigurationChange() } .store(in: &cancellables) } private func handleScreenConfigurationChange() { screenRegistry.refreshConnectedScreens() let currentScreenIDs = Set(visibleScreens().map(\.displayUUID)) let knownScreenIDs = windowCoordinator.windowScreenIDs() if currentScreenIDs != knownScreenIDs { rebuildWindows() } else { repositionWindows() } } private func activeWorkspace() -> WorkspaceController { guard let screenID = screenRegistry.activeScreenID() else { return WorkspaceRegistry.shared.defaultWorkspaceController } return screenRegistry.workspaceController(for: screenID) } private func visibleScreens() -> [NSScreen] { if showOnAllDisplays { return NSScreen.screens } return [NSScreen.main].compactMap { $0 } } } extension ScreenManager: NotchPresentationHost { func canPresentNotch(for screenID: ScreenID) -> Bool { windowCoordinator.hasWindow(for: screenID) } func performOpenPresentation(for screenID: ScreenID) { guard screenRegistry.screenContext(for: screenID) != nil else { return } windowCoordinator.presentOpen(for: screenID) { [weak self] in self?.screenRegistry.workspaceController(for: screenID).activeTab?.terminalView } } func performClosePresentation(for screenID: ScreenID) { guard screenRegistry.screenContext(for: screenID) != nil else { return } windowCoordinator.presentClose(for: screenID) } }