Merge 154fe8e66c into 629838b9bd
commit
9280630f1e
|
|
@ -149,9 +149,7 @@
|
|||
Features/AppleScript/ScriptTab.swift,
|
||||
Features/AppleScript/ScriptTerminal.swift,
|
||||
Features/AppleScript/ScriptWindow.swift,
|
||||
Features/ClipboardConfirmation/ClipboardConfirmation.xib,
|
||||
Features/ClipboardConfirmation/ClipboardConfirmationController.swift,
|
||||
Features/ClipboardConfirmation/ClipboardConfirmationView.swift,
|
||||
"Features/Command Palette/CommandPalette.swift",
|
||||
"Features/Command Palette/TerminalCommandPalette.swift",
|
||||
"Features/Custom App Icon/AppIcon.swift",
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22155" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22155"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="PasteProtectionController" customModule="Ghostty" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="window" destination="F0z-JX-Cv5" id="V20-69-5rG"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Clipboard Confirmation" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="F0z-JX-Cv5" userLabel="Clipboard Confirmation">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1512" height="944"/>
|
||||
<view key="contentView" wantsLayer="YES" id="se5-gp-TjO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
<point key="canvasLocation" x="-116" y="100"/>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
|
|
@ -1,49 +1,119 @@
|
|||
import Foundation
|
||||
import Cocoa
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
/// This initializes a clipboard confirmation warning window. The window itself
|
||||
/// This initializes a clipboard confirmation warning alert. The window itself
|
||||
/// WILL NOT show automatically and the caller must show the window via
|
||||
/// showWindow, beginSheet, etc.
|
||||
class ClipboardConfirmationController: NSWindowController {
|
||||
override var windowNibName: NSNib.Name? { "ClipboardConfirmation" }
|
||||
|
||||
class ClipboardConfirmationAlert: NSAlert, NSAlertDelegate {
|
||||
let surface: ghostty_surface_t
|
||||
let contents: String
|
||||
let request: Ghostty.ClipboardRequest
|
||||
let state: UnsafeMutableRawPointer?
|
||||
weak private var delegate: ClipboardConfirmationViewDelegate?
|
||||
|
||||
init(surface: ghostty_surface_t, contents: String, request: Ghostty.ClipboardRequest, state: UnsafeMutableRawPointer?, delegate: ClipboardConfirmationViewDelegate) {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(surface: ghostty_surface_t, contents: String, request: Ghostty.ClipboardRequest, state: UnsafeMutableRawPointer?) {
|
||||
self.surface = surface
|
||||
self.contents = contents
|
||||
self.request = request
|
||||
self.state = state
|
||||
self.delegate = delegate
|
||||
super.init(window: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported for this view")
|
||||
}
|
||||
|
||||
// MARK: - NSWindowController
|
||||
|
||||
override func windowDidLoad() {
|
||||
guard let window = window else { return }
|
||||
super.init()
|
||||
|
||||
showsHelp = true
|
||||
switch request {
|
||||
case .paste:
|
||||
window.title = "Warning: Potentially Unsafe Paste"
|
||||
messageText = "Potentially Unsafe Paste"
|
||||
alertStyle = .critical
|
||||
helpAnchor = "clipboard-paste-protection"
|
||||
case .osc_52_read, .osc_52_write:
|
||||
window.title = "Authorize Clipboard Access"
|
||||
messageText = "Authorize Clipboard Access"
|
||||
alertStyle = .warning
|
||||
helpAnchor = "clipboard-write"
|
||||
}
|
||||
|
||||
window.contentView = NSHostingView(rootView: ClipboardConfirmationView(
|
||||
contents: contents,
|
||||
request: request,
|
||||
delegate: delegate
|
||||
))
|
||||
informativeText = request.text()
|
||||
let accessoryView = NSTextView.scrollableTextView()
|
||||
// Maximum frame when calculating the content size
|
||||
accessoryView.frame = .init(x: 0, y: 0, width: 400, height: 270)
|
||||
if let textView = accessoryView.documentView as? NSTextView {
|
||||
textView.drawsBackground = false
|
||||
textView.isEditable = false
|
||||
textView.font = .monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .medium)
|
||||
textView.textContainerInset = .zero
|
||||
|
||||
textView.string = contents
|
||||
}
|
||||
|
||||
self.accessoryView = accessoryView
|
||||
|
||||
addCancelButton(Action.text(.cancel, request))
|
||||
addConfirmButton(Action.text(.confirm, request))
|
||||
layout()
|
||||
updateContentHeight()
|
||||
|
||||
delegate = self
|
||||
}
|
||||
|
||||
private func updateContentHeight() {
|
||||
guard
|
||||
let accessoryView,
|
||||
let textView = (accessoryView as? NSScrollView)?.documentView as? NSTextView,
|
||||
let layoutManager = textView.layoutManager,
|
||||
let textContainer = textView.textContainer
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
textContainer.containerSize = CGSize(
|
||||
width: accessoryView.frame.width,
|
||||
height: .greatestFiniteMagnitude,
|
||||
)
|
||||
textContainer.widthTracksTextView = false
|
||||
|
||||
layoutManager.ensureLayout(for: textContainer)
|
||||
|
||||
let usedRect = layoutManager.usedRect(for: textContainer)
|
||||
accessoryView.frame.size.height = .minimum(
|
||||
accessoryView.frame.height,
|
||||
.maximum(10, usedRect.height),
|
||||
)
|
||||
}
|
||||
|
||||
func addCancelButton(_ buttonTitle: String) {
|
||||
addButton(withTitle: buttonTitle)
|
||||
.keyEquivalent = .init([KeyboardShortcut(.escape).key.character])
|
||||
}
|
||||
|
||||
func addConfirmButton(_ buttonTitle: String) {
|
||||
addButton(withTitle: buttonTitle)
|
||||
.keyEquivalent = .init([KeyboardShortcut(.return).key.character])
|
||||
}
|
||||
|
||||
func alertShowHelp(_ alert: NSAlert) -> Bool {
|
||||
var components = URLComponents(string: "https://ghostty.org/docs/config/reference")
|
||||
components?.fragment = alert.helpAnchor
|
||||
guard let url = components?.url else {
|
||||
return false
|
||||
}
|
||||
NSWorkspace.shared.open(url)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
import SwiftUI
|
||||
|
||||
/// This delegate is notified of the completion result of the clipboard confirmation dialog.
|
||||
protocol ClipboardConfirmationViewDelegate: AnyObject {
|
||||
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest)
|
||||
}
|
||||
|
||||
/// 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, request)
|
||||
}
|
||||
|
||||
private func onPaste() {
|
||||
delegate?.clipboardConfirmationComplete(.confirm, request)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,6 @@ class BaseTerminalController: NSWindowController,
|
|||
NSWindowDelegate,
|
||||
TerminalViewDelegate,
|
||||
TerminalViewModel,
|
||||
ClipboardConfirmationViewDelegate,
|
||||
FullscreenDelegate {
|
||||
/// The app instance that this terminal view will represent.
|
||||
let ghostty: Ghostty.App
|
||||
|
|
@ -63,7 +62,7 @@ class BaseTerminalController: NSWindowController,
|
|||
private var alert: NSAlert?
|
||||
|
||||
/// The clipboard confirmation window, if shown.
|
||||
private var clipboardConfirmation: ClipboardConfirmationController?
|
||||
private var clipboardConfirmation: ClipboardConfirmationAlert?
|
||||
|
||||
/// Fullscreen state management.
|
||||
private(set) var fullscreenStyle: FullscreenStyle?
|
||||
|
|
@ -1117,25 +1116,32 @@ class BaseTerminalController: NSWindowController,
|
|||
}
|
||||
|
||||
// Show our paste confirmation
|
||||
self.clipboardConfirmation = ClipboardConfirmationController(
|
||||
self.clipboardConfirmation = ClipboardConfirmationAlert(
|
||||
surface: surface,
|
||||
contents: str,
|
||||
request: request,
|
||||
state: state,
|
||||
delegate: self
|
||||
)
|
||||
window.beginSheet(self.clipboardConfirmation!.window!)
|
||||
|
||||
clipboardConfirmation?.beginSheetModal(for: window) { [weak self] response in
|
||||
switch response {
|
||||
case .alertFirstButtonReturn:
|
||||
self?.clipboardConfirmationComplete(.cancel, request)
|
||||
case .alertSecondButtonReturn:
|
||||
self?.clipboardConfirmationComplete(.confirm, request)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) {
|
||||
func clipboardConfirmationComplete(_ action: ClipboardConfirmationAlert.Action, _ request: Ghostty.ClipboardRequest) {
|
||||
// End our clipboard confirmation no matter what
|
||||
guard let cc = self.clipboardConfirmation else { return }
|
||||
self.clipboardConfirmation = nil
|
||||
|
||||
// Close the sheet
|
||||
if let ccWindow = cc.window {
|
||||
window?.endSheet(ccWindow)
|
||||
}
|
||||
cc.window.orderOut(nil)
|
||||
|
||||
switch request {
|
||||
case let .osc_52_write(pasteboard):
|
||||
|
|
|
|||
Loading…
Reference in New Issue