#!/usr/bin/env bash
|
set -euo pipefail
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
python3 - "$ROOT_DIR" <<'PY'
|
import json
|
import re
|
import sys
|
from pathlib import Path
|
|
root = Path(sys.argv[1])
|
app_dir = root / "Apptag"
|
localization_dir = app_dir / "Localization"
|
|
EXPECTED_LANGUAGE_COUNT = 29
|
REQUIRED_HOTKEY_KEYS = [
|
"hotkeys.customize",
|
"hotkeys.restoreDefault",
|
"hotkeys.recording",
|
"hotkeys.recordingInline",
|
"hotkeys.recordingCancelled",
|
"hotkeys.saved",
|
"hotkeys.restored",
|
"hotkeys.proRequired",
|
"hotkeys.error.modifierRequired",
|
"hotkeys.error.unsupportedKey",
|
"hotkeys.error.reserved",
|
"hotkeys.error.duplicate",
|
"hotkeys.error.registrationFailed",
|
"pro.feature.customHotkeys",
|
"pro.feature.customHotkeys.benefit",
|
"pro.prompt.customHotkeys",
|
]
|
|
|
def fail(message: str) -> None:
|
raise SystemExit(f"FAIL: {message}")
|
|
|
def read(path: Path) -> str:
|
try:
|
return path.read_text(encoding="utf-8")
|
except Exception as error:
|
fail(f"could not read {path}: {error}")
|
|
|
def require(source: str, needle: str, message: str) -> None:
|
if needle not in source:
|
fail(message)
|
|
|
def function_body(source: str, signature: str) -> str:
|
start = source.find(signature)
|
if start == -1:
|
fail(f"missing function signature: {signature}")
|
brace = source.find("{", start)
|
if brace == -1:
|
fail(f"missing function body for: {signature}")
|
depth = 0
|
for index in range(brace, len(source)):
|
char = source[index]
|
if char == "{":
|
depth += 1
|
elif char == "}":
|
depth -= 1
|
if depth == 0:
|
return source[brace + 1:index]
|
fail(f"unterminated function body for: {signature}")
|
|
|
def load_json(path: Path) -> dict[str, str]:
|
try:
|
data = json.loads(path.read_text(encoding="utf-8"))
|
except Exception as error:
|
fail(f"{path.name} is not valid JSON: {error}")
|
if not isinstance(data, dict):
|
fail(f"{path.name} must contain a JSON object")
|
return data
|
|
|
settings = read(app_dir / "LauncherHotkeySettings.swift")
|
app = read(app_dir / "ApptagApp.swift")
|
preferences = read(app_dir / "PreferencesView.swift")
|
pro = read(app_dir / "ProEntitlement.swift")
|
access_views = read(app_dir / "ProAccessViews.swift")
|
defaults = read(app_dir / "AppDefaults.swift")
|
quick_search = read(app_dir / "QuickSearch.swift")
|
|
require(settings, 'private static let namespace = "customHotkeys.v1"', "custom hotkeys must use the new v1 namespace")
|
require(settings, "defaultHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey", "default hotkey helper is missing")
|
require(settings, "kind.hotkey", "default hotkey must remain the fixed built-in shortcut")
|
require(settings, "ProEntitlementPolicy.isUnlocked(.customHotkeys)", "effective hotkey must be gated by Pro entitlement")
|
require(settings, "structuralValidationError(for: saved) == nil", "stored hotkeys must pass structural validation before use")
|
require(settings, "!duplicatesOtherEffectiveHotkey(saved, kind: kind)", "effective hotkey must reject duplicated saved shortcuts")
|
require(settings, "effectiveHotkeyIgnoringDuplicateCheck(for:", "duplicate validation must avoid recursive effectiveHotkey calls")
|
require(settings, "kEventKeyModifierFnMask", "Fn modifier support is required for the default Quick Search hotkey")
|
require(settings, "modifiers == UInt32(kEventKeyModifierFnMask)", "Fn-only must be restricted explicitly")
|
require(settings, "hotkey.keyCode == UInt32(kVK_Space)", "Fn-only must only be valid for Space")
|
require(settings, "isReservedBySystem", "system-reserved shortcuts must be rejected")
|
require(settings, "modifiers == UInt32(shiftKey)", "Shift-only must remain rejected")
|
if "modifiers == UInt32(shiftKey) || modifiers == UInt32(optionKey)" in settings:
|
fail("Option-only shortcuts such as Option+Space must be allowed")
|
require(settings, "private static let functionKeyCodes: Set<Int>", "function keys must use an explicit Carbon key-code set")
|
require(settings, "functionKeyCodes.contains(key)", "supported-key validation must check the explicit function key set")
|
if "kVK_F1...kVK_F12" in settings:
|
fail("Carbon function key codes are not contiguous; kVK_F1...kVK_F12 crashes at runtime")
|
|
effective_body = function_body(settings, "static func effectiveHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey")
|
if "validationError(for:" in effective_body:
|
fail("effectiveHotkey must not call validationError because duplicate checks can recurse")
|
|
validation_body = function_body(settings, "static func validationError(for hotkey: LauncherHotkey, kind: LauncherHotkeyKind) -> LauncherHotkeyValidationError?")
|
require(validation_body, "structuralValidationError(for:", "public validation must run structural checks")
|
require(validation_body, "duplicatesOtherEffectiveHotkey", "public validation must reject duplicates")
|
|
apply_body = function_body(app, "func applyCustomHotkey(_ hotkey: LauncherHotkey, for kind: LauncherHotkeyKind) -> LauncherHotkeyCustomizationResult")
|
require(apply_body, "guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else", "AppDelegate save path must be Pro-gated")
|
require(apply_body, "LauncherHotkeySettings.validationError(for: hotkey, kind: kind)", "AppDelegate save path must validate the candidate")
|
require(apply_body, "activateHotkey(hotkey, for: kind, persistCustom: true)", "custom save must go through transactional activation")
|
|
restore_body = function_body(app, "func restoreDefaultHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkeyCustomizationResult")
|
require(restore_body, "guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else", "restore default path must be Pro-gated")
|
require(restore_body, "persistCustom: false", "restore default must clear custom storage after successful registration")
|
|
activate_body = function_body(app, "private func activateHotkey(")
|
try_register_index = activate_body.find("tryRegisterHotkey(hotkey")
|
success_index = activate_body.find("case .success(let newRef):")
|
if try_register_index == -1 or success_index == -1:
|
fail("transactional registration body is incomplete")
|
success_body = activate_body[success_index:]
|
unregister_index = success_body.find("UnregisterEventHotKey(oldRef)")
|
save_index = success_body.find("LauncherHotkeySettings.save")
|
if unregister_index == -1 or save_index == -1:
|
fail("transactional success branch is incomplete")
|
if not (try_register_index < success_index and unregister_index < save_index):
|
fail("custom hotkey must register first, then unregister old ref, then persist")
|
require(activate_body, "return .registrationFailed(status)", "registration failure must not save the candidate")
|
|
require(app, "observeProEntitlementChanges()", "AppDelegate must observe Pro entitlement changes")
|
require(app, "static private(set) weak var shared: AppDelegate?", "Preferences must have a stable AppDelegate bridge for custom hotkey writes")
|
require(app, "Self.shared = self", "AppDelegate must publish its stable shared bridge during initialization")
|
require(app, "private var configuredHotkeysSuspendedForRecording = false", "AppDelegate must track hotkey recording suspension")
|
require(app, "registerConfiguredHotkeys()", "Pro entitlement changes must re-register effective hotkeys")
|
require(app, "LauncherHotkeySettings.effectiveHotkey(for: .main).displayString", "menu title must display the effective App Grid shortcut")
|
require(app, "LauncherHotkeySettings.effectiveHotkey(for: kind)", "hotkey registration must read effective shortcuts")
|
require(app, "RegisterEventHotKey", "Carbon RegisterEventHotKey must remain the global hotkey backend")
|
require(app, "UnregisterEventHotKey(oldRef)", "old hotkey ref must be released after successful replacement")
|
|
register_configured_body = function_body(app, "private func registerConfiguredHotkeys()")
|
require(register_configured_body, "guard !configuredHotkeysSuspendedForRecording else { return }", "configured hotkeys must stay unregistered while recording")
|
|
suspend_body = function_body(app, "func suspendConfiguredHotkeysForRecording()")
|
require(suspend_body, "guard !configuredHotkeysSuspendedForRecording else { return }", "recording suspension must be idempotent")
|
require(suspend_body, "configuredHotkeysSuspendedForRecording = true", "recording suspension must set the guard flag")
|
require(suspend_body, "unregisterHotkey(for: .main)", "recording must suspend the App Grid global hotkey")
|
require(suspend_body, "unregisterHotkey(for: .quickSearch)", "recording must suspend the Quick Search global hotkey")
|
if "LauncherHotkeySettings.save" in suspend_body or "clearCustomHotkey" in suspend_body:
|
fail("recording suspension must not write custom hotkey storage")
|
|
resume_body = function_body(app, "func resumeConfiguredHotkeysAfterRecording()")
|
require(resume_body, "guard configuredHotkeysSuspendedForRecording else { return }", "recording resume must be idempotent")
|
require(resume_body, "configuredHotkeysSuspendedForRecording = false", "recording resume must clear the guard flag")
|
require(resume_body, "registerConfiguredHotkeys()", "recording resume must restore effective hotkeys")
|
|
for forbidden in ["CGEventTapCreate", "AXIsProcessTrusted", "IOHIDManagerCreate", "addGlobalMonitorForEvents(matching: .keyDown"]:
|
if forbidden in app + preferences + settings + quick_search:
|
fail(f"forbidden API used for custom hotkeys: {forbidden}")
|
|
require(preferences, "presentSettingsProPrompt(for: .customHotkeys)", "free users must see the Pro prompt instead of recording")
|
require(preferences, "NSEvent.addLocalMonitorForEvents(matching: .keyDown)", "recording must use local key-down capture in Settings")
|
require(preferences, "Int(event.keyCode) == kVK_Escape", "Esc must cancel recording")
|
require(preferences, "LauncherHotkeySettings.candidate(from: event)", "recording must normalize NSEvent into LauncherHotkey")
|
require(preferences, "AppDelegate.shared?", "recording must use the stable AppDelegate bridge instead of NSApp.delegate casting")
|
require(preferences, "appDelegate.suspendConfiguredHotkeysForRecording()", "recording must suspend existing global hotkeys before capture")
|
require(preferences, "AppDelegate.shared?.resumeConfiguredHotkeysAfterRecording()", "recording cleanup must restore effective global hotkeys")
|
require(preferences, ".applyCustomHotkey(candidate, for: kind)", "recording must call the AppDelegate transactional save path")
|
require(preferences, "CustomizableHotkeyInfoRow", "Settings hotkey rows must expose custom controls")
|
require(preferences, "StaticHotkeyInfoRow(", "internal App Grid Space shortcut must remain static")
|
require(preferences, "LauncherHotkeySettings.effectiveHotkey(for: .main).displayString", "Settings must display effective App Grid shortcut")
|
require(preferences, "LauncherHotkeySettings.effectiveHotkey(for: .quickSearch).displayString", "Settings must display effective Quick Search shortcut")
|
require(preferences, "HotkeyPrimaryActionButton", "custom hotkey rows must use the shared action button")
|
require(preferences, 'customizeTitle: tr("hotkeys.customize")', "locked custom hotkey actions must still show the action label")
|
require(preferences, 'proBadgeTitle: tr("pro.card.badge")', "locked custom hotkey actions must compose the global Pro badge beside the action label")
|
require(preferences, "ProStatusPill(text: proBadgeTitle, style: .locked, compact: true)", "locked custom hotkey action must use the global Pro pill")
|
require(preferences, "if canRestore {", "restore-default action must only appear when a custom shortcut exists")
|
|
start_recording_body = function_body(preferences, "private func startHotkeyRecording(for kind: LauncherHotkeyKind)")
|
suspend_index = start_recording_body.find("appDelegate.suspendConfiguredHotkeysForRecording()")
|
monitor_index = start_recording_body.find("NSEvent.addLocalMonitorForEvents(matching: .keyDown)")
|
if suspend_index == -1 or monitor_index == -1 or not suspend_index < monitor_index:
|
fail("recording must suspend global hotkeys before installing the local monitor")
|
|
stop_recording_body = function_body(preferences, "private func stopHotkeyRecording(showCancelledToast: Bool = false)")
|
require(stop_recording_body, "let wasRecording", "recording cleanup must only resume when a recording was active")
|
require(stop_recording_body, "NSEvent.removeMonitor(hotkeyRecorderMonitor)", "recording cleanup must remove the local monitor")
|
require(stop_recording_body, "recordingHotkeyKind = nil", "recording cleanup must clear recording state")
|
require(stop_recording_body, "AppDelegate.shared?.resumeConfiguredHotkeysAfterRecording()", "recording cleanup must restore global hotkeys")
|
|
capture_recording_body = function_body(preferences, "private func captureHotkeyEvent(_ event: NSEvent)")
|
stop_index = capture_recording_body.find("stopHotkeyRecording(showCancelledToast: false)")
|
handle_index = capture_recording_body.find("handleHotkeyCustomizationResult(result)")
|
if stop_index == -1 or handle_index == -1 or not stop_index < handle_index:
|
fail("recording capture must restore global hotkeys before showing save/failure feedback")
|
|
require(preferences, "if newTab != .hotkeys", "leaving the Hotkeys tab must end recording")
|
require(preferences, "stopHotkeyRecording(showCancelledToast: false)", "recording must clean up on tab change, Pro changes, and view disappearance")
|
|
custom_row_start = preferences.find("private struct CustomizableHotkeyInfoRow")
|
custom_row_end = preferences.find("private struct HotkeyStatusPill")
|
if custom_row_start == -1 or custom_row_end == -1 or custom_row_end <= custom_row_start:
|
fail("could not inspect CustomizableHotkeyInfoRow layout")
|
custom_row = preferences[custom_row_start:custom_row_end]
|
if ".buttonStyle(.borderedProminent)" in custom_row:
|
fail("custom hotkey rows must not use the default blue borderedProminent Pro button")
|
|
hotkey_button_start = preferences.find("private struct HotkeyPrimaryActionButton")
|
hotkey_button_end = preferences.find("private struct HotkeySecondaryActionButton")
|
if hotkey_button_start == -1 or hotkey_button_end == -1 or hotkey_button_end <= hotkey_button_start:
|
fail("could not inspect HotkeyPrimaryActionButton layout")
|
hotkey_button = preferences[hotkey_button_start:hotkey_button_end]
|
require(hotkey_button, ".buttonStyle(.bordered)", "locked custom hotkey action must visually match Data tab Pro-gated buttons")
|
|
require(pro, "case customHotkeys", "ProFeature.customHotkeys is missing")
|
require(pro, "case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .unlimitedNotes, .persistentAppSorting, .customHotkeys:", "customHotkeys must be unlocked only by the Pro access state")
|
require(pro, "NotificationCenter.default.post(name: .tagLauncherProEntitlementChanged", "entitlement changes must notify hotkey registration")
|
require(access_views, "case .customHotkeys:", "Pro prompt routing must include custom hotkeys")
|
|
cleanup_body = function_body(defaults, "private static func removeShortcutCustomizationDefaults()")
|
if "customHotkeys.v1" in cleanup_body:
|
fail("legacy cleanup must not delete new custom hotkey storage")
|
|
language_files = sorted(localization_dir.glob("*.json"))
|
if len(language_files) != EXPECTED_LANGUAGE_COUNT:
|
fail(f"expected {EXPECTED_LANGUAGE_COUNT} localization files, found {len(language_files)}")
|
|
for path in language_files:
|
data = load_json(path)
|
missing = [
|
key for key in REQUIRED_HOTKEY_KEYS
|
if not isinstance(data.get(key), str) or not data[key].strip()
|
]
|
if missing:
|
fail(f"{path.name} missing custom hotkey translations: {', '.join(missing)}")
|
if "%status%" not in data["hotkeys.error.registrationFailed"]:
|
fail(f"{path.name} hotkeys.error.registrationFailed must keep %status% placeholder")
|
|
print("PASS Pro custom hotkeys static QA: Pro gating, recording suspension, transactional save path, duplicate-safe effective shortcuts, local recording, Pro-consistent row UI, and 29-language copy are wired")
|
PY
|
|
swift - <<'SWIFT'
|
import Carbon
|
import Darwin
|
|
func fail(_ message: String) -> Never {
|
fputs("FAIL: \(message)\n", stderr)
|
exit(1)
|
}
|
|
@discardableResult
|
func register(
|
label: String,
|
keyCode: Int,
|
modifiers: UInt32,
|
id: UInt32,
|
expectSuccess: Bool
|
) -> EventHotKeyRef? {
|
let hotkeyID = EventHotKeyID(signature: OSType(0x54474C51), id: id) // 'TGLQ'
|
var ref: EventHotKeyRef?
|
let status = RegisterEventHotKey(
|
UInt32(keyCode),
|
modifiers,
|
hotkeyID,
|
GetApplicationEventTarget(),
|
0,
|
&ref
|
)
|
if expectSuccess {
|
guard status == noErr, let ref else {
|
fail("\(label) should register successfully, got status \(status)")
|
}
|
return ref
|
}
|
if status == noErr {
|
if let ref { UnregisterEventHotKey(ref) }
|
fail("\(label) should not register successfully")
|
}
|
return nil
|
}
|
|
let modifiers = UInt32(controlKey | optionKey | shiftKey)
|
let mainRef = register(
|
label: "main representative shortcut",
|
keyCode: kVK_ANSI_9,
|
modifiers: modifiers,
|
id: 1,
|
expectSuccess: true
|
)
|
let quickSearchRef = register(
|
label: "quick search representative shortcut",
|
keyCode: kVK_ANSI_8,
|
modifiers: modifiers,
|
id: 2,
|
expectSuccess: true
|
)
|
let replacementRef = register(
|
label: "same-id replacement shortcut",
|
keyCode: kVK_ANSI_7,
|
modifiers: modifiers,
|
id: 1,
|
expectSuccess: true
|
)
|
let optionSpaceRef = register(
|
label: "Option-Space shortcut",
|
keyCode: kVK_Space,
|
modifiers: UInt32(optionKey),
|
id: 4,
|
expectSuccess: true
|
)
|
_ = register(
|
label: "duplicate shortcut conflict",
|
keyCode: kVK_ANSI_7,
|
modifiers: modifiers,
|
id: 3,
|
expectSuccess: false
|
)
|
|
if let optionSpaceRef { UnregisterEventHotKey(optionSpaceRef) }
|
if let replacementRef { UnregisterEventHotKey(replacementRef) }
|
if let quickSearchRef { UnregisterEventHotKey(quickSearchRef) }
|
if let mainRef { UnregisterEventHotKey(mainRef) }
|
|
print("PASS Pro custom hotkeys runtime QA: Carbon accepts representative custom registrations, Option-Space, same-id replacement, and rejects duplicate active shortcuts")
|
SWIFT
|