Files
downterm/Downterm/CommandNotch/Models/WorkspaceController.swift

175 lines
4.8 KiB
Swift

import SwiftUI
import Combine
@MainActor
protocol TerminalSessionFactoryType {
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession
}
struct LiveTerminalSessionFactory: TerminalSessionFactoryType {
func makeSession(fontSize: CGFloat, theme: TerminalTheme, shellPath: String) -> TerminalSession {
TerminalSession(fontSize: fontSize, theme: theme, shellPath: shellPath)
}
}
@MainActor
final class WorkspaceController: ObservableObject {
let id: WorkspaceID
let createdAt: Date
@Published private(set) var name: String
@Published private(set) var hotkey: HotkeyBinding?
@Published private(set) var tabs: [TerminalSession] = []
@Published private(set) var activeTabIndex: Int = 0
private let sessionFactory: TerminalSessionFactoryType
private let settingsProvider: TerminalSessionConfigurationProviding
private var titleObservers: [UUID: AnyCancellable] = [:]
init(
summary: WorkspaceSummary,
sessionFactory: TerminalSessionFactoryType,
settingsProvider: TerminalSessionConfigurationProviding,
bootstrapDefaultTab: Bool = true
) {
self.id = summary.id
self.name = summary.name
self.createdAt = summary.createdAt
self.hotkey = summary.hotkey
self.sessionFactory = sessionFactory
self.settingsProvider = settingsProvider
if bootstrapDefaultTab {
newTab()
}
}
convenience init(summary: WorkspaceSummary) {
self.init(
summary: summary,
sessionFactory: LiveTerminalSessionFactory(),
settingsProvider: AppSettingsController.shared
)
}
var summary: WorkspaceSummary {
WorkspaceSummary(id: id, name: name, createdAt: createdAt, hotkey: hotkey)
}
var state: WorkspaceState {
WorkspaceState(
id: id,
name: name,
tabs: tabs.map { WorkspaceTabState(id: $0.id, title: $0.title) },
activeTabID: activeTab?.id
)
}
var activeTab: TerminalSession? {
guard tabs.indices.contains(activeTabIndex) else { return nil }
return tabs[activeTabIndex]
}
var activeTitle: String {
activeTab?.title ?? "shell"
}
func rename(to updatedName: String) {
let trimmed = updatedName.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, trimmed != name else { return }
name = trimmed
}
func updateHotkey(_ updatedHotkey: HotkeyBinding?) {
guard hotkey != updatedHotkey else { return }
hotkey = updatedHotkey
}
func newTab() {
let config = settingsProvider.terminalSessionConfiguration
let session = sessionFactory.makeSession(
fontSize: config.fontSize,
theme: config.theme,
shellPath: config.shellPath
)
titleObservers[session.id] = session.$title
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.objectWillChange.send()
}
tabs.append(session)
activeTabIndex = tabs.count - 1
}
func closeTab(at index: Int) {
guard tabs.indices.contains(index) else { return }
let session = tabs.remove(at: index)
titleObservers.removeValue(forKey: session.id)
session.terminate()
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 switchToTab(id: UUID) {
guard let index = tabs.firstIndex(where: { $0.id == id }) 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
}
func detachTab(at index: Int) -> TerminalSession? {
guard tabs.indices.contains(index) else { return nil }
let session = tabs.remove(at: index)
titleObservers.removeValue(forKey: session.id)
if tabs.isEmpty {
newTab()
} else if activeTabIndex >= tabs.count {
activeTabIndex = tabs.count - 1
}
return session
}
func detachActiveTab() -> TerminalSession? {
detachTab(at: activeTabIndex)
}
func updateAllFontSizes(_ size: CGFloat) {
for tab in tabs {
tab.updateFontSize(size)
}
}
func updateAllThemes(_ theme: TerminalTheme) {
for tab in tabs {
tab.applyTheme(theme)
}
}
}