mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-03 17:43:44 +00:00
refactor(ng2): remove system-level shortcuts
Keep proxy controls explicit and leave existing shortcut preferences untouched.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<ProxyModeKind> = 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? {
|
||||
|
||||
@@ -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<ProxyModeKind> = [.pac, .manual]
|
||||
|
||||
XCTAssertEqual(
|
||||
StatusMenuModel.nextMode(after: .pac, availableModes: enabledModes),
|
||||
.manual)
|
||||
XCTAssertEqual(
|
||||
StatusMenuModel.nextMode(after: .manual, availableModes: enabledModes),
|
||||
.pac)
|
||||
}
|
||||
|
||||
// MARK: - 活动目标路径(目录树 projection 派生,issue #41)
|
||||
|
||||
/// 目录树 projection 构建夹具(同工作流 module 的推导路径)。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user