168 lines
4.6 KiB
Swift
168 lines
4.6 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 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.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)
|
|
}
|
|
|
|
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 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)
|
|
}
|
|
}
|
|
}
|