#!/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", "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, .customContainerQuota, .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