#!/usr/bin/env bash
|
set -euo pipefail
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
INFO_PLIST="$ROOT_DIR/Apptag/Info.plist"
|
APPTAG_APP_SWIFT="$ROOT_DIR/Apptag/ApptagApp.swift"
|
PREFERENCES_VIEW_SWIFT="$ROOT_DIR/Apptag/PreferencesView.swift"
|
L10N_SWIFT="$ROOT_DIR/Apptag/L10n.swift"
|
LOCALIZATION_DIR="$ROOT_DIR/Apptag/Localization"
|
|
fail() {
|
printf 'FAIL: %s\n' "$*" >&2
|
exit 1
|
}
|
|
[[ -f "$INFO_PLIST" ]] || fail "missing Info.plist"
|
[[ -f "$APPTAG_APP_SWIFT" ]] || fail "missing ApptagApp.swift"
|
[[ -f "$PREFERENCES_VIEW_SWIFT" ]] || fail "missing PreferencesView.swift"
|
[[ -f "$L10N_SWIFT" ]] || fail "missing L10n.swift"
|
[[ -d "$LOCALIZATION_DIR" ]] || fail "missing Localization directory"
|
|
plutil -lint "$INFO_PLIST" >/dev/null || fail "Info.plist is not valid"
|
|
SCHEME=$(/usr/libexec/PlistBuddy -c "Print :CFBundleURLTypes:0:CFBundleURLSchemes:0" "$INFO_PLIST" 2>/dev/null || true)
|
[[ "$SCHEME" == "taglauncher" ]] || fail "Info.plist must register taglauncher URL scheme"
|
|
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$INFO_PLIST")
|
BUILD=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$INFO_PLIST")
|
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "invalid CFBundleShortVersionString: $VERSION"
|
[[ "$BUILD" =~ ^[0-9]{8}\.[0-9]{4}$ ]] || fail "invalid CFBundleVersion: $BUILD"
|
|
python3 - "$ROOT_DIR" <<'PY'
|
import json
|
import pathlib
|
import re
|
import sys
|
|
root = pathlib.Path(sys.argv[1])
|
app = (root / "Apptag" / "ApptagApp.swift").read_text(encoding="utf-8")
|
preferences = (root / "Apptag" / "PreferencesView.swift").read_text(encoding="utf-8")
|
l10n = (root / "Apptag" / "L10n.swift").read_text(encoding="utf-8")
|
localization_dir = root / "Apptag" / "Localization"
|
|
|
def fail(message: str) -> None:
|
print(f"FAIL: {message}", file=sys.stderr)
|
sys.exit(1)
|
|
|
def require(pattern: str, source: str, message: str) -> None:
|
if not re.search(pattern, source, re.S):
|
fail(message)
|
|
|
require(
|
r"func\s+application\s*\(\s*_\s+application\s*:\s*NSApplication\s*,\s*open\s+urls\s*:\s*\[URL\]\s*\)",
|
app,
|
"AppDelegate must implement application(_:open:) for URL scheme activation",
|
)
|
require(r'externalInvocationScheme\s*=\s*"taglauncher"', app, "URL scheme constant must be taglauncher")
|
require(r'externalInvocationShowHost\s*=\s*"show"', app, "URL show route constant must be show")
|
require(r"pendingShowOverlayInvocation", app, "cold-launch URL requests must have a pending show flag")
|
require(r"didFinishLaunching", app, "URL requests must be gated on launch readiness")
|
require(
|
r"private\s+func\s+requestShowOverlayFromExternalInvocation\s*\(\)\s*\{(?P<body>.*?)showOrFocusOverlay\s*\(",
|
app,
|
"external show invocation must call showOrFocusOverlay()",
|
)
|
request_match = re.search(
|
r"private\s+func\s+requestShowOverlayFromExternalInvocation\s*\(\)\s*\{(?P<body>.*?)\n\s*private\s+func\s+consumePendingShowOverlayInvocationIfNeeded",
|
app,
|
re.S,
|
)
|
if not request_match:
|
fail("could not inspect requestShowOverlayFromExternalInvocation()")
|
request_body = request_match.group("body")
|
if "performToggleOverlay" in request_body or "toggleOverlay" in request_body:
|
fail("external URL show must not use toggle overlay behavior")
|
if "dismissQuickSearchIfNeeded()" not in request_body:
|
fail("external URL show must dismiss conflicting Quick Search transient state")
|
require(
|
r"private\s+func\s+consumePendingShowOverlayInvocationIfNeeded\s*\(\)\s*\{(?P<body>.*?)requestShowOverlayFromExternalInvocation\s*\(\)",
|
app,
|
"launch completion must consume pending URL show requests",
|
)
|
require(
|
r"quickSearch.globalHotkey(?P<body>.*?)trackpadGesture.title(?P<body2>.*?)taglauncher://show",
|
preferences,
|
"Hotkeys settings must document taglauncher://show for external gesture tools",
|
)
|
|
for forbidden in [
|
"MultitouchSupport",
|
"IOHID",
|
"CGEventTapCreate",
|
"AXIsProcessTrusted",
|
"InputMonitoring",
|
"addGlobalMonitorForEvents(matching: .gesture",
|
]:
|
if forbidden in app:
|
fail(f"AppDelegate must not add native/global trackpad capture: {forbidden}")
|
|
match = re.search(
|
r"static\s+let\s+supported\s*:\s*\[\(code:\s*String,\s*name:\s*String\)\]\s*=\s*\[(.*?)\n\s*\]",
|
l10n,
|
re.S,
|
)
|
if not match:
|
fail("could not read L10n.supported")
|
codes = re.findall(r'\("([^"]+)",\s*"[^"]+"\)', match.group(1))
|
if len(codes) != 29:
|
fail(f"expected 29 supported languages, found {len(codes)}")
|
|
required_keys = [
|
"trackpadGesture.title",
|
"trackpadGesture.description",
|
"trackpadGesture.status",
|
]
|
for code in codes:
|
path = localization_dir / f"{code}.json"
|
if not path.is_file():
|
fail(f"missing localization file for {code}")
|
data = json.loads(path.read_text(encoding="utf-8"))
|
missing = [key for key in required_keys if not isinstance(data.get(key), str) or not data[key].strip()]
|
if missing:
|
fail(f"{code}.json missing URL scheme gesture keys: {', '.join(missing)}")
|
if "taglauncher://show" in data["trackpadGesture.description"]:
|
fail(f"{code}.json should keep the URL in the fixed display field, not inside localized prose")
|
|
print(
|
"PASS URL scheme activation QA: taglauncher://show, show/focus semantics, "
|
f"{len(codes)} localized settings strings, no private gesture capture"
|
)
|
PY
|