#!/usr/bin/env bash
|
set -euo pipefail
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
APP_DELEGATE="$ROOT_DIR/Apptag/ApptagApp.swift"
|
QUICK_SEARCH="$ROOT_DIR/Apptag/QuickSearch.swift"
|
CONTENT_VIEW="$ROOT_DIR/Apptag/ContentView.swift"
|
|
python3 - "$APP_DELEGATE" "$QUICK_SEARCH" "$CONTENT_VIEW" <<'PY'
|
import re
|
import sys
|
from pathlib import Path
|
|
app_delegate = Path(sys.argv[1]).read_text(encoding="utf-8")
|
quick_search = Path(sys.argv[2]).read_text(encoding="utf-8")
|
content_view = Path(sys.argv[3]).read_text(encoding="utf-8")
|
|
|
def fail(message: str) -> None:
|
print(f"FAIL: {message}", file=sys.stderr)
|
sys.exit(1)
|
|
|
def require(condition: bool, message: str) -> None:
|
if not condition:
|
fail(message)
|
|
|
def require_contains(text: str, needle: str, message: str) -> None:
|
require(needle in text, message)
|
|
|
local_monitor = re.search(
|
r"private func installQuickSearchLocalMouseMonitor\(\).*?private func removeQuickSearchExternalMouseMonitor\(\)",
|
app_delegate,
|
re.S,
|
)
|
require(local_monitor is not None, "Quick Search local mouse monitor block is missing")
|
local_monitor_body = local_monitor.group(0)
|
require_contains(
|
local_monitor_body,
|
"if event.window === self.overlayWindow {\n return event\n }",
|
"local mouse monitor must pass through overlayWindow events so result rows receive clicks",
|
)
|
require(
|
"tagLauncherQuickSearchDismissRequested" not in local_monitor_body,
|
"local mouse monitor must not dismiss/swallow clicks inside the overlay window",
|
)
|
|
panel_apply = re.search(
|
r"func apply\(_ configuration: QuickSearchPanelConfiguration, anchorView: QuickSearchPanelAnchorView\).*?func closePanel\(\)",
|
quick_search,
|
re.S,
|
)
|
require(panel_apply is not None, "QuickSearchPanel apply block is missing")
|
panel_apply_body = panel_apply.group(0)
|
activate_index = panel_apply_body.find("NSApp.activate(ignoringOtherApps: true)")
|
make_key_index = panel_apply_body.find("panel.makeKeyAndOrderFront(nil)")
|
require(activate_index >= 0, "Quick Search panel must activate the app before accepting keyboard input")
|
require(make_key_index >= 0, "Quick Search panel must be made key and ordered front")
|
require(
|
activate_index < make_key_index,
|
"Quick Search panel activation must happen before makeKeyAndOrderFront",
|
)
|
|
panel_factory = re.search(
|
r"private func ensurePanel\(parentWindow: NSWindow\).*?let panel = QuickSearchPanel\((.*?)\n \)",
|
quick_search,
|
re.S,
|
)
|
require(panel_factory is not None, "QuickSearchPanel factory block is missing")
|
require(
|
".nonactivatingPanel" not in panel_factory.group(0),
|
"Quick Search panel must not use .nonactivatingPanel; Return can otherwise go to the previous app",
|
)
|
require_contains(
|
quick_search,
|
"override var canBecomeKey: Bool { true }",
|
"Quick Search panel must be allowed to become key",
|
)
|
panel_class = re.search(
|
r"private final class QuickSearchPanel: NSPanel.*?final class QuickSearchPanelAnchorView",
|
quick_search,
|
re.S,
|
)
|
require(panel_class is not None, "QuickSearchPanel class block is missing")
|
panel_class_body = panel_class.group(0)
|
for needle, message in [
|
("override func sendEvent(_ event: NSEvent)", "Quick Search panel must intercept mouse events before SwiftUI/scroll intermediate views can drop result clicks"),
|
("event.type == .leftMouseDown", "Quick Search panel click fallback must be limited to left mouse down"),
|
("quickSearchResultRow(at: event.locationInWindow)", "Quick Search panel must resolve the clicked result row from the window point"),
|
("row.launchFromMouseClick()", "Quick Search panel must launch the resolved result row directly"),
|
("quickSearchResultRowFromHitTest(atWindowPoint: windowPoint)", "Quick Search panel should keep the direct hitTest path for normal AppKit row clicks"),
|
("quickSearchResultRowDescendant(containingWindowPoint: windowPoint)", "Quick Search panel must use a recursive geometry fallback when SwiftUI/scroll intermediate views hide rows from hitTest"),
|
]:
|
require_contains(panel_class_body, needle, message)
|
|
for needle, message in [
|
("func quickSearchResultRowDescendant(containingWindowPoint windowPoint: NSPoint) -> QuickSearchResultRowView?", "Quick Search must expose a recursive row lookup fallback"),
|
("for subview in subviews.reversed()", "Quick Search recursive lookup must scan frontmost subviews first"),
|
("row.containsWindowPointForLaunch(windowPoint)", "Quick Search recursive lookup must verify the clicked window point is inside a result row"),
|
]:
|
require_contains(quick_search, needle, message)
|
|
text_field = re.search(
|
r"private struct QuickSearchTextField: NSViewRepresentable.*?private final class QuickSearchNativeTextField",
|
quick_search,
|
re.S,
|
)
|
require(text_field is not None, "QuickSearchTextField block is missing")
|
text_field_body = text_field.group(0)
|
for needle, message in [
|
("field.target = context.coordinator", "Quick Search text field must set a target for Return submit fallback"),
|
("field.action = #selector(Coordinator.submitAction(_:))", "Quick Search text field must set submitAction as its action"),
|
("@objc func submitAction(_ sender: Any?)", "Quick Search coordinator must expose submitAction"),
|
("onCommand?(.submit)", "Quick Search submitAction/delegate path must emit .submit"),
|
]:
|
require_contains(text_field_body, needle, message)
|
|
row_view = re.search(
|
r"final class QuickSearchResultRowView: NSView.*?private func setup\(\)",
|
quick_search,
|
re.S,
|
)
|
require(row_view is not None, "QuickSearchResultRowView block is missing")
|
row_view_body = row_view.group(0)
|
require_contains(
|
row_view_body,
|
"override func acceptsFirstMouse(for event: NSEvent?) -> Bool {\n true\n }",
|
"Quick Search result rows must accept first mouse",
|
)
|
require_contains(
|
row_view_body,
|
"override func mouseDown(with event: NSEvent)",
|
"Quick Search result rows must handle mouseDown",
|
)
|
require_contains(
|
row_view_body,
|
"onLaunch(result)",
|
"Quick Search result rows must launch their result on click",
|
)
|
require_contains(
|
row_view_body,
|
"func launchFromMouseClick() -> Bool",
|
"Quick Search result rows must expose a panel-level click fallback",
|
)
|
require_contains(
|
row_view_body,
|
"func containsWindowPointForLaunch(_ windowPoint: NSPoint) -> Bool",
|
"Quick Search result rows must expose geometry-based click containment for panel fallback",
|
)
|
require_contains(
|
row_view_body,
|
"bounds.contains(localPoint) && visibleRect.contains(localPoint)",
|
"Quick Search result row geometry fallback must reject hidden or clipped row areas",
|
)
|
require_contains(
|
row_view_body,
|
"DispatchQueue.main.async { [onLaunch, result] in",
|
"Quick Search row launch fallback must capture the clicked result before async launch",
|
)
|
|
command_submit = re.search(
|
r"private func handleQuickSearchCommand\(_ command: QuickSearchCommand\).*?private var selectedQuickSearchResult",
|
content_view,
|
re.S,
|
)
|
require(command_submit is not None, "ContentView Quick Search command handler is missing")
|
require_contains(
|
command_submit.group(0),
|
"case .submit:\n guard let selected = selectedQuickSearchResult else { return }\n launchQuickSearchResult(selected)",
|
"Quick Search submit command must launch the selected result",
|
)
|
require_contains(
|
content_view,
|
"NSWorkspace.shared.openApplication(at: app.path, configuration: configuration)",
|
"Quick Search launch path must still use NSWorkspace.openApplication",
|
)
|
|
print("PASS quick search launch contract")
|
PY
|