Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/components/terminal.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ Scrollback, wide/CJK character rendering, and copy/paste are handled by Ghostty
with its defaults — Prowl doesn't override them. Customize terminal behavior in
your Ghostty config at `~/.config/ghostty/config`.

When the clipboard contains only an image, Prowl leaves performable paste
shortcuts to the terminal program so agents that support clipboard image import
can handle the image directly.

## Color scheme

The terminal theme automatically follows the macOS light/dark appearance. There's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,9 @@ extension GhosttySurfaceView {
if bridge.state.keyTableDepth > 0 { return false }
let raw = flags.rawValue
let isAll = (raw & GHOSTTY_BINDING_FLAGS_ALL.rawValue) != 0
let isPerformable = (raw & GHOSTTY_BINDING_FLAGS_PERFORMABLE.rawValue) != 0
let isConsumed = (raw & GHOSTTY_BINDING_FLAGS_CONSUMED.rawValue) != 0
return !isAll && isConsumed
return !isAll && !isPerformable && isConsumed
}

@IBAction func copy(_ sender: Any?) {
Expand Down
50 changes: 50 additions & 0 deletions supacodeTests/GhosttySurfaceViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,47 @@ struct GhosttySurfaceViewTests {
#expect(!GhosttySurfaceView.hasKeyEquivalentFocusOwnership(cachedFocused: false, isActualFirstResponder: true))
}

@Test func imageClipboardLeavesPerformablePasteForPTY() {
let pasteboard = NSPasteboard(name: .init("prowl-tests.image-clipboard.\(UUID().uuidString)"))
pasteboard.clearContents()
defer { pasteboard.clearContents() }

#expect(pasteboard.setData(Data([0x89, 0x50, 0x4E, 0x47]), forType: .png))
#expect(pasteboard.getOpinionatedStringContents() == nil)

let surfaceView = makeSurfaceView()
let flags = ghostty_binding_flags_e(
GHOSTTY_BINDING_FLAGS_CONSUMED.rawValue | GHOSTTY_BINDING_FLAGS_PERFORMABLE.rawValue
)

#expect(!surfaceView.shouldAttemptMenu(for: flags))
}

@Test func menuAttemptRequiresConsumedNonAllBinding() {
let surfaceView = makeSurfaceView()
let consumedAndAll = ghostty_binding_flags_e(
GHOSTTY_BINDING_FLAGS_CONSUMED.rawValue | GHOSTTY_BINDING_FLAGS_ALL.rawValue
)

#expect(surfaceView.shouldAttemptMenu(for: GHOSTTY_BINDING_FLAGS_CONSUMED))
#expect(!surfaceView.shouldAttemptMenu(for: ghostty_binding_flags_e(0)))
#expect(!surfaceView.shouldAttemptMenu(for: consumedAndAll))
}

@Test func activeBindingScopesBypassMenuUntilTheyExit() {
let surfaceView = makeSurfaceView()

surfaceView.bridge.state.keySequenceActive = true
#expect(!surfaceView.shouldAttemptMenu(for: GHOSTTY_BINDING_FLAGS_CONSUMED))

surfaceView.bridge.state.keySequenceActive = false
surfaceView.bridge.state.keyTableDepth = 1
#expect(!surfaceView.shouldAttemptMenu(for: GHOSTTY_BINDING_FLAGS_CONSUMED))

surfaceView.bridge.state.keyTableDepth = 0
#expect(surfaceView.shouldAttemptMenu(for: GHOSTTY_BINDING_FLAGS_CONSUMED))
}

@Test func occlusionStateResendsDesiredValueAfterAttachmentChange() {
var state = GhosttySurfaceView.OcclusionState()

Expand Down Expand Up @@ -494,4 +535,13 @@ struct GhosttySurfaceViewTests {
)
)
}

private func makeSurfaceView() -> GhosttySurfaceView {
GhosttySurfaceView(
runtime: GhosttyRuntime(),
workingDirectory: nil,
context: GHOSTTY_SURFACE_CONTEXT_TAB,
skipsSurfaceCreationForTesting: true
)
}
}
Loading