Files
ggbond268--mactools/Plugins/LaunchControl/Sources/LaunchControlModels.swift
T
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

460 lines
14 KiB
Swift

import Foundation
import MacToolsPluginKit
enum LaunchControlScope: String, CaseIterable, Identifiable, Sendable {
case user
case global
case system
var id: String { rawValue }
var title: String {
title()
}
func title(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
switch self {
case .user:
return localization.string("scope.user.title", defaultValue: "当前用户")
case .global:
return localization.string("scope.global.title", defaultValue: "全局")
case .system:
return localization.string("scope.system.title", defaultValue: "系统")
}
}
}
enum LaunchControlState: String, CaseIterable, Identifiable, Sendable {
case running
case loaded
case disabled
case failed
case unloaded
case unknown
var id: String { rawValue }
var title: String {
title()
}
func title(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
switch self {
case .running:
return localization.string("state.running.title", defaultValue: "运行中")
case .loaded:
return localization.string("state.loaded.title", defaultValue: "已加载")
case .disabled:
return localization.string("state.disabled.title", defaultValue: "已禁用")
case .failed:
return localization.string("state.failed.title", defaultValue: "异常退出")
case .unloaded:
return localization.string("state.unloaded.title", defaultValue: "未加载")
case .unknown:
return localization.string("state.unknown.title", defaultValue: "未知")
}
}
}
enum LaunchControlOrigin: String, CaseIterable, Identifiable, Sendable {
case userCreated
case thirdParty
case system
var id: String { rawValue }
var title: String {
title()
}
func title(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
switch self {
case .userCreated:
return localization.string("origin.userCreated.title", defaultValue: "用户创建")
case .thirdParty:
return localization.string("origin.thirdParty.title", defaultValue: "应用创建")
case .system:
return localization.string("origin.system.title", defaultValue: "系统内置")
}
}
}
enum LaunchControlOriginFilter: String, CaseIterable, Identifiable {
case all
case favorite
case manageable
case userCreated
case appCreated
case system
case readOnly
var id: String { rawValue }
var title: String {
title()
}
func title(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
switch self {
case .all:
return localization.string("originFilter.all.title", defaultValue: "全部来源")
case .favorite:
return localization.string("originFilter.favorite.title", defaultValue: "已关注")
case .manageable:
return localization.string("originFilter.manageable.title", defaultValue: "可操作")
case .userCreated:
return LaunchControlOrigin.userCreated.title(localization: localization)
case .appCreated:
return LaunchControlOrigin.thirdParty.title(localization: localization)
case .system:
return LaunchControlOrigin.system.title(localization: localization)
case .readOnly:
return localization.string("originFilter.readOnly.title", defaultValue: "只读")
}
}
func matches(_ item: LaunchControlItem) -> Bool {
switch self {
case .all:
return true
case .favorite:
return item.isFavorite
case .manageable:
return item.canManage
case .userCreated:
return item.origin == .userCreated
case .appCreated:
return item.origin == .thirdParty
case .system:
return item.origin == .system
case .readOnly:
return !item.canManage
}
}
}
enum LaunchControlFilter: String, CaseIterable, Identifiable {
case all
case user
case global
case system
var id: String { rawValue }
var title: String {
title()
}
func title(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
switch self {
case .all:
return localization.string("scopeFilter.all.title", defaultValue: "全部范围")
case .user:
return LaunchControlScope.user.title(localization: localization)
case .global:
return LaunchControlScope.global.title(localization: localization)
case .system:
return LaunchControlScope.system.title(localization: localization)
}
}
var scope: LaunchControlScope? {
switch self {
case .all:
return nil
case .user:
return .user
case .global:
return .global
case .system:
return .system
}
}
}
enum LaunchControlStateFilter: String, CaseIterable, Identifiable {
case all
case running
case loaded
case disabled
case failed
var id: String { rawValue }
var title: String {
title()
}
func title(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
switch self {
case .all:
return localization.string("stateFilter.all.title", defaultValue: "全部状态")
case .running:
return LaunchControlState.running.title(localization: localization)
case .loaded:
return LaunchControlState.loaded.title(localization: localization)
case .disabled:
return LaunchControlState.disabled.title(localization: localization)
case .failed:
return LaunchControlState.failed.title(localization: localization)
}
}
func matches(_ state: LaunchControlState) -> Bool {
switch self {
case .all:
return true
case .running:
return state == .running
case .loaded:
return state == .loaded || state == .running
case .disabled:
return state == .disabled
case .failed:
return state == .failed
}
}
}
struct LaunchControlItem: Identifiable, Equatable, Sendable {
let id: String
let label: String
let plistURL: URL
let scope: LaunchControlScope
let origin: LaunchControlOrigin
let state: LaunchControlState
let pid: Int?
let lastExitStatus: Int?
let programArguments: [String]
let runAtLoad: Bool
let keepAliveDescription: String?
let startInterval: Int?
let startCalendarDescription: String?
let rawPlist: String
let launchctlDomain: String
let isDisabled: Bool
let isLoaded: Bool
var isFavorite: Bool
/// User-authored local note (persisted separately; never written to the plist).
var note: String = ""
var commandText: String {
commandText()
}
func commandText(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
if !programArguments.isEmpty {
return programArguments.joined(separator: " ")
}
return localization.string("item.commandText.empty", defaultValue: "未声明 ProgramArguments")
}
var triggerSummary: String {
triggerSummary()
}
func triggerSummary(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
var parts: [String] = []
if runAtLoad {
parts.append(localization.string("item.trigger.runAtLoad", defaultValue: "登录/加载时运行"))
}
if let keepAliveDescription {
parts.append(localization.format(
"item.trigger.keepAlive",
defaultValue: "KeepAlive: %@",
keepAliveDescription
))
}
if let startInterval {
parts.append(localization.format("item.trigger.startInterval", defaultValue: "每 %d 秒", startInterval))
}
if let startCalendarDescription {
parts.append(localization.format(
"item.trigger.startCalendar",
defaultValue: "定时: %@",
startCalendarDescription
))
}
return parts.isEmpty
? localization.string("item.trigger.empty", defaultValue: "未声明自动触发条件")
: parts.joined(separator: " · ")
}
var canManage: Bool {
scope == .user && !label.isEmpty && plistURL.path.hasPrefix(FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/LaunchAgents").path)
}
var statusText: String {
statusText()
}
func statusText(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
if let pid {
return localization.format(
"item.status.pid",
defaultValue: "%@ · PID %d",
state.title(localization: localization),
pid
)
}
if let lastExitStatus {
return localization.format(
"item.status.exitCode",
defaultValue: "%@ · 退出码 %d",
state.title(localization: localization),
lastExitStatus
)
}
return state.title(localization: localization)
}
}
struct LaunchControlSnapshot: Equatable {
var items: [LaunchControlItem] = []
var selectedItemID: String?
var isRefreshing = false
var lastRefreshDate: Date?
var errorMessage: String?
var operationMessage: String?
var scanLogEntries: [String] = []
var currentScanTarget: String?
var selectedItem: LaunchControlItem? {
guard let selectedItemID else {
return items.first
}
return items.first(where: { $0.id == selectedItemID }) ?? items.first
}
}
@MainActor
final class LaunchControlFavoritesStore {
private enum StorageKey {
static let storage = "launch-control.favorite-item-ids"
}
private let storage: PluginStorage
init(
context: PluginRuntimeContext = PluginRuntimeContext(pluginID: "launch-control"),
userDefaults: UserDefaults? = nil
) {
self.storage = userDefaults.map {
UserDefaultsPluginStorage(pluginID: context.pluginID, userDefaults: $0)
} ?? context.storage
storage.migrateValueIfNeeded(fromLegacyKey: StorageKey.storage, to: StorageKey.storage)
}
func favoriteItemIDs() -> Set<String> {
Set(storage.stringArray(forKey: StorageKey.storage) ?? [])
}
func isFavorite(_ itemID: String) -> Bool {
favoriteItemIDs().contains(itemID)
}
func setFavorite(_ isFavorite: Bool, for itemID: String) {
var favorites = favoriteItemIDs()
if isFavorite {
favorites.insert(itemID)
} else {
favorites.remove(itemID)
}
storage.set(favorites.sorted(), forKey: StorageKey.storage)
}
}
/// Persists per-item user notes locally (keyed by item ID / plist path). Notes are
/// never written back to the launchd plist — they live only in plugin storage.
@MainActor
final class LaunchControlNotesStore {
private enum StorageKey {
static let storage = "launch-control.item-notes"
}
private let storage: PluginStorage
init(
context: PluginRuntimeContext = PluginRuntimeContext(pluginID: "launch-control"),
userDefaults: UserDefaults? = nil
) {
self.storage = userDefaults.map {
UserDefaultsPluginStorage(pluginID: context.pluginID, userDefaults: $0)
} ?? context.storage
}
func allNotes() -> [String: String] {
guard
let data = storage.data(forKey: StorageKey.storage),
let notes = try? JSONDecoder().decode([String: String].self, from: data)
else {
return [:]
}
return notes
}
func note(for itemID: String) -> String {
allNotes()[itemID] ?? ""
}
func setNote(_ note: String, for itemID: String) {
var notes = allNotes()
let trimmed = note.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
notes.removeValue(forKey: itemID)
} else {
notes[itemID] = trimmed
}
guard let data = try? JSONEncoder().encode(notes) else { return }
storage.set(data, forKey: StorageKey.storage)
}
}
enum LaunchControlScanEvent: Sendable {
case directory(String)
case file(String)
case found(LaunchControlItem)
case message(String)
}
enum LaunchControlManagedAction: String, Sendable {
case bootstrap
case bootout
case enable
case disable
case start
case stop
case restart
func title(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
switch self {
case .bootstrap:
return localization.string("managedAction.bootstrap.title", defaultValue: "加载")
case .bootout:
return localization.string("managedAction.bootout.title", defaultValue: "卸载")
case .enable:
return localization.string("managedAction.enable.title", defaultValue: "启用")
case .disable:
return localization.string("managedAction.disable.title", defaultValue: "禁用")
case .start:
return localization.string("managedAction.start.title", defaultValue: "启动")
case .stop:
return localization.string("managedAction.stop.title", defaultValue: "停止")
case .restart:
return localization.string("managedAction.restart.title", defaultValue: "重启")
}
}
}
struct LaunchControlPlistSummary: Sendable {
let label: String
let programArguments: [String]
let runAtLoad: Bool
let keepAliveDescription: String?
let startInterval: Int?
let startCalendarDescription: String?
let rawPlist: String
}