127 lines
4.3 KiB
Swift
127 lines
4.3 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import TermindDomain
|
|
|
|
/// Settings edits persist first, then update every window and the terminal runtime.
|
|
@Observable
|
|
public final class TerminalSettingsModel {
|
|
public private(set) var preferences = TerminalPreferences()
|
|
public private(set) var themeLibrary = TerminalThemeLibrary()
|
|
public private(set) var storageError: String?
|
|
private let persistence: (any TerminalPreferencesPersisting)?
|
|
|
|
public init(persistence: (any TerminalPreferencesPersisting)? = nil) {
|
|
self.persistence = persistence
|
|
do {
|
|
if let persistence {
|
|
preferences = try persistence.loadTerminal()
|
|
themeLibrary = try persistence.loadTerminalThemes()
|
|
}
|
|
} catch {
|
|
storageError = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
public func set<Value>(_ value: Value, for keyPath: WritableKeyPath<TerminalPreferences, Value>) -> Bool {
|
|
var next = preferences
|
|
next[keyPath: keyPath] = value
|
|
return save(next)
|
|
}
|
|
|
|
/// A theme choice is one persisted edit, including disabling the custom overlay.
|
|
@discardableResult
|
|
public func selectTheme(named name: String, isDark: Bool) -> Bool {
|
|
if persistence?.usesNativeTerminalThemes == true || !themeLibrary.selections.isEmpty {
|
|
var next = themeLibrary
|
|
next.select(.init(appearance: isDark ? .dark : .light, builtInThemeName: name))
|
|
return saveLibrary(next)
|
|
}
|
|
var next = preferences
|
|
if isDark {
|
|
next.darkTheme = name
|
|
next.darkCustomEnabled = false
|
|
} else {
|
|
next.lightTheme = name
|
|
next.lightCustomEnabled = false
|
|
}
|
|
return save(next)
|
|
}
|
|
|
|
@discardableResult
|
|
public func selectCustomTheme(isDark: Bool) -> Bool {
|
|
set(true, for: isDark ? \.darkCustomEnabled : \.lightCustomEnabled)
|
|
}
|
|
|
|
private func save(_ next: TerminalPreferences) -> Bool {
|
|
guard next != preferences else { return true }
|
|
do {
|
|
try persistence?.saveTerminal(next)
|
|
applyRemoteSnapshot(next)
|
|
return true
|
|
} catch {
|
|
storageError = error.localizedDescription
|
|
return false
|
|
}
|
|
}
|
|
|
|
public func applyRemoteSnapshot(_ value: TerminalPreferences) {
|
|
preferences = value
|
|
storageError = nil
|
|
}
|
|
|
|
public func applyRemoteThemes(_ value: TerminalThemeLibrary) {
|
|
themeLibrary = value
|
|
storageError = nil
|
|
}
|
|
|
|
@discardableResult
|
|
public func saveCustomTheme(_ value: TerminalCustomTheme, isNew: Bool) -> Bool {
|
|
guard value.isValid, isNew != themeLibrary.themes.contains(where: { $0.id == value.id }) else { return false }
|
|
var next = themeLibrary
|
|
next.themes.removeAll { $0.id == value.id }
|
|
next.themes.append(value)
|
|
next.repairSelections(for: value.id)
|
|
return saveLibrary(next)
|
|
}
|
|
|
|
@discardableResult
|
|
public func deleteCustomTheme(id: UUID) -> Bool {
|
|
var next = themeLibrary
|
|
next.themes.removeAll { $0.id == id }
|
|
next.repairSelections(for: id)
|
|
return saveLibrary(next)
|
|
}
|
|
|
|
@discardableResult
|
|
public func selectCustomTheme(id: UUID, isDark: Bool) -> Bool {
|
|
guard themeLibrary.themes.contains(where: { $0.id == id && $0.appearance.includes(isDark: isDark) }) else {
|
|
return false
|
|
}
|
|
var next = themeLibrary
|
|
let legacyBase = isDark ? preferences.darkTheme : preferences.lightTheme
|
|
var selection =
|
|
next.selection(isDark: isDark)
|
|
?? .init(
|
|
appearance: isDark ? .dark : .light,
|
|
builtInThemeName: legacyBase.isEmpty ? (isDark ? "Termind Dark" : "Termind Light") : legacyBase)
|
|
selection.customThemeID = id
|
|
next.select(selection)
|
|
return saveLibrary(next)
|
|
}
|
|
|
|
private func saveLibrary(_ value: TerminalThemeLibrary) -> Bool {
|
|
guard value != themeLibrary else { return true }
|
|
do {
|
|
try persistence?.saveTerminalThemes(value)
|
|
applyRemoteThemes(value)
|
|
return true
|
|
} catch {
|
|
storageError = error.localizedDescription
|
|
return false
|
|
}
|
|
}
|
|
}
|