diff --git a/CONTEXT.md b/CONTEXT.md index 6f4b852c..bd4127a9 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -22,6 +22,9 @@ - **Sensitive information**: Server passwords, plugin options (sensitive in their entirety), and nonempty user-provided remote URLs, including subscription, external PAC, and custom GFW List URLs. - **Credential exposure boundary**: This product reduces accidental disclosure and exposure to other user accounts, but does not guarantee protection against a compromised same-user process, root access, or APFS snapshots and backups; it does not promise secure erasure. - **Credential reference**: A non-secret association from a server configuration, subscription, or remote-URL setting to its durable credential; the credential value is kept outside the configuration tree and resolved only when needed. +- **System-level shortcut**: A user-configurable keyboard action registered by the app that can trigger an app action even when the app has no focus. + _Avoid_: global hotkey, global shortcut. +- **Window-local default action shortcut**: A keyboard action bound to the focused app window or form context, such as confirming a dialog; it is distinct from a system-level shortcut. ## Relationships and invariants diff --git a/ShadowsocksX-NG2/App/GlobalShortcuts.swift b/ShadowsocksX-NG2/App/GlobalShortcuts.swift deleted file mode 100644 index 809f9c20..00000000 --- a/ShadowsocksX-NG2/App/GlobalShortcuts.swift +++ /dev/null @@ -1,36 +0,0 @@ -import KeyboardShortcuts - -/// 全局快捷键(spec #21 D11,issue #31):开关代理、切换模式两个系统级热键。 -/// 快捷键不迁移 Legacy(D12),用 2.0 默认键位(Name.initialShortcut:用户 -/// 未改过即生效);编辑 UI 在设置区(#33)。 -extension KeyboardShortcuts.Name { - /// ⌃⌥⌘S:开关代理。 - static let toggleProxy = Self( - "toggleProxy", - initial: .init(.s, modifiers: [.control, .option, .command])) - /// ⌃⌥⌘M:在 PAC / 全局 / 手动三个内置模式间循环切换。 - static let cycleProxyMode = Self( - "cycleProxyMode", - initial: .init(.m, modifiers: [.control, .option, .command])) -} - -enum GlobalShortcuts { - /// 接线快捷键动作(应用启动时调用一次)。两个动作都与菜单栏同名项走同一 - /// 控制器入口,状态一致由 @Published 事实来源保证;「开关代理」的意图判定 - /// 与菜单开关联用同一 `isOn(state:)` 口径(含启动失败等中间态的「开」判定)。 - @MainActor - static func wire(controller: ProxyRuntimeController) { - KeyboardShortcuts.onKeyUp(for: .toggleProxy) { - Task { @MainActor in - await controller.setProxyEnabled(!StatusMenuModel.isOn(state: controller.state)) - } - } - KeyboardShortcuts.onKeyUp(for: .cycleProxyMode) { - Task { @MainActor in - await controller.setProxyMode( - StatusMenuModel.nextMode( - after: controller.proxyMode, availableModes: controller.settings.enabledModes)) - } - } - } -} diff --git a/ShadowsocksX-NG2/App/MainApp.swift b/ShadowsocksX-NG2/App/MainApp.swift index 0c52afca..c41d53f5 100644 --- a/ShadowsocksX-NG2/App/MainApp.swift +++ b/ShadowsocksX-NG2/App/MainApp.swift @@ -41,8 +41,6 @@ struct ShadowsocksXNG2App: App { MenuBarExtra("ShadowsocksX-NG 2.0", systemImage: "network") { ProxyStatusMenu(controller: proxyController, catalogWorkflow: catalogWorkflow) .task { - // 全局快捷键(issue #31):开关代理、切换模式,与菜单项同一入口。 - GlobalShortcuts.wire(controller: proxyController) await proxyController.resyncOnLaunch() } } diff --git a/ShadowsocksX-NG2/App/SettingsView.swift b/ShadowsocksX-NG2/App/SettingsView.swift index 603f4f23..bc3e9999 100644 --- a/ShadowsocksX-NG2/App/SettingsView.swift +++ b/ShadowsocksX-NG2/App/SettingsView.swift @@ -1,5 +1,3 @@ -import AppKit -import KeyboardShortcuts import SwiftUI /// Native macOS settings for issue #33. The view edits one local snapshot and @@ -66,7 +64,7 @@ struct SettingsView: View { } Button("取消", role: .cancel) {} } message: { - Text("端口、监听范围、快捷键意图和 PAC 设置都会恢复为出厂值。") + Text("端口、监听范围和 PAC 设置都会恢复为出厂值。") } } @@ -88,13 +86,6 @@ struct SettingsView: View { .foregroundStyle(.red) } - LabeledContent("全局快捷键") { - VStack(alignment: .leading, spacing: 8) { - KeyboardShortcuts.Recorder("开关代理", name: .toggleProxy) - KeyboardShortcuts.Recorder("切换模式", name: .cycleProxyMode) - } - } - Picker("系统代理模式", selection: modeBinding) { if isModeAvailable(.pac) { Text(ProxyMode.pac.label).tag(ProxyMode.pac) @@ -407,7 +398,6 @@ extension SettingsView { Task { @MainActor in do { try await proxyController.resetPreferences() - KeyboardShortcuts.reset(.toggleProxy, .cycleProxyMode) loginController.resetToDefaults() draft = proxyController.settings refreshPortOccupancy() diff --git a/ShadowsocksX-NG2/App/StatusMenuModel.swift b/ShadowsocksX-NG2/App/StatusMenuModel.swift index 9e8a64e4..99097cc3 100644 --- a/ShadowsocksX-NG2/App/StatusMenuModel.swift +++ b/ShadowsocksX-NG2/App/StatusMenuModel.swift @@ -58,8 +58,8 @@ enum StatusMenuModel { targetPath: targetPath) } - /// 代理开关的当前意图(菜单开关与全局快捷键共用的同一判定):启动失败等 - /// 中间态视为未开——两个入口的下一步动作都是「启动」。 + /// 代理开关的当前意图(状态菜单与当前运行端口判断共用同一判定):启动失败等 + /// 中间态视为未开——状态菜单的下一步动作是「启动」。 static func isOn(state: ProxyRuntimeController.ProxyState) -> Bool { switch state { case .off, .launchFailed, .activationFailed, .serviceFailed: @@ -78,29 +78,6 @@ enum StatusMenuModel { return "export http_proxy=\(endpoint);export https_proxy=\(endpoint);" } - /// 全局快捷键「切换模式」的循环序:仅内置三模式参与;外部 PAC 的 URL 只能 - /// 在设置区配置(#33),不进循环——快捷键从外部 PAC 回到第一个可用内置模式。 - static func nextMode( - after mode: ProxyMode, - availableModes: Set = ProxySettings.defaultEnabledModes - ) -> ProxyMode { - let cycle = [ProxyModeKind.pac, .global, .manual].filter(availableModes.contains) - guard !cycle.isEmpty else { return mode } - guard let currentIndex = cycle.firstIndex(of: mode.kind) else { - return proxyMode(for: cycle[0]) - } - return proxyMode(for: cycle[(currentIndex + 1) % cycle.count]) - } - - private static func proxyMode(for kind: ProxyModeKind) -> ProxyMode { - switch kind { - case .pac: .pac - case .global: .global - case .manual: .manual - case .externalPAC: .pac - } - } - /// 活动目标的显示名路径(根 → 节点," / " 连接);基于目录树 projection /// 派生(issue #41)。未激活或目标已不在树中为 nil。 static func targetPath(in nodes: [CatalogTreeNode], activeTargetID: NodeID?) -> String? { diff --git a/ShadowsocksX-NG2/Tests/StatusMenuModelTests.swift b/ShadowsocksX-NG2/Tests/StatusMenuModelTests.swift index 5fb5cab9..c3f9c333 100644 --- a/ShadowsocksX-NG2/Tests/StatusMenuModelTests.swift +++ b/ShadowsocksX-NG2/Tests/StatusMenuModelTests.swift @@ -83,31 +83,6 @@ final class StatusMenuModelTests: XCTestCase { XCTAssertNil(StatusMenuModel.httpExportLine(settings: settings)) } - // MARK: - 全局快捷键「切换模式」循环 - - func testNextModeCyclesBuiltinModes() { - XCTAssertEqual(StatusMenuModel.nextMode(after: .pac), .global) - XCTAssertEqual(StatusMenuModel.nextMode(after: .global), .manual) - XCTAssertEqual(StatusMenuModel.nextMode(after: .manual), .pac) - } - - func testNextModeFromExternalPACReturnsToPAC() { - XCTAssertEqual( - StatusMenuModel.nextMode(after: .externalPAC(URL(string: "http://example.com/pac")!)), - .pac) - } - - func testNextModeSkipsLegacyDisabledModes() { - let enabledModes: Set = [.pac, .manual] - - XCTAssertEqual( - StatusMenuModel.nextMode(after: .pac, availableModes: enabledModes), - .manual) - XCTAssertEqual( - StatusMenuModel.nextMode(after: .manual, availableModes: enabledModes), - .pac) - } - // MARK: - 活动目标路径(目录树 projection 派生,issue #41) /// 目录树 projection 构建夹具(同工作流 module 的推导路径)。 diff --git a/ShadowsocksX-NG2/project.yml b/ShadowsocksX-NG2/project.yml index 03a93d11..1d2d1afe 100644 --- a/ShadowsocksX-NG2/project.yml +++ b/ShadowsocksX-NG2/project.yml @@ -4,12 +4,6 @@ options: developmentLanguage: zh-Hans deploymentTarget: macOS: "15.0" -# Swift 包依赖:全局快捷键(spec #21 D11,issue #31)用 KeyboardShortcuts; -# 快捷键不迁移 Legacy(D12),默认键位见 App/GlobalShortcuts.swift。 -packages: - KeyboardShortcuts: - url: https://github.com/sindresorhus/KeyboardShortcuts - from: 3.1.0 settings: base: ARCHS: arm64 @@ -96,8 +90,6 @@ targets: copy: destination: wrapper subpath: Contents/MacOS - # 全局快捷键(D11,issue #31):开关代理、切换模式。 - - package: KeyboardShortcuts ShadowsocksX-NG2Agent: type: tool platform: macOS diff --git a/docs/adr/0003-remove-system-level-shortcuts.md b/docs/adr/0003-remove-system-level-shortcuts.md new file mode 100644 index 00000000..a8065c4e --- /dev/null +++ b/docs/adr/0003-remove-system-level-shortcuts.md @@ -0,0 +1,14 @@ +# Remove system-level shortcuts from NG2 + +NG2 no longer provides user-configurable system-level shortcuts for toggling the proxy or cycling proxy modes. We remove the `KeyboardShortcuts` integration and keep status-menu controls and window-local default action shortcuts; Legacy remains frozen, and existing stored shortcut preferences are neither migrated nor proactively cleaned up. + +## Considered options + +- Keep the feature or replace it with another global shortcut library: rejected because the product now has an explicit menu-driven interaction boundary and no approved replacement gesture. +- Remove only the UI while retaining the dependency and handlers: rejected because it would leave an invisible system-level behavior and dead configuration state. + +## Consequences + +- Proxy enablement and mode selection remain available from the status menu and settings/window flows. +- The `nextMode` helper and its tests are removed because no remaining product behavior needs implicit mode cycling. +- Historical Legacy research and prior ADRs remain unchanged as historical records.