import SwiftUI import Combine /// Manages multiple terminal tabs. Singleton shared across all screens — /// whichever notch is currently open displays these tabs. @MainActor class TerminalManager: ObservableObject { static let shared = TerminalManager() @Published var tabs: [TerminalSession] = [] @Published var activeTabIndex: Int = 0 @AppStorage(NotchSettings.Keys.terminalFontSize) private var fontSize: Double = NotchSettings.Defaults.terminalFontSize @AppStorage(NotchSettings.Keys.terminalTheme) private var theme: String = NotchSettings.Defaults.terminalTheme private var cancellables = Set() private init() { newTab() } // MARK: - Active tab var activeTab: TerminalSession? { guard tabs.indices.contains(activeTabIndex) else { return nil } return tabs[activeTabIndex] } /// Short title for the closed notch bar — the active tab's process name. var activeTitle: String { activeTab?.title ?? "shell" } // MARK: - Tab operations func newTab() { let session = TerminalSession( fontSize: CGFloat(fontSize), theme: TerminalTheme.resolve(theme) ) // Forward title changes to trigger view updates in this manager session.$title .receive(on: RunLoop.main) .sink { [weak self] _ in self?.objectWillChange.send() } .store(in: &cancellables) tabs.append(session) activeTabIndex = tabs.count - 1 } func closeTab(at index: Int) { guard tabs.indices.contains(index) else { return } tabs[index].terminate() tabs.remove(at: index) // Adjust active index if tabs.isEmpty { newTab() } else if activeTabIndex >= tabs.count { activeTabIndex = tabs.count - 1 } } func closeActiveTab() { closeTab(at: activeTabIndex) } func switchToTab(at index: Int) { guard tabs.indices.contains(index) else { return } activeTabIndex = index } func nextTab() { guard tabs.count > 1 else { return } activeTabIndex = (activeTabIndex + 1) % tabs.count } func previousTab() { guard tabs.count > 1 else { return } activeTabIndex = (activeTabIndex - 1 + tabs.count) % tabs.count } /// Removes the tab at the given index and returns the session so it /// can be hosted in a pop-out window. func detachTab(at index: Int) -> TerminalSession? { guard tabs.indices.contains(index) else { return nil } let session = tabs.remove(at: index) if tabs.isEmpty { newTab() } else if activeTabIndex >= tabs.count { activeTabIndex = tabs.count - 1 } return session } func detachActiveTab() -> TerminalSession? { detachTab(at: activeTabIndex) } /// Updates font size on all existing terminal sessions. func updateAllFontSizes(_ size: CGFloat) { for tab in tabs { tab.updateFontSize(size) } } func updateAllThemes(_ theme: TerminalTheme) { for tab in tabs { tab.applyTheme(theme) } } }