Files
Termind/Frameworks/TermindApplication/FeatureModels/TerminalHighlightSettingsModel.swift
T
Kaede Akino 82d871c4fe refactor: separate feature policies and UI workflows
Move connection policy out of ProAccess, keep terminal uploads with their
session, and share question interaction and highlight editing operations.
Consolidate CPU usage and settings snapshot handling, and remove obsolete
terminal selection, theme helpers, and hard-coded latest-schema assertions.

Fix draining connection-pool recovery and stale pane upload bindings. Keep
screenshot matrices opt-in and retain behavioral migration coverage.

Validation: 2468 macOS tests passed serially, 31 iOS tests passed, and both
SSH forwarding regressions passed against the local OpenSSH fixture.
Both app builds, Swift formatting, AppStrings, and Chinese localization
checks passed. Parallel test runs still exhibit intermittent timeouts.
2026-10-02 16:40:27 +08:00

135 lines
4.5 KiB
Swift

import Foundation
import TermindDomain
@Observable
public final class TerminalHighlightSettingsModel {
public enum ValidationIssue: Equatable {
case builtIn
case invalidRule
case invalidPattern(String)
case ruleLimit
}
public enum Failure: Equatable {
case validation(ValidationIssue)
case storage(String)
}
public private(set) var preferences = TerminalHighlightPreferences()
public private(set) var failure: Failure?
private let persistence: (any TerminalHighlightPreferencesPersisting)?
private let validatePattern: @MainActor (TerminalHighlightRule) -> String?
public init(
persistence: (any TerminalHighlightPreferencesPersisting)? = nil,
validatePattern: @escaping @MainActor (TerminalHighlightRule) -> String? = { _ in nil }
) {
self.persistence = persistence
self.validatePattern = validatePattern
do { if let persistence { preferences = try persistence.loadTerminalHighlightPreferences() } } catch {
failure = .storage(error.localizedDescription)
}
}
public var storageError: String? {
if case .storage(let message) = failure { message } else { nil }
}
public var canAddRule: Bool { preferences.rules.count < TerminalHighlightPreferences.maximumCustomRuleCount }
public func validationIssue(for rule: TerminalHighlightRule) -> ValidationIssue? {
if rule.isBuiltIn { return .builtIn }
if !rule.isValid { return .invalidRule }
if let issue = patternValidationIssue(for: rule) { return .invalidPattern(issue) }
if !canAddRule && !preferences.rules.contains(where: { $0.id == rule.id }) { return .ruleLimit }
return nil
}
public func patternValidationIssue(for rule: TerminalHighlightRule) -> String? {
rule.pattern.isEmpty ? nil : validatePattern(rule)
}
@discardableResult
private func save(_ next: TerminalHighlightPreferences) -> Bool {
guard next.isValid else {
failure = .validation(.invalidRule)
return false
}
guard next != preferences else {
failure = nil
return true
}
do {
try persistence?.saveTerminalHighlightPreferences(next)
applyRemoteSnapshot(next)
return true
} catch {
failure = .storage(error.localizedDescription)
return false
}
}
@discardableResult
public func setEnabled(_ enabled: Bool) -> Bool {
var next = preferences
next.isEnabled = enabled
return save(next)
}
@discardableResult
public func upsert(_ rule: TerminalHighlightRule) -> Bool {
if let issue = validationIssue(for: rule) {
failure = .validation(issue)
return false
}
var next = preferences
if let index = next.rules.firstIndex(where: { $0.id == rule.id }) {
next.rules[index] = rule
} else {
next.rules.append(rule)
}
return save(next)
}
@discardableResult
public func delete(_ id: UUID) -> Bool {
var next = preferences
next.rules.removeAll { $0.id == id }
return save(next)
}
public func canMove(_ id: UUID, by offset: Int) -> Bool {
guard let index = preferences.rules.firstIndex(where: { $0.id == id }) else { return false }
return preferences.rules.indices.contains(index + offset)
}
@discardableResult
public func move(_ id: UUID, by offset: Int) -> Bool {
guard canMove(id, by: offset), let index = preferences.rules.firstIndex(where: { $0.id == id }) else {
return false
}
var next = preferences
next.rules.swapAt(index, index + offset)
return save(next)
}
@discardableResult
public func move(fromOffsets indices: IndexSet, toOffset destination: Int) -> Bool {
guard indices.allSatisfy(preferences.rules.indices.contains),
(0...preferences.rules.count).contains(destination)
else { return false }
var next = preferences
let moved = indices.map { next.rules[$0] }
for index in indices.reversed() { next.rules.remove(at: index) }
let insertion = destination - indices.filter { $0 < destination }.count
next.rules.insert(contentsOf: moved, at: insertion)
return save(next)
}
public func applyRemoteSnapshot(_ value: TerminalHighlightPreferences) {
guard value.isValid else { return }
preferences = value
failure = nil
}
}