native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199import AppKitimport GhosttyKitimport SwiftUIimport Testing
@testable import Prowl
@MainActorstruct CanvasCardEventRoutingTests { @Test func commandClickOnFollowerLinkRoutesToGhosttyWithoutChangingSelection() async throws { let fixture = CanvasCardEventRoutingFixture(hasHoveredLink: true) defer { fixture.close() } await fixture.prepareForHitTesting()
let target = try #require(fixture.sendCommandClick()) await fixture.drainMainQueue()
#expect(target === fixture.surfaceView) #expect(fixture.tapRecorder.selectionTapCount == 0) #expect(fixture.tapRecorder.outerTapCount == 0) }
@Test func commandClickOnFollowerNonLinkContentStillRoutesToSelection() async throws { let fixture = CanvasCardEventRoutingFixture(hasHoveredLink: false) defer { fixture.close() } await fixture.prepareForHitTesting()
let target = try #require(fixture.sendCommandClick()) await fixture.drainMainQueue()
#expect(target !== fixture.surfaceView) #expect(fixture.tapRecorder.selectionTapCount == 1) #expect(fixture.tapRecorder.outerTapCount == 0) }
@Test func plainClickOnFocusedCardTerminalDoesNotChangeSelection() async throws { let fixture = CanvasCardEventRoutingFixture(hasHoveredLink: false, isFocused: true) defer { fixture.close() } await fixture.prepareForHitTesting()
let target = try #require(fixture.sendClick(modifierFlags: [])) await fixture.drainMainQueue()
#expect(target === fixture.surfaceView) #expect(fixture.tapRecorder.selectionTapCount == 0) #expect(fixture.tapRecorder.outerTapCount == 0) }}
@MainActorprivate final class CanvasCardEventRoutingFixture { let surfaceView: GhosttySurfaceView let tapRecorder = CanvasCardTapRecorder()
private let cardSize = CGSize(width: 320, height: 200) private let window: NSWindow private let hostingView: NSHostingView<CanvasCardView>
init(hasHoveredLink: Bool, isFocused: Bool = false) { let runtime = GhosttyRuntime() let surfaceView = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_TAB, skipsSurfaceCreationForTesting: true ) self.surfaceView = surfaceView surfaceView.bridge.state.mouseOverLink = hasHoveredLink ? "https://example.com" : nil
let tab = TerminalTabItem(title: "Follower", icon: nil) // A focused card models a plain click on the active card; every other // fixture models a Command-click on a broadcast follower. let linkActivationRequested = CanvasInteractionPolicy.linkActivationRequested( hasHoveredLink: CanvasInteractionPolicy.hasHoveredLink(in: SplitTree(view: surfaceView)), isCommandModifierActive: !isFocused ) let showsSelectionShield = CanvasInteractionPolicy.showsSelectionShield( commandSelectionActive: !isFocused, selectionModeActive: !isFocused, broadcastFollower: !isFocused, linkActivationRequested: linkActivationRequested ) let card = CanvasCardView( repositoryName: "Prowl", worktreeName: tab.displayTitle, tree: SplitTree(view: surfaceView), activeSurfaceID: surfaceView.id, unfocusedSplitOverlay: (nil, 0), isFocused: isFocused, isSelected: true, hasUnseenNotification: false, tabIcon: nil, tabId: tab.id, tabs: [tab], tabContextMenuActions: TerminalTabContextMenuActions( renameTab: { _ in }, changeIcon: { _ in }, closeTab: { _ in }, closeOthers: { _ in }, closeToRight: { _ in }, closeAll: {} ), cardSize: cardSize, isExpanded: false, expandHelp: "Expand card", canvasScale: 1, linkActivationRequested: linkActivationRequested, showsSelectionShield: showsSelectionShield, onTap: { [tapRecorder] in tapRecorder.outerTapCount += 1 }, onSelectionTap: { [tapRecorder] in tapRecorder.selectionTapCount += 1 }, onDragCommit: { _ in }, onResize: { _, _ in }, onResizeEnd: {}, onSplitOperation: { _ in }, onTitleBarTap: {}, onExpand: {}, onClose: {} )
let contentSize = CGSize(width: cardSize.width, height: cardSize.height + 28) window = NSWindow( contentRect: NSRect(origin: .zero, size: contentSize), styleMask: [.borderless], backing: .buffered, defer: false ) hostingView = NSHostingView(rootView: card) hostingView.frame = NSRect(origin: .zero, size: contentSize) window.contentView = hostingView }
func prepareForHitTesting() async { window.orderFront(nil) hostingView.layoutSubtreeIfNeeded() await drainMainQueue() hostingView.layoutSubtreeIfNeeded() }
func sendCommandClick() -> NSView? { sendClick(modifierFlags: .command) }
func sendClick(modifierFlags: NSEvent.ModifierFlags) -> NSView? { let point = NSPoint(x: cardSize.width / 2, y: cardSize.height / 2) let target = hostingView.hitTest(point) let locationInWindow = hostingView.convert(point, to: nil) let timestamp = ProcessInfo.processInfo.systemUptime
guard let mouseDown = NSEvent.mouseEvent( with: .leftMouseDown, location: locationInWindow, modifierFlags: modifierFlags, timestamp: timestamp, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1 ), let mouseUp = NSEvent.mouseEvent( with: .leftMouseUp, location: locationInWindow, modifierFlags: modifierFlags, timestamp: timestamp, windowNumber: window.windowNumber, context: nil, eventNumber: 2, clickCount: 1, pressure: 0 ) else { return nil }
window.sendEvent(mouseDown) window.sendEvent(mouseUp) return target }
func close() { window.orderOut(nil) }
func drainMainQueue() async { await withCheckedContinuation { continuation in DispatchQueue.main.async { continuation.resume() } } }}
@MainActorprivate final class CanvasCardTapRecorder { var outerTapCount = 0 var selectionTapCount = 0}