Files
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

381 lines
12 KiB
Swift

import AppKit
import Carbon
import Foundation
import OSLog
import SwiftUI
import MacToolsPluginKit
public final class PhysicalCleanModePluginFactory: NSObject, MacToolsPluginBundleFactory {
public static func makeProvider(context: PluginRuntimeContext) throws -> any PluginProvider {
PhysicalCleanModePluginProvider(context: context)
}
}
@MainActor
private struct PhysicalCleanModePluginProvider: PluginProvider {
let context: PluginRuntimeContext
func makePlugins() -> [any MacToolsPlugin] {
[PhysicalCleanModePlugin(context: context)]
}
}
@MainActor
final class PhysicalCleanModePlugin: MacToolsPlugin, PluginPrimaryPanel, AccessibilityPermissionRefreshing {
private enum StorageKey {
static let legacyEnabledState = "feature.cleanModeEnabled"
}
private enum ActionID {
static let exitPhysicalCleanMode = "exitPhysicalCleanMode"
}
private enum PermissionID {
static let accessibility = "accessibility"
}
private enum ShortcutID {
static let exitPhysicalCleanMode = "exit-physical-clean-mode"
}
private enum ShortcutSettingsGroupID {
static let exitPhysicalCleanMode = "physical-clean-mode.exit"
}
let metadata: PluginMetadata
let primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
controlStyle: .switch,
menuActionBehavior: .dismissBeforeHandling
)
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
private let storage: PluginStorage
private let localization: PluginLocalization
private let logger = PhysicalCleanModeLog.plugin
private var isAccessibilityGranted: Bool
private var lastErrorKey: String?
private var lastErrorMessage: String?
private var session: PhysicalCleanModeSession?
init(
context: PluginRuntimeContext = PluginRuntimeContext(pluginID: "physical-clean-mode"),
userDefaults: UserDefaults? = nil
) {
let localization = PluginLocalization(bundle: context.resourceBundle)
self.localization = localization
self.storage = userDefaults.map {
UserDefaultsPluginStorage(pluginID: context.pluginID, userDefaults: $0)
} ?? context.storage
self.metadata = PluginMetadata(
id: "physical-clean-mode",
title: localization.string("metadata.title", defaultValue: "清洁模式"),
iconName: "sparkles",
iconTint: Color(nsColor: .systemCyan),
order: 100,
defaultDescription: localization.string(
"metadata.description",
defaultValue: "屏幕全黑并临时禁用键盘输入"
)
)
self.isAccessibilityGranted = AccessibilityCheck.isTrusted()
storage.migrateValueIfNeeded(
fromLegacyKey: StorageKey.legacyEnabledState,
to: StorageKey.legacyEnabledState
)
storage.removeObject(forKey: StorageKey.legacyEnabledState)
}
var primaryPanelState: PluginPanelState {
PluginPanelState(
subtitle: panelSubtitle,
isOn: session != nil,
isExpanded: false,
isEnabled: true,
isVisible: true,
detail: nil,
errorMessage: lastErrorMessage
)
}
var permissionRequirements: [PluginPermissionRequirement] {
[
PluginPermissionRequirement(
id: PermissionID.accessibility,
kind: .accessibility,
title: localization.string("permission.accessibility.title", defaultValue: "辅助功能授权"),
description: localization.string(
"permission.accessibility.description",
defaultValue: "辅助功能权限是清洁模式运行所需的必要权限。"
)
)
]
}
var settingsSections: [PluginSettingsSection] { [] }
var shortcutDefinitions: [PluginShortcutDefinition] {
[
PluginShortcutDefinition(
id: ShortcutID.exitPhysicalCleanMode,
title: localization.string("shortcut.exit.title", defaultValue: "退出清洁模式"),
description: localization.string(
"shortcut.exit.description",
defaultValue: "清洁模式启用时用于恢复输入和关闭黑屏覆盖的快捷键。"
),
actionID: ActionID.exitPhysicalCleanMode,
scope: .whilePluginActive,
defaultBinding: ShortcutBinding(
keyCode: UInt16(kVK_Escape),
modifiers: [.control, .command]
),
isRequired: true,
settingsGroupID: ShortcutSettingsGroupID.exitPhysicalCleanMode,
settingsGroupTitle: localization.string("shortcut.exit.settingsGroupTitle", defaultValue: "退出快捷键"),
settingsGroupDescription: localization.string(
"shortcut.exit.settingsGroupDescription",
defaultValue: "清洁模式启用时恢复输入并关闭黑屏覆盖。"
),
settingsControlTitle: localization.string("shortcut.exit.settingsControlTitle", defaultValue: "退出"),
settingsControlSystemImage: "keyboard"
)
]
}
func refresh() {
let previousAccessState = isAccessibilityGranted
isAccessibilityGranted = AccessibilityCheck.isTrusted()
if isAccessibilityGranted {
clearErrorIfKey("error.accessibilityRequired")
} else if session != nil {
session?.requestEmergencyExit(message: localization.string(
"error.accessibilityRevokedEmergencyExit",
defaultValue: "辅助功能授权已失效,已自动退出清洁模式。"
))
}
if previousAccessState != isAccessibilityGranted {
notifyChange()
}
}
func handleAction(_ action: PluginPanelAction) {
switch action {
case let .setSwitch(isEnabled):
if PhysicalCleanModeLog.isVerboseLoggingEnabled {
logger.debug("panel action setSwitch isEnabled=\(isEnabled, privacy: .public)")
}
setPhysicalCleanModeEnabled(isEnabled)
case .setDisclosureExpanded,
.setSelection,
.setNavigationSelection,
.clearNavigationSelection,
.setDate,
.setSlider,
.invokeAction:
break
}
}
func permissionState(for permissionID: String) -> PluginPermissionState {
switch permissionID {
case PermissionID.accessibility:
return PluginPermissionState(
isGranted: isAccessibilityGranted,
footnote: lastErrorMessage
)
default:
return PluginPermissionState(isGranted: true, footnote: nil)
}
}
func handlePermissionAction(id: String) {
guard id == PermissionID.accessibility else {
return
}
if isAccessibilityGranted {
refresh()
} else {
requestAccessibilityPermission(showSettingsGuidance: false)
}
}
func handleSettingsAction(id: String) {}
func handleShortcutAction(id: String) {
guard id == ActionID.exitPhysicalCleanMode else {
return
}
session?.requestStop(reason: .userRequested)
}
private var panelSubtitle: String {
if let session {
return localization.format(
"panel.subtitle.enabledExitFormat",
defaultValue: "已启用,使用 %@ 退出",
ShortcutFormatter.displayString(for: session.exitBinding)
)
}
if isAccessibilityGranted {
return metadata.defaultDescription
}
return localization.string("panel.subtitle.needsAccessibility", defaultValue: "启用前需要辅助功能授权")
}
private func requestAccessibilityPermission(showSettingsGuidance: Bool) {
if PhysicalCleanModeLog.isVerboseLoggingEnabled {
logger.debug("request accessibility permission showSettingsGuidance=\(showSettingsGuidance, privacy: .public)")
}
isAccessibilityGranted = AccessibilityCheck.requestTrust(prompt: true)
if isAccessibilityGranted {
clearError()
} else {
logger.notice("accessibility permission is required before entering physical clean mode")
setError("error.accessibilityRequired")
if showSettingsGuidance {
requestPermissionGuidance?(PermissionID.accessibility)
}
}
notifyChange()
}
private func setPhysicalCleanModeEnabled(_ isEnabled: Bool) {
guard isEnabled else {
clearError()
if let session {
session.requestStop(reason: .userRequested)
} else {
notifyChange()
}
return
}
enablePhysicalCleanModeIfPossible()
}
private func enablePhysicalCleanModeIfPossible() {
isAccessibilityGranted = AccessibilityCheck.isTrusted()
guard isAccessibilityGranted else {
requestAccessibilityPermission(showSettingsGuidance: true)
return
}
guard let exitBinding = shortcutBindingResolver?(ShortcutID.exitPhysicalCleanMode), exitBinding.isValid else {
logger.error("enable aborted because exit shortcut is missing or invalid")
setError("error.invalidExitShortcut")
notifyChange()
return
}
if session != nil {
return
}
let session = PhysicalCleanModeSession(
exitBinding: exitBinding,
onEnd: { [weak self] reason in
self?.handleSessionEnd(reason)
},
localization: localization
)
do {
self.session = session
if PhysicalCleanModeLog.isVerboseLoggingEnabled {
logger.debug("starting physical clean mode session exitBinding=\(ShortcutFormatter.displayString(for: exitBinding), privacy: .public)")
}
try session.start()
guard self.session === session else {
logger.error("session ended during startup sequence before completion")
return
}
clearError()
notifyChange()
} catch {
if self.session === session {
self.session = nil
}
logger.error("physical clean mode session start failed: \(error.localizedDescription, privacy: .public)")
lastErrorMessage = error.localizedDescription
notifyChange()
}
}
private func handleSessionEnd(_ reason: PhysicalCleanModeSession.EndReason) {
switch reason {
case .userRequested:
break
case .emergency:
logger.error("physical clean mode session ended unexpectedly reason=\(String(describing: reason), privacy: .public)")
}
session = nil
switch reason {
case .userRequested:
clearError()
case let .emergency(message):
setRawError(message)
}
notifyChange()
}
private func clearError() {
lastErrorKey = nil
lastErrorMessage = nil
}
private func clearErrorIfKey(_ key: String) {
guard lastErrorKey == key else { return }
clearError()
}
private func setError(_ key: String) {
lastErrorKey = key
lastErrorMessage = localizedErrorMessage(for: key)
}
private func setRawError(_ message: String) {
lastErrorKey = nil
lastErrorMessage = message
}
private func localizedErrorMessage(for key: String) -> String {
switch key {
case "error.accessibilityRequired":
return localization.string(key, defaultValue: "清洁模式需要辅助功能权限,请先前往设置完成授权。")
case "error.invalidExitShortcut":
return localization.string(key, defaultValue: "请先在功能中设置有效的退出快捷键。")
default:
return localization.string(key, defaultValue: "清洁模式不可用。")
}
}
private func notifyChange() {
onStateChange?()
}
// MARK: - AccessibilityPermissionRefreshing
func refreshAccessibilityPermission() {
refresh()
}
}