This commit is contained in:
@@ -0,0 +1,459 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user