mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-07 14:37:31 +00:00
Fixes #10077 Clipboard read confirmations would immediately show a sheet which grabbed focus. This could be used for a bunch of dumb reasons, including DoS attacks. But, it also caused focus/sheet loops for programs that did OSC52 on focus changes (which was seen via some Neovim configs!). Now, if a surface is unfocused, we bell the surface and show the confirmation request on next focus. If the surface is not focused or another request comes in, we cancel the prior one. This also fixes some memory management issues around clipboard requests that were likely small leaks (didn't verify the old bug, but verified the new code, and eyeballed the old).
97 lines
3.2 KiB
Swift
97 lines
3.2 KiB
Swift
import SwiftUI
|
|
|
|
/// This delegate is notified of the completion result of the clipboard confirmation dialog.
|
|
protocol ClipboardConfirmationViewDelegate: AnyObject {
|
|
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action)
|
|
}
|
|
|
|
/// The SwiftUI view for showing a clipboard confirmation dialog.
|
|
struct ClipboardConfirmationView: View {
|
|
enum Action: String {
|
|
case cancel
|
|
case confirm
|
|
|
|
static func text(_ action: Action, _ reason: Ghostty.ClipboardRequest) -> String {
|
|
switch (action, reason) {
|
|
case (.cancel, .paste):
|
|
return "Cancel"
|
|
case (.cancel, .osc_52_read), (.cancel, .osc_52_write):
|
|
return "Deny"
|
|
case (.confirm, .paste):
|
|
return "Paste"
|
|
case (.confirm, .osc_52_read), (.confirm, .osc_52_write):
|
|
return "Allow"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The contents of the paste.
|
|
let contents: String
|
|
|
|
/// The type of the clipboard request
|
|
let request: Ghostty.ClipboardRequest
|
|
|
|
/// Optional delegate to get results. If this is nil, then this view will never close on its own.
|
|
weak var delegate: ClipboardConfirmationViewDelegate?
|
|
|
|
/// Used to track if we should rehide on disappear
|
|
@State private var cursorHiddenCount: UInt = 0
|
|
|
|
var body: some View {
|
|
VStack {
|
|
HStack {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.foregroundColor(.yellow)
|
|
.font(.system(size: 42))
|
|
.padding()
|
|
.frame(alignment: .center)
|
|
|
|
Text(request.text())
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding()
|
|
}
|
|
|
|
TextEditor(text: .constant(contents))
|
|
.focusable(false)
|
|
.font(.system(.body, design: .monospaced))
|
|
|
|
HStack {
|
|
Spacer()
|
|
Button(Action.text(.cancel, request)) { onCancel() }
|
|
.keyboardShortcut(.cancelAction)
|
|
Button(Action.text(.confirm, request)) { onPaste() }
|
|
.keyboardShortcut(.defaultAction)
|
|
Spacer()
|
|
}
|
|
.padding(.bottom)
|
|
}
|
|
.onAppear {
|
|
// I can't find a better way to handle this. There is no API to detect
|
|
// if the cursor is hidden and OTHER THINGS do unhide the cursor. So we
|
|
// try to unhide it completely here and hope for the best. Issue #1516.
|
|
cursorHiddenCount = Cursor.unhideCompletely()
|
|
|
|
// If we didn't unhide anything, we just send an unhide to be safe.
|
|
// I don't think the count can go negative on NSCursor so this handles
|
|
// scenarios cursor is hidden outside of our own NSCursor usage.
|
|
if cursorHiddenCount == 0 {
|
|
_ = Cursor.unhide()
|
|
}
|
|
}
|
|
.onDisappear {
|
|
// Rehide if we unhid
|
|
for _ in 0..<cursorHiddenCount {
|
|
Cursor.hide()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func onCancel() {
|
|
delegate?.clipboardConfirmationComplete(.cancel)
|
|
}
|
|
|
|
private func onPaste() {
|
|
delegate?.clipboardConfirmationComplete(.confirm)
|
|
}
|
|
}
|