native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580import AppKitimport Foundationimport GhosttyKitimport Testing
@testable import Prowl
@MainActorstruct GhosttySurfaceViewTests { @Test func editingAndCompositionAreLocalToEachSurface() { let runtime = GhosttyRuntime() let edited = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_TAB, skipsSurfaceCreationForTesting: true ) let sibling = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_TAB, skipsSurfaceCreationForTesting: true ) let state = WorktreeTerminalState( runtime: runtime, worktree: Worktree( id: "/tmp/input-test", name: "input-test", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/input-test"), repositoryRootURL: URL(fileURLWithPath: "/tmp") ), skipsSurfaceCreationForTesting: true ) state.surfaces[edited.id] = edited state.surfaces[sibling.id] = sibling #expect(edited.lastEditingAt == nil) edited.setMarkedText("draft", selectedRange: NSRange(location: 5, length: 0), replacementRange: NSRange()) #expect(edited.hasMarkedText()) #expect(edited.lastEditingAt != nil) #expect(sibling.lastEditingAt == nil) #expect(!sibling.hasMarkedText()) let candidates = state.closeProtectionCandidates(surfaceIDs: [edited.id, sibling.id]) #expect(candidates[0].hasMarkedText) #expect(candidates[0].editingAge != nil) #expect(candidates[1].editingAge == nil) #expect(TerminalCloseConfirmationPolicy.decision(for: candidates).protectedPaneCount == 1) edited.unmarkText() #expect(!edited.hasMarkedText()) #expect(edited.lastEditingAt != nil) }
@Test func mainMenuExactMatchRejectsShiftVariantOfCommandComma() throws { let menu = NSMenu() let item = NSMenuItem(title: "Settings", action: nil, keyEquivalent: ",") item.keyEquivalentModifierMask = [.command] menu.addItem(item)
let event = try makeKeyEvent( characters: "<", charactersIgnoringModifiers: ",", modifiers: [.command, .shift], keyCode: 43 )
#expect(!GhosttySurfaceView.mainMenuHasMatchingItem(for: event, in: menu)) }
@Test func mainMenuExactMatchAcceptsExactCommandComma() throws { let menu = NSMenu() let item = NSMenuItem(title: "Settings", action: nil, keyEquivalent: ",") item.keyEquivalentModifierMask = [.command] menu.addItem(item)
let event = try makeKeyEvent( characters: ",", charactersIgnoringModifiers: ",", modifiers: [.command], keyCode: 43 )
#expect(GhosttySurfaceView.mainMenuHasMatchingItem(for: event, in: menu)) }
@Test func mainMenuExactMatchAcceptsShiftedSymbolKeyEquivalent() throws { let menu = NSMenu() let item = NSMenuItem(title: "Help", action: nil, keyEquivalent: "?") item.keyEquivalentModifierMask = [.command] menu.addItem(item)
let event = try makeKeyEvent( characters: "?", charactersIgnoringModifiers: "/", modifiers: [.command, .shift], keyCode: 44 )
#expect(GhosttySurfaceView.mainMenuHasMatchingItem(for: event, in: menu)) }
@Test func mainMenuExactMatchRejectsUnshiftedVariantOfShiftedSymbolKeyEquivalent() throws { let menu = NSMenu() let item = NSMenuItem(title: "Help", action: nil, keyEquivalent: "?") item.keyEquivalentModifierMask = [.command] menu.addItem(item)
let event = try makeKeyEvent( characters: "/", charactersIgnoringModifiers: "/", modifiers: [.command], keyCode: 44 )
#expect(!GhosttySurfaceView.mainMenuHasMatchingItem(for: event, in: menu)) }
@Test func mainMenuExactMatchFindsSubmenuItems() throws { let menu = NSMenu() let parent = NSMenuItem(title: "View", action: nil, keyEquivalent: "") let submenu = NSMenu() let item = NSMenuItem(title: "Show Diff", action: nil, keyEquivalent: "y") item.keyEquivalentModifierMask = [.command, .shift] submenu.addItem(item) parent.submenu = submenu menu.addItem(parent)
let event = try makeKeyEvent( characters: "Y", charactersIgnoringModifiers: "y", modifiers: [.command, .shift], keyCode: 16 )
#expect(GhosttySurfaceView.mainMenuHasMatchingItem(for: event, in: menu)) }
@Test func keyEquivalentFocusOwnershipRequiresActualFirstResponder() { #expect(GhosttySurfaceView.hasKeyEquivalentFocusOwnership(cachedFocused: true, isActualFirstResponder: true)) #expect(!GhosttySurfaceView.hasKeyEquivalentFocusOwnership(cachedFocused: true, isActualFirstResponder: false)) #expect(!GhosttySurfaceView.hasKeyEquivalentFocusOwnership(cachedFocused: false, isActualFirstResponder: true)) }
@Test func occlusionStateResendsDesiredValueAfterAttachmentChange() { var state = GhosttySurfaceView.OcclusionState()
let firstApply = state.prepareToApply(true) let secondApply = state.prepareToApply(true)
#expect(firstApply) #expect(!secondApply) let desired = state.invalidateForAttachmentChange() let reapply = state.prepareToApply(true)
#expect(desired == true) #expect(reapply) }
@Test func occlusionStateDoesNotResendBeforeAnyDesiredValueExists() { var state = GhosttySurfaceView.OcclusionState()
let desired = state.invalidateForAttachmentChange() let firstApply = state.prepareToApply(false) let secondApply = state.prepareToApply(false)
#expect(desired == nil) #expect(firstApply) #expect(!secondApply) }
@Test func occlusionStateStoresDesiredValueWithoutMarkingItApplied() { var state = GhosttySurfaceView.OcclusionState()
state.setDesired(true) let firstApply = state.prepareToApply(true) let secondApply = state.prepareToApply(true)
#expect(firstApply) #expect(!secondApply) }
@Test func occlusionStateUsesLatestDeferredDesiredValue() { var state = GhosttySurfaceView.OcclusionState()
state.setDesired(true) state.setDesired(false) let applyDeferredValue = state.prepareToApply(false) let secondApply = state.prepareToApply(false)
#expect(applyDeferredValue) #expect(!secondApply) }
@Test func occlusionStateAppliesLatestValueAfterAttachmentInvalidation() { var state = GhosttySurfaceView.OcclusionState()
let firstApply = state.prepareToApply(true) let desiredAfterAttachmentChange = state.invalidateForAttachmentChange() #expect(firstApply) #expect(desiredAfterAttachmentChange == true)
state.setDesired(false) let applyLatestValue = state.prepareToApply(false) let duplicateApply = state.prepareToApply(false)
#expect(applyLatestValue) #expect(!duplicateApply) }
@Test func occlusionStateRetainsLatestDesiredValueAcrossMultipleAttachmentChanges() { var state = GhosttySurfaceView.OcclusionState()
state.setDesired(true) let desiredAfterFirstAttachmentChange = state.invalidateForAttachmentChange() #expect(desiredAfterFirstAttachmentChange == true) state.setDesired(false) let desiredAfterSecondAttachmentChange = state.invalidateForAttachmentChange() #expect(desiredAfterSecondAttachmentChange == false)
let applyLatestValue = state.prepareToApply(false) let duplicateApply = state.prepareToApply(false)
#expect(applyLatestValue) #expect(!duplicateApply) }
@Test func launchEnvironmentCarriesThePaneIdentity() { let runtime = GhosttyRuntime() let surfaceView = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_SPLIT, environment: ["PROWL_WORKTREE_PATH": "/repo/wt", "PROWL_PANE_ID": "forged"], skipsSurfaceCreationForTesting: true ) let sibling = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_SPLIT, skipsSurfaceCreationForTesting: true )
#expect(surfaceView.launchEnvironment["PROWL_PANE_ID"] == surfaceView.id.uuidString) #expect(surfaceView.launchEnvironment["PROWL_WORKTREE_PATH"] == "/repo/wt") #expect(sibling.launchEnvironment["PROWL_PANE_ID"] == sibling.id.uuidString) #expect(sibling.id != surfaceView.id) }
@Test func launchEnvironmentRecordsACodexSessionLogPerPane() { let runtime = GhosttyRuntime() let surfaceView = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_SPLIT, skipsSurfaceCreationForTesting: true ) let explicit = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_SPLIT, environment: ["CODEX_TUI_SESSION_LOG_PATH": "/custom.jsonl"], skipsSurfaceCreationForTesting: true )
#expect(surfaceView.launchEnvironment["CODEX_TUI_RECORD_SESSION"] == "1") #expect( surfaceView.launchEnvironment["CODEX_TUI_SESSION_LOG_PATH"] == CodexTUISessionLog.url(for: surfaceView.id).path(percentEncoded: false)) #expect(explicit.launchEnvironment["CODEX_TUI_SESSION_LOG_PATH"] == "/custom.jsonl") }
@Test func occlusionDoesNotApplyUntilViewHasSuperviewAndWindow() async { let runtime = GhosttyRuntime() let surfaceView = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_TAB, skipsSurfaceCreationForTesting: true ) var appliedValues: [Bool] = [] surfaceView.onOcclusionAppliedForTesting = { appliedValues.append($0) } var attachmentState = (hasSuperview: false, hasWindow: false) surfaceView.attachmentStateForTesting = { attachmentState }
surfaceView.setOcclusion(true) await drainMainQueue() #expect(appliedValues.isEmpty)
attachmentState = (hasSuperview: true, hasWindow: false) surfaceView.handleAttachmentChangeForTesting() await drainMainQueue() #expect(appliedValues.isEmpty)
attachmentState = (hasSuperview: true, hasWindow: true) surfaceView.handleAttachmentChangeForTesting() await drainMainQueue() #expect(appliedValues == [true]) }
@Test func occlusionAppliesLatestDeferredValueAfterWindowReattachment() async { let runtime = GhosttyRuntime() let surfaceView = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_TAB, skipsSurfaceCreationForTesting: true ) var appliedValues: [Bool] = [] surfaceView.onOcclusionAppliedForTesting = { appliedValues.append($0) } var attachmentState = (hasSuperview: true, hasWindow: true) surfaceView.attachmentStateForTesting = { attachmentState }
surfaceView.handleAttachmentChangeForTesting() await drainMainQueue() #expect(appliedValues.isEmpty)
surfaceView.setOcclusion(true) await drainMainQueue() #expect(appliedValues == [true])
attachmentState = (hasSuperview: false, hasWindow: false) surfaceView.handleAttachmentChangeForTesting() await drainMainQueue()
// Occluding (false) applies immediately even while detached to stop // the render loop. Un-occluding (true) is deferred until reattached. surfaceView.setOcclusion(false) surfaceView.setOcclusion(true) surfaceView.setOcclusion(false) await drainMainQueue() #expect(appliedValues == [true, false])
attachmentState = (hasSuperview: true, hasWindow: true) surfaceView.handleAttachmentChangeForTesting() await drainMainQueue() // Reattachment re-applies the desired value (false) even though it // was already applied, because invalidateForAttachmentChange clears // the applied cache. #expect(appliedValues == [true, false, false]) }
@Test func occlusionFalseAppliesImmediatelyWithoutViewAttachment() async { let runtime = GhosttyRuntime() let surfaceView = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_TAB, skipsSurfaceCreationForTesting: true ) var appliedValues: [Bool] = [] surfaceView.onOcclusionAppliedForTesting = { appliedValues.append($0) } var attachmentState = (hasSuperview: false, hasWindow: false) surfaceView.attachmentStateForTesting = { attachmentState }
// Occluding without a view hierarchy applies immediately (stops the // Metal render loop for restored surfaces that are never displayed). surfaceView.setOcclusion(false) await drainMainQueue() #expect(appliedValues == [false])
// Un-occluding without a view hierarchy is deferred. surfaceView.setOcclusion(true) await drainMainQueue() #expect(appliedValues == [false])
// Once attached, the deferred un-occlude is applied. attachmentState = (hasSuperview: true, hasWindow: true) surfaceView.handleAttachmentChangeForTesting() await drainMainQueue() #expect(appliedValues == [false, true]) }
@Test func occlusionCanRecoverWhenAttachmentCallbackIsMissedAfterReattachment() async { let runtime = GhosttyRuntime() let surfaceView = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_TAB, skipsSurfaceCreationForTesting: true ) var appliedValues: [Bool] = [] surfaceView.onOcclusionAppliedForTesting = { appliedValues.append($0) } var attachmentState = (hasSuperview: true, hasWindow: true) surfaceView.attachmentStateForTesting = { attachmentState }
surfaceView.setOcclusion(true) await drainMainQueue() #expect(appliedValues == [true])
attachmentState = (hasSuperview: false, hasWindow: false) surfaceView.handleAttachmentChangeForTesting() await drainMainQueue()
attachmentState = (hasSuperview: true, hasWindow: true) surfaceView.resumeDeferredOcclusionIfNeededForTesting() await drainMainQueue() #expect(appliedValues == [true, true]) }
@Test func terminalHostReattachesSurfaceOnlyAfterItLeavesTheViewTree() { let runtime = GhosttyRuntime() let surfaceView = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_TAB, skipsSurfaceCreationForTesting: true ) let terminalHost = GhosttySurfaceScrollView(surfaceView: surfaceView, hostKind: .terminal) let foreignHost = NSView()
#expect(terminalHost.isSurfaceAttachedToDocumentView)
foreignHost.addSubview(surfaceView) #expect(!terminalHost.isSurfaceAttachedToDocumentView)
terminalHost.ensureSurfaceAttached(requiresLiveHost: false)
#expect(!terminalHost.isSurfaceAttachedToDocumentView) #expect(surfaceView.superview === foreignHost)
surfaceView.removeFromSuperview() #expect(surfaceView.superview == nil)
terminalHost.ensureSurfaceAttached(requiresLiveHost: false)
#expect(terminalHost.isSurfaceAttachedToDocumentView) #expect(surfaceView.scrollWrapper === terminalHost) }
@Test func terminalHostDoesNotStealSurfaceFromCanvasHost() { let runtime = GhosttyRuntime() let surfaceView = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_TAB, skipsSurfaceCreationForTesting: true ) let terminalHost = GhosttySurfaceScrollView(surfaceView: surfaceView, hostKind: .terminal) let canvasHost = GhosttySurfaceScrollView(surfaceView: surfaceView, hostKind: .canvas)
#expect(!terminalHost.isSurfaceAttachedToDocumentView) #expect(canvasHost.isSurfaceAttachedToDocumentView) #expect(surfaceView.scrollWrapper === canvasHost)
terminalHost.ensureSurfaceAttached(requiresLiveHost: false)
#expect(!terminalHost.isSurfaceAttachedToDocumentView) #expect(canvasHost.isSurfaceAttachedToDocumentView) #expect(surfaceView.scrollWrapper === canvasHost) }
@Test func canvasHostDoesNotStealDetachedSurfaceBack() { let runtime = GhosttyRuntime() let surfaceView = GhosttySurfaceView( runtime: runtime, workingDirectory: nil, context: GHOSTTY_SURFACE_CONTEXT_TAB, skipsSurfaceCreationForTesting: true ) let canvasHost = GhosttySurfaceScrollView(surfaceView: surfaceView, hostKind: .canvas) let foreignHost = NSView()
#expect(canvasHost.isSurfaceAttachedToDocumentView)
foreignHost.addSubview(surfaceView) #expect(!canvasHost.isSurfaceAttachedToDocumentView)
canvasHost.ensureSurfaceAttached(requiresLiveHost: false)
#expect(!canvasHost.isSurfaceAttachedToDocumentView) #expect(surfaceView.superview === foreignHost) }
private func drainMainQueue() async { await withCheckedContinuation { continuation in DispatchQueue.main.async { continuation.resume() } } }
@Test func normalizedWorkingDirectoryPathRemovesTrailingSlashForNonRootPath() { #expect( GhosttySurfaceView.normalizedWorkingDirectoryPath("/Users/onevcat/Sync/github/Prowl/") == "/Users/onevcat/Sync/github/Prowl" ) #expect( GhosttySurfaceView.normalizedWorkingDirectoryPath("/Users/onevcat/Sync/github/Prowl///") == "/Users/onevcat/Sync/github/Prowl" ) }
@Test func normalizedWorkingDirectoryPathKeepsRootPath() { #expect(GhosttySurfaceView.normalizedWorkingDirectoryPath("/") == "/") }
@Test func accessibilityLineCountsLineBreaksUpToIndex() { let content = "alpha\nbeta\ngamma"
#expect(GhosttySurfaceView.accessibilityLine(for: 0, in: content) == 0) #expect(GhosttySurfaceView.accessibilityLine(for: 5, in: content) == 0) #expect(GhosttySurfaceView.accessibilityLine(for: 6, in: content) == 1) #expect(GhosttySurfaceView.accessibilityLine(for: content.count, in: content) == 2) }
@Test func accessibilityStringReturnsSubstringForValidRange() { let content = "alpha\nbeta"
#expect( GhosttySurfaceView.accessibilityString( for: NSRange(location: 6, length: 4), in: content ) == "beta" ) #expect( GhosttySurfaceView.accessibilityString( for: NSRange(location: 99, length: 1), in: content ) == nil ) }
@Test func stringFromGhosttyTextUsesExplicitLength() { let bytes: [UInt8] = Array("alpha".utf8) + [0] + Array("omega".utf8)
let decoded = bytes.withUnsafeBufferPointer { buffer in let pointer = UnsafeRawPointer(buffer.baseAddress!).assumingMemoryBound(to: CChar.self) return GhosttySurfaceView.stringFromGhosttyText(pointer: pointer, length: UInt(bytes.count)) }
#expect(Array(decoded.utf8) == bytes) }
@Test func keyboardLayoutChangeKeyUpSuppressionSuppressesMatchingKeyUp() { let suppression = GhosttySurfaceView.KeyboardLayoutChangeKeyUpSuppression( keyCode: 49, timestamp: 10 )
#expect(suppression.suppresses(keyCode: 49, timestamp: 10.1)) #expect(!suppression.isExpired(at: 10.1)) }
@Test func keyboardLayoutChangeKeyUpSuppressionIgnoresDifferentKeyUp() { let suppression = GhosttySurfaceView.KeyboardLayoutChangeKeyUpSuppression( keyCode: 49, timestamp: 10 )
#expect(!suppression.suppresses(keyCode: 50, timestamp: 10.1)) #expect(suppression.suppresses(keyCode: 49, timestamp: 10.2)) #expect(!suppression.isExpired(at: 10.1)) }
@Test func keyboardLayoutChangeKeyUpSuppressionExpires() { let suppression = GhosttySurfaceView.KeyboardLayoutChangeKeyUpSuppression( keyCode: 49, timestamp: 10 )
#expect(!suppression.suppresses(keyCode: 49, timestamp: 11.1)) #expect(suppression.isExpired(at: 11.1)) }
private func makeKeyEvent( characters: String, charactersIgnoringModifiers: String, modifiers: NSEvent.ModifierFlags, keyCode: UInt16 ) throws -> NSEvent { try #require( NSEvent.keyEvent( with: .keyDown, location: .zero, modifierFlags: modifiers, timestamp: 1, windowNumber: 0, context: nil, characters: characters, charactersIgnoringModifiers: charactersIgnoringModifiers, isARepeat: false, keyCode: keyCode ) ) }}