mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: add option to prompt user for confirmation on OSC 52 commands
This commit is contained in:
@ -35,6 +35,12 @@ typedef enum {
|
|||||||
GHOSTTY_CLIPBOARD_SELECTION,
|
GHOSTTY_CLIPBOARD_SELECTION,
|
||||||
} ghostty_clipboard_e;
|
} ghostty_clipboard_e;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GHOSTTY_CLIPBOARD_PROMPT_UNSAFE = 1,
|
||||||
|
GHOSTTY_CLIPBOARD_PROMPT_READ = 2,
|
||||||
|
GHOSTTY_CLIPBOARD_PROMPT_WRITE = 3,
|
||||||
|
} ghostty_clipboard_prompt_reason_e;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GHOSTTY_SPLIT_RIGHT,
|
GHOSTTY_SPLIT_RIGHT,
|
||||||
GHOSTTY_SPLIT_DOWN
|
GHOSTTY_SPLIT_DOWN
|
||||||
@ -340,8 +346,8 @@ typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
|
|||||||
typedef void (*ghostty_runtime_set_mouse_shape_cb)(void *, ghostty_mouse_shape_e);
|
typedef void (*ghostty_runtime_set_mouse_shape_cb)(void *, ghostty_mouse_shape_e);
|
||||||
typedef void (*ghostty_runtime_set_mouse_visibility_cb)(void *, bool);
|
typedef void (*ghostty_runtime_set_mouse_visibility_cb)(void *, bool);
|
||||||
typedef void (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e, void *);
|
typedef void (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e, void *);
|
||||||
typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(void *, const char*, void *);
|
typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(void *, const char*, void *, ghostty_clipboard_prompt_reason_e);
|
||||||
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e);
|
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e, bool);
|
||||||
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e, ghostty_surface_config_s);
|
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e, ghostty_surface_config_s);
|
||||||
typedef void (*ghostty_runtime_new_tab_cb)(void *, ghostty_surface_config_s);
|
typedef void (*ghostty_runtime_new_tab_cb)(void *, ghostty_surface_config_s);
|
||||||
typedef void (*ghostty_runtime_new_window_cb)(void *, ghostty_surface_config_s);
|
typedef void (*ghostty_runtime_new_window_cb)(void *, ghostty_surface_config_s);
|
||||||
|
@ -40,9 +40,9 @@
|
|||||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; };
|
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; };
|
||||||
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; };
|
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; };
|
||||||
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||||
A5E112932AF73E6E00C6E0C2 /* PasteProtection.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* PasteProtection.xib */; };
|
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
|
||||||
A5E112952AF73E8A00C6E0C2 /* PasteProtectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* PasteProtectionController.swift */; };
|
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
|
||||||
A5E112972AF7401B00C6E0C2 /* PasteProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* PasteProtectionView.swift */; };
|
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
|
||||||
A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; };
|
A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@ -83,9 +83,9 @@
|
|||||||
A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; };
|
A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; };
|
||||||
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||||
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
|
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
|
||||||
A5E112922AF73E6E00C6E0C2 /* PasteProtection.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PasteProtection.xib; sourceTree = "<group>"; };
|
A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = "<group>"; };
|
||||||
A5E112942AF73E8A00C6E0C2 /* PasteProtectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteProtectionController.swift; sourceTree = "<group>"; };
|
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = "<group>"; };
|
||||||
A5E112962AF7401B00C6E0C2 /* PasteProtectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteProtectionView.swift; sourceTree = "<group>"; };
|
A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = "<group>"; };
|
||||||
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@ -107,7 +107,7 @@
|
|||||||
children = (
|
children = (
|
||||||
A56D58872ACDE6BE00508D2C /* Services */,
|
A56D58872ACDE6BE00508D2C /* Services */,
|
||||||
A59630982AEE1C4400D64628 /* Terminal */,
|
A59630982AEE1C4400D64628 /* Terminal */,
|
||||||
A5E112912AF73E4D00C6E0C2 /* Paste Protection */,
|
A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */,
|
||||||
A534263E2A7DCC5800EBB7A2 /* Settings */,
|
A534263E2A7DCC5800EBB7A2 /* Settings */,
|
||||||
);
|
);
|
||||||
path = Features;
|
path = Features;
|
||||||
@ -234,14 +234,14 @@
|
|||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
A5E112912AF73E4D00C6E0C2 /* Paste Protection */ = {
|
A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A5E112922AF73E6E00C6E0C2 /* PasteProtection.xib */,
|
A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */,
|
||||||
A5E112942AF73E8A00C6E0C2 /* PasteProtectionController.swift */,
|
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */,
|
||||||
A5E112962AF7401B00C6E0C2 /* PasteProtectionView.swift */,
|
A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */,
|
||||||
);
|
);
|
||||||
path = "Paste Protection";
|
path = ClipboardConfirmation;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
@ -306,7 +306,7 @@
|
|||||||
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */,
|
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */,
|
||||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
||||||
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */,
|
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */,
|
||||||
A5E112932AF73E6E00C6E0C2 /* PasteProtection.xib in Resources */,
|
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */,
|
||||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */,
|
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */,
|
||||||
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */,
|
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */,
|
||||||
);
|
);
|
||||||
@ -342,11 +342,11 @@
|
|||||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
|
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
|
||||||
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */,
|
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */,
|
||||||
A5E112952AF73E8A00C6E0C2 /* PasteProtectionController.swift in Sources */,
|
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */,
|
||||||
8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */,
|
8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */,
|
||||||
A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */,
|
A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */,
|
||||||
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */,
|
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */,
|
||||||
A5E112972AF7401B00C6E0C2 /* PasteProtectionView.swift in Sources */,
|
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
</customObject>
|
</customObject>
|
||||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||||
<window title="Warning: Potentially Unsafe Paste" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="F0z-JX-Cv5">
|
<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"/>
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="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="contentRect" x="196" y="240" width="480" height="270"/>
|
||||||
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1667"/>
|
<rect key="screenRect" x="0.0" y="0.0" width="1512" height="944"/>
|
||||||
<view key="contentView" wantsLayer="YES" id="se5-gp-TjO">
|
<view key="contentView" wantsLayer="YES" id="se5-gp-TjO">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
@ -0,0 +1,49 @@
|
|||||||
|
import Foundation
|
||||||
|
import Cocoa
|
||||||
|
import SwiftUI
|
||||||
|
import GhosttyKit
|
||||||
|
|
||||||
|
/// This initializes a clipboard confirmation warning window. 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" }
|
||||||
|
|
||||||
|
let surface: ghostty_surface_t
|
||||||
|
let contents: String
|
||||||
|
let reason: Ghostty.ClipboardPromptReason
|
||||||
|
let state: UnsafeMutableRawPointer?
|
||||||
|
weak private var delegate: ClipboardConfirmationViewDelegate? = nil
|
||||||
|
|
||||||
|
init(surface: ghostty_surface_t, contents: String, reason: Ghostty.ClipboardPromptReason, state: UnsafeMutableRawPointer?, delegate: ClipboardConfirmationViewDelegate) {
|
||||||
|
self.surface = surface
|
||||||
|
self.contents = contents
|
||||||
|
self.reason = reason
|
||||||
|
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 }
|
||||||
|
|
||||||
|
switch (reason) {
|
||||||
|
case .unsafe:
|
||||||
|
window.title = "Warning: Potentially Unsafe Paste"
|
||||||
|
case .read, .write:
|
||||||
|
window.title = "Authorize Clipboard Access"
|
||||||
|
}
|
||||||
|
|
||||||
|
window.contentView = NSHostingView(rootView: ClipboardConfirmationView(
|
||||||
|
contents: contents,
|
||||||
|
reason: reason,
|
||||||
|
delegate: delegate
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// This delegate is notified of the completion result of the clipboard confirmation dialog.
|
||||||
|
protocol ClipboardConfirmationViewDelegate: AnyObject {
|
||||||
|
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ reason: Ghostty.ClipboardPromptReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.ClipboardPromptReason) -> String {
|
||||||
|
switch (action) {
|
||||||
|
case .cancel:
|
||||||
|
switch (reason) {
|
||||||
|
case .unsafe: return "Cancel"
|
||||||
|
case .read, .write: return "Deny"
|
||||||
|
}
|
||||||
|
case .confirm:
|
||||||
|
switch (reason) {
|
||||||
|
case .unsafe: return "Paste"
|
||||||
|
case .read, .write: return "Allow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The contents of the paste.
|
||||||
|
let contents: String
|
||||||
|
|
||||||
|
/// The reason for displaying the view
|
||||||
|
let reason: Ghostty.ClipboardPromptReason
|
||||||
|
|
||||||
|
/// Optional delegate to get results. If this is nil, then this view will never close on its own.
|
||||||
|
weak var delegate: ClipboardConfirmationViewDelegate? = nil
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundColor(.yellow)
|
||||||
|
.font(.system(size: 42))
|
||||||
|
.padding()
|
||||||
|
.frame(alignment: .center)
|
||||||
|
|
||||||
|
Text(reason.text())
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEditor(text: .constant(contents))
|
||||||
|
.textSelection(.enabled)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.padding(.all, 4)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button(Action.text(.cancel, reason)) { onCancel() }
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
Button(Action.text(.confirm, reason)) { onPaste() }
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func onCancel() {
|
||||||
|
delegate?.clipboardConfirmationComplete(.cancel, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func onPaste() {
|
||||||
|
delegate?.clipboardConfirmationComplete(.confirm, reason)
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Cocoa
|
|
||||||
import SwiftUI
|
|
||||||
import GhosttyKit
|
|
||||||
|
|
||||||
/// This initializes an "unsafe paste" warning window. The window itself WILL NOT show automatically
|
|
||||||
/// and the caller must show the window via showWindow, beginSheet, etc.
|
|
||||||
class PasteProtectionController: NSWindowController {
|
|
||||||
override var windowNibName: NSNib.Name? { "PasteProtection" }
|
|
||||||
|
|
||||||
let surface: ghostty_surface_t
|
|
||||||
let contents: String
|
|
||||||
let state: UnsafeMutableRawPointer?
|
|
||||||
weak private var delegate: PasteProtectionViewDelegate? = nil
|
|
||||||
|
|
||||||
init(surface: ghostty_surface_t, contents: String, state: UnsafeMutableRawPointer?, delegate: PasteProtectionViewDelegate) {
|
|
||||||
self.surface = surface
|
|
||||||
self.contents = contents
|
|
||||||
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 }
|
|
||||||
window.contentView = NSHostingView(rootView: PasteProtectionView(
|
|
||||||
contents: contents,
|
|
||||||
delegate: delegate
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// This delegate is notified of the completion result of the paste protection dialog.
|
|
||||||
protocol PasteProtectionViewDelegate: AnyObject {
|
|
||||||
func pasteProtectionComplete(_ action: PasteProtectionView.Action)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The SwiftUI view for showing a paste protection dialog.
|
|
||||||
struct PasteProtectionView: View {
|
|
||||||
enum Action : String {
|
|
||||||
case cancel
|
|
||||||
case paste
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The contents of the paste.
|
|
||||||
let contents: String
|
|
||||||
|
|
||||||
/// Optional delegate to get results. If this is nil, then this view will never close on its own.
|
|
||||||
weak var delegate: PasteProtectionViewDelegate? = nil
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
|
||||||
.foregroundColor(.yellow)
|
|
||||||
.font(.system(size: 42))
|
|
||||||
.padding()
|
|
||||||
.frame(alignment: .center)
|
|
||||||
|
|
||||||
Text("Pasting this text to the terminal may be dangerous as it looks like " +
|
|
||||||
"some commands may be executed.")
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEditor(text: .constant(contents))
|
|
||||||
.textSelection(.enabled)
|
|
||||||
.font(.system(.body, design: .monospaced))
|
|
||||||
.padding(.all, 4)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Button("Cancel") { onCancel() }
|
|
||||||
.keyboardShortcut(.cancelAction)
|
|
||||||
Button("Paste") { onPaste() }
|
|
||||||
.keyboardShortcut(.defaultAction)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func onCancel() {
|
|
||||||
delegate?.pasteProtectionComplete(.cancel)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func onPaste() {
|
|
||||||
delegate?.pasteProtectionComplete(.paste)
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,7 +6,7 @@ import GhosttyKit
|
|||||||
/// The terminal controller is an NSWindowController that maps 1:1 to a terminal window.
|
/// The terminal controller is an NSWindowController that maps 1:1 to a terminal window.
|
||||||
class TerminalController: NSWindowController, NSWindowDelegate,
|
class TerminalController: NSWindowController, NSWindowDelegate,
|
||||||
TerminalViewDelegate, TerminalViewModel,
|
TerminalViewDelegate, TerminalViewModel,
|
||||||
PasteProtectionViewDelegate
|
ClipboardConfirmationViewDelegate
|
||||||
{
|
{
|
||||||
override var windowNibName: NSNib.Name? { "Terminal" }
|
override var windowNibName: NSNib.Name? { "Terminal" }
|
||||||
|
|
||||||
@ -31,8 +31,8 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
/// True when an alert is active so we don't overlap multiple.
|
/// True when an alert is active so we don't overlap multiple.
|
||||||
private var alert: NSAlert? = nil
|
private var alert: NSAlert? = nil
|
||||||
|
|
||||||
/// The paste protection window, if shown.
|
/// The clipboard confirmation window, if shown.
|
||||||
private var pasteProtection: PasteProtectionController? = nil
|
private var clipboardConfirmation: ClipboardConfirmationController? = nil
|
||||||
|
|
||||||
init(_ ghostty: Ghostty.AppState, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) {
|
init(_ ghostty: Ghostty.AppState, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) {
|
||||||
self.ghostty = ghostty
|
self.ghostty = ghostty
|
||||||
@ -56,8 +56,8 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
object: nil)
|
object: nil)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(onConfirmUnsafePaste),
|
selector: #selector(onConfirmClipboardRequest),
|
||||||
name: Ghostty.Notification.confirmUnsafePaste,
|
name: Ghostty.Notification.confirmClipboard,
|
||||||
object: nil)
|
object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,28 +346,36 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
self.window?.close()
|
self.window?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - Paste Protection
|
//MARK: - Clipboard Confirmation
|
||||||
|
|
||||||
func pasteProtectionComplete(_ action: PasteProtectionView.Action) {
|
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ reason: Ghostty.ClipboardPromptReason) {
|
||||||
// End our paste protection no matter what
|
// End our clipboard confirmation no matter what
|
||||||
guard let pp = self.pasteProtection else { return }
|
guard let cc = self.clipboardConfirmation else { return }
|
||||||
self.pasteProtection = nil
|
self.clipboardConfirmation = nil
|
||||||
|
|
||||||
// Close the sheet
|
// Close the sheet
|
||||||
if let ppWindow = pp.window {
|
if let ccWindow = cc.window {
|
||||||
window?.endSheet(ppWindow)
|
window?.endSheet(ccWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (reason) {
|
||||||
|
case .write:
|
||||||
|
guard case .confirm = action else { break }
|
||||||
|
let pb = NSPasteboard.general
|
||||||
|
pb.declareTypes([.string], owner: nil)
|
||||||
|
pb.setString(cc.contents, forType: .string)
|
||||||
|
case .read, .unsafe:
|
||||||
let str: String
|
let str: String
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
str = ""
|
str = ""
|
||||||
|
|
||||||
case .paste:
|
case .confirm:
|
||||||
str = pp.contents
|
str = cc.contents
|
||||||
}
|
}
|
||||||
|
|
||||||
Ghostty.AppState.completeClipboardRequest(pp.surface, data: str, state: pp.state, confirmed: true)
|
Ghostty.AppState.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - Notifications
|
//MARK: - Notifications
|
||||||
@ -429,7 +437,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func onConfirmUnsafePaste(notification: SwiftUI.Notification) {
|
@objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) {
|
||||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||||
guard target == self.focusedSurface else { return }
|
guard target == self.focusedSurface else { return }
|
||||||
guard let surface = target.surface else { return }
|
guard let surface = target.surface else { return }
|
||||||
@ -438,23 +446,25 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
guard let window = self.window else { return }
|
guard let window = self.window else { return }
|
||||||
|
|
||||||
// Check whether we use non-native fullscreen
|
// Check whether we use non-native fullscreen
|
||||||
guard let str = notification.userInfo?[Ghostty.Notification.UnsafePasteStrKey] as? String else { return }
|
guard let str = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStrKey] as? String else { return }
|
||||||
guard let state = notification.userInfo?[Ghostty.Notification.UnsafePasteStateKey] as? UnsafeMutableRawPointer? else { return }
|
guard let state = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStateKey] as? UnsafeMutableRawPointer? else { return }
|
||||||
|
guard let reason = notification.userInfo?[Ghostty.Notification.ConfirmClipboardReasonKey] as? Ghostty.ClipboardPromptReason else { return }
|
||||||
|
|
||||||
// If we already have a paste protection view up, we ignore this request.
|
// If we already have a clipboard confirmation view up, we ignore this request.
|
||||||
// This shouldn't be possible...
|
// This shouldn't be possible...
|
||||||
guard self.pasteProtection == nil else {
|
guard self.clipboardConfirmation == nil else {
|
||||||
Ghostty.AppState.completeClipboardRequest(surface, data: "", state: state, confirmed: true)
|
Ghostty.AppState.completeClipboardRequest(surface, data: "", state: state, confirmed: true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show our paste confirmation
|
// Show our paste confirmation
|
||||||
self.pasteProtection = PasteProtectionController(
|
self.clipboardConfirmation = ClipboardConfirmationController(
|
||||||
surface: surface,
|
surface: surface,
|
||||||
contents: str,
|
contents: str,
|
||||||
|
reason: reason,
|
||||||
state: state,
|
state: state,
|
||||||
delegate: self
|
delegate: self
|
||||||
)
|
)
|
||||||
window.beginSheet(self.pasteProtection!.window!)
|
window.beginSheet(self.clipboardConfirmation!.window!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,8 +150,8 @@ extension Ghostty {
|
|||||||
set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) },
|
set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) },
|
||||||
set_mouse_visibility_cb: { userdata, visible in AppState.setMouseVisibility(userdata, visible: visible) },
|
set_mouse_visibility_cb: { userdata, visible in AppState.setMouseVisibility(userdata, visible: visible) },
|
||||||
read_clipboard_cb: { userdata, loc, state in AppState.readClipboard(userdata, location: loc, state: state) },
|
read_clipboard_cb: { userdata, loc, state in AppState.readClipboard(userdata, location: loc, state: state) },
|
||||||
confirm_read_clipboard_cb: { userdata, str, state in AppState.confirmReadClipboard(userdata, string: str, state: state ) },
|
confirm_read_clipboard_cb: { userdata, str, state, reason in AppState.confirmReadClipboard(userdata, string: str, state: state, reason: reason ) },
|
||||||
write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) },
|
write_clipboard_cb: { userdata, str, loc, confirm in AppState.writeClipboard(userdata, string: str, location: loc, confirm: confirm) },
|
||||||
new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) },
|
new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) },
|
||||||
new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) },
|
new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) },
|
||||||
new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) },
|
new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) },
|
||||||
@ -433,16 +433,19 @@ extension Ghostty {
|
|||||||
static func confirmReadClipboard(
|
static func confirmReadClipboard(
|
||||||
_ userdata: UnsafeMutableRawPointer?,
|
_ userdata: UnsafeMutableRawPointer?,
|
||||||
string: UnsafePointer<CChar>?,
|
string: UnsafePointer<CChar>?,
|
||||||
state: UnsafeMutableRawPointer?
|
state: UnsafeMutableRawPointer?,
|
||||||
|
reason: ghostty_clipboard_prompt_reason_e
|
||||||
) {
|
) {
|
||||||
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||||
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
|
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
|
||||||
|
guard let reason = Ghostty.ClipboardPromptReason.from(reason: reason) else { return }
|
||||||
NotificationCenter.default.post(
|
NotificationCenter.default.post(
|
||||||
name: Notification.confirmUnsafePaste,
|
name: Notification.confirmClipboard,
|
||||||
object: surface,
|
object: surface,
|
||||||
userInfo: [
|
userInfo: [
|
||||||
Notification.UnsafePasteStrKey: valueStr,
|
Notification.ConfirmClipboardStrKey: valueStr,
|
||||||
Notification.UnsafePasteStateKey: state as Any
|
Notification.ConfirmClipboardStateKey: state as Any,
|
||||||
|
Notification.ConfirmClipboardReasonKey: reason,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -458,14 +461,27 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer<CChar>?, location: ghostty_clipboard_e) {
|
static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer<CChar>?, location: ghostty_clipboard_e, confirm: Bool) {
|
||||||
|
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||||
|
|
||||||
// We only support the standard clipboard
|
// We only support the standard clipboard
|
||||||
if (location != GHOSTTY_CLIPBOARD_STANDARD) { return }
|
if (location != GHOSTTY_CLIPBOARD_STANDARD) { return }
|
||||||
|
|
||||||
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
|
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
|
||||||
|
if !confirm {
|
||||||
let pb = NSPasteboard.general
|
let pb = NSPasteboard.general
|
||||||
pb.declareTypes([.string], owner: nil)
|
pb.declareTypes([.string], owner: nil)
|
||||||
pb.setString(valueStr, forType: .string)
|
pb.setString(valueStr, forType: .string)
|
||||||
|
} else {
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: Notification.confirmClipboard,
|
||||||
|
object: surface,
|
||||||
|
userInfo: [
|
||||||
|
Notification.ConfirmClipboardStrKey: valueStr,
|
||||||
|
Notification.ConfirmClipboardReasonKey: Ghostty.ClipboardPromptReason.write,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {
|
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {
|
||||||
|
@ -94,6 +94,61 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The reason a clipboard prompt is shown to the user
|
||||||
|
enum ClipboardPromptReason {
|
||||||
|
/// An unsafe paste may cause commands to be executed
|
||||||
|
case unsafe
|
||||||
|
|
||||||
|
/// An application is attempting to read from the clipboard
|
||||||
|
case read
|
||||||
|
|
||||||
|
/// An applciation is attempting to write to the clipboard
|
||||||
|
case write
|
||||||
|
|
||||||
|
func text() -> String {
|
||||||
|
switch (self) {
|
||||||
|
case .unsafe:
|
||||||
|
return """
|
||||||
|
Pasting this text to the terminal may be dangerous as it looks like some commands may be executed.
|
||||||
|
"""
|
||||||
|
case .read:
|
||||||
|
return """
|
||||||
|
An application is attempting to read from the clipboard.
|
||||||
|
The current clipboard contents are shown below.
|
||||||
|
"""
|
||||||
|
case .write:
|
||||||
|
return """
|
||||||
|
An application is attempting to write to the clipboard.
|
||||||
|
The content to write is shown below.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func from(reason: ghostty_clipboard_prompt_reason_e) -> ClipboardPromptReason? {
|
||||||
|
switch (reason) {
|
||||||
|
case GHOSTTY_CLIPBOARD_PROMPT_UNSAFE:
|
||||||
|
return .unsafe
|
||||||
|
case GHOSTTY_CLIPBOARD_PROMPT_READ:
|
||||||
|
return .read
|
||||||
|
case GHOSTTY_CLIPBOARD_PROMPT_WRITE:
|
||||||
|
return .write
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toNative() -> ghostty_clipboard_prompt_reason_e {
|
||||||
|
switch (self) {
|
||||||
|
case .unsafe:
|
||||||
|
return GHOSTTY_CLIPBOARD_PROMPT_UNSAFE
|
||||||
|
case .read:
|
||||||
|
return GHOSTTY_CLIPBOARD_PROMPT_READ
|
||||||
|
case .write:
|
||||||
|
return GHOSTTY_CLIPBOARD_PROMPT_WRITE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Ghostty.Notification {
|
extension Ghostty.Notification {
|
||||||
@ -142,9 +197,10 @@ extension Ghostty.Notification {
|
|||||||
/// Notification to show/hide the inspector
|
/// Notification to show/hide the inspector
|
||||||
static let didControlInspector = Notification.Name("com.mitchellh.ghostty.didControlInspector")
|
static let didControlInspector = Notification.Name("com.mitchellh.ghostty.didControlInspector")
|
||||||
|
|
||||||
static let confirmUnsafePaste = Notification.Name("com.mitchellh.ghostty.confirmUnsafePaste")
|
static let confirmClipboard = Notification.Name("com.mitchellh.ghostty.confirmClipboard")
|
||||||
static let UnsafePasteStrKey = confirmUnsafePaste.rawValue + ".str"
|
static let ConfirmClipboardStrKey = confirmClipboard.rawValue + ".str"
|
||||||
static let UnsafePasteStateKey = confirmUnsafePaste.rawValue + ".state"
|
static let ConfirmClipboardStateKey = confirmClipboard.rawValue + ".state"
|
||||||
|
static let ConfirmClipboardReasonKey = confirmClipboard.rawValue + ".reason"
|
||||||
|
|
||||||
/// Notification sent to the active split view to resize the split.
|
/// Notification sent to the active split view to resize the split.
|
||||||
static let didResizeSplit = Notification.Name("com.mitchellh.ghostty.didResizeSplit")
|
static let didResizeSplit = Notification.Name("com.mitchellh.ghostty.didResizeSplit")
|
||||||
|
@ -140,8 +140,8 @@ const DerivedConfig = struct {
|
|||||||
/// For docs for these, see the associated config they are derived from.
|
/// For docs for these, see the associated config they are derived from.
|
||||||
original_font_size: u8,
|
original_font_size: u8,
|
||||||
keybind: configpkg.Keybinds,
|
keybind: configpkg.Keybinds,
|
||||||
clipboard_read: bool,
|
clipboard_read: configpkg.Config.ClipboardRequest,
|
||||||
clipboard_write: bool,
|
clipboard_write: configpkg.Config.ClipboardRequest,
|
||||||
clipboard_trim_trailing_spaces: bool,
|
clipboard_trim_trailing_spaces: bool,
|
||||||
clipboard_paste_protection: bool,
|
clipboard_paste_protection: bool,
|
||||||
clipboard_paste_bracketed_safe: bool,
|
clipboard_paste_bracketed_safe: bool,
|
||||||
@ -689,8 +689,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
|||||||
.cell_size => |size| try self.setCellSize(size),
|
.cell_size => |size| try self.setCellSize(size),
|
||||||
|
|
||||||
.clipboard_read => |kind| {
|
.clipboard_read => |kind| {
|
||||||
if (!self.config.clipboard_read) {
|
if (self.config.clipboard_read == .deny) {
|
||||||
log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{});
|
log.info("application attempted to read clipboard, but 'clipboard-read' is set to deny", .{});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -822,8 +822,8 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) !void {
|
fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) !void {
|
||||||
if (!self.config.clipboard_write) {
|
if (self.config.clipboard_write == .deny) {
|
||||||
log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{});
|
log.info("application attempted to write clipboard, but 'clipboard-write' is set to deny", .{});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -856,7 +856,8 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard)
|
|||||||
};
|
};
|
||||||
assert(buf[buf.len] == 0);
|
assert(buf[buf.len] == 0);
|
||||||
|
|
||||||
self.rt_surface.setClipboardString(buf, loc) catch |err| {
|
const confirm = self.config.clipboard_write == .ask;
|
||||||
|
self.rt_surface.setClipboardString(buf, loc, confirm) catch |err| {
|
||||||
log.err("error setting clipboard string err={}", .{err});
|
log.err("error setting clipboard string err={}", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -901,7 +902,7 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) void {
|
|||||||
};
|
};
|
||||||
defer self.alloc.free(buf);
|
defer self.alloc.free(buf);
|
||||||
|
|
||||||
self.rt_surface.setClipboardString(buf, clipboard) catch |err| {
|
self.rt_surface.setClipboardString(buf, clipboard, false) catch |err| {
|
||||||
log.err("error setting clipboard string err={}", .{err});
|
log.err("error setting clipboard string err={}", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -2279,7 +2280,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||||||
};
|
};
|
||||||
defer self.alloc.free(buf);
|
defer self.alloc.free(buf);
|
||||||
|
|
||||||
self.rt_surface.setClipboardString(buf, .standard) catch |err| {
|
self.rt_surface.setClipboardString(buf, .standard, false) catch |err| {
|
||||||
log.err("error setting clipboard string err={}", .{err});
|
log.err("error setting clipboard string err={}", .{err});
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@ -2509,19 +2510,25 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||||||
/// only be called once for each request. The data is immediately copied so
|
/// only be called once for each request. The data is immediately copied so
|
||||||
/// it is safe to free the data after this call.
|
/// it is safe to free the data after this call.
|
||||||
///
|
///
|
||||||
/// If "allow_unsafe" is false, then the data is checked for "safety" prior.
|
/// If `confirmed` is true then any clipboard confirmation prompts are skipped:
|
||||||
/// If unsafe data is detected, this will return error.UnsafePaste. Unsafe
|
///
|
||||||
|
/// - For "regular" pasting this means that unsafe pastes are allowed. Unsafe
|
||||||
/// data is defined as data that contains newlines, though this definition
|
/// data is defined as data that contains newlines, though this definition
|
||||||
/// may change later to detect other scenarios.
|
/// may change later to detect other scenarios.
|
||||||
|
///
|
||||||
|
/// - For OSC 52 pastes no prompt is shown to the user if `confirmed` is true.
|
||||||
|
///
|
||||||
|
/// If `confirmed` is false and either unsafe data is detected or the
|
||||||
|
/// `clipboard-read` option is set to `ask`, this will return error.UnsafePaste.
|
||||||
pub fn completeClipboardRequest(
|
pub fn completeClipboardRequest(
|
||||||
self: *Surface,
|
self: *Surface,
|
||||||
req: apprt.ClipboardRequest,
|
req: apprt.ClipboardRequest,
|
||||||
data: []const u8,
|
data: []const u8,
|
||||||
allow_unsafe: bool,
|
confirmed: bool,
|
||||||
) !void {
|
) !void {
|
||||||
switch (req) {
|
switch (req) {
|
||||||
.paste => try self.completeClipboardPaste(data, allow_unsafe),
|
.paste => try self.completeClipboardPaste(data, confirmed),
|
||||||
.osc_52 => |kind| try self.completeClipboardReadOSC52(data, kind),
|
.osc_52 => |kind| try self.completeClipboardReadOSC52(data, kind, confirmed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2534,9 +2541,9 @@ fn startClipboardRequest(
|
|||||||
) !void {
|
) !void {
|
||||||
switch (req) {
|
switch (req) {
|
||||||
.paste => {}, // always allowed
|
.paste => {}, // always allowed
|
||||||
.osc_52 => if (!self.config.clipboard_read) {
|
.osc_52 => if (self.config.clipboard_read == .deny) {
|
||||||
log.info(
|
log.info(
|
||||||
"application attempted to read clipboard, but 'clipboard-read' setting is off",
|
"application attempted to read clipboard, but 'clipboard-read' is set to deny",
|
||||||
.{},
|
.{},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -2635,7 +2642,16 @@ fn completeClipboardPaste(
|
|||||||
try self.io_thread.wakeup.notify();
|
try self.io_thread.wakeup.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn completeClipboardReadOSC52(self: *Surface, data: []const u8, kind: u8) !void {
|
fn completeClipboardReadOSC52(self: *Surface, data: []const u8, kind: u8, confirmed: bool) !void {
|
||||||
|
// We should never get here if clipboard-read is set to deny
|
||||||
|
assert(self.config.clipboard_read != .deny);
|
||||||
|
|
||||||
|
// If clipboard-read is set to ask and we haven't confirmed with the user,
|
||||||
|
// do that now
|
||||||
|
if (self.config.clipboard_read == .ask and !confirmed) {
|
||||||
|
return error.UnauthorizedPaste;
|
||||||
|
}
|
||||||
|
|
||||||
// Even if the clipboard data is empty we reply, since presumably
|
// Even if the clipboard data is empty we reply, since presumably
|
||||||
// the client app is expecting a reply. We first allocate our buffer.
|
// the client app is expecting a reply. We first allocate our buffer.
|
||||||
// This must hold the base64 encoded data PLUS the OSC code surrounding it.
|
// This must hold the base64 encoded data PLUS the OSC code surrounding it.
|
||||||
|
@ -17,6 +17,7 @@ const CoreInspector = @import("../inspector/main.zig").Inspector;
|
|||||||
const CoreSurface = @import("../Surface.zig");
|
const CoreSurface = @import("../Surface.zig");
|
||||||
const configpkg = @import("../config.zig");
|
const configpkg = @import("../config.zig");
|
||||||
const Config = configpkg.Config;
|
const Config = configpkg.Config;
|
||||||
|
const ClipboardPromptReason = @import("../apprt/structs.zig").ClipboardPromptReason;
|
||||||
|
|
||||||
const log = std.log.scoped(.embedded_window);
|
const log = std.log.scoped(.embedded_window);
|
||||||
|
|
||||||
@ -68,10 +69,11 @@ pub const App = struct {
|
|||||||
SurfaceUD,
|
SurfaceUD,
|
||||||
[*:0]const u8,
|
[*:0]const u8,
|
||||||
*apprt.ClipboardRequest,
|
*apprt.ClipboardRequest,
|
||||||
|
ClipboardPromptReason,
|
||||||
) callconv(.C) void,
|
) callconv(.C) void,
|
||||||
|
|
||||||
/// Write the clipboard value.
|
/// Write the clipboard value.
|
||||||
write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int) callconv(.C) void,
|
write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int, bool) callconv(.C) void,
|
||||||
|
|
||||||
/// Create a new split view. If the embedder doesn't support split
|
/// Create a new split view. If the embedder doesn't support split
|
||||||
/// views then this can be null.
|
/// views then this can be null.
|
||||||
@ -506,7 +508,7 @@ pub const Surface = struct {
|
|||||||
) void {
|
) void {
|
||||||
const alloc = self.app.core_app.alloc;
|
const alloc = self.app.core_app.alloc;
|
||||||
|
|
||||||
// Attempt to complete the request, but if its unsafe we may request
|
// Attempt to complete the request, but we may request
|
||||||
// confirmation.
|
// confirmation.
|
||||||
self.core_surface.completeClipboardRequest(
|
self.core_surface.completeClipboardRequest(
|
||||||
state.*,
|
state.*,
|
||||||
@ -518,6 +520,17 @@ pub const Surface = struct {
|
|||||||
self.opts.userdata,
|
self.opts.userdata,
|
||||||
str.ptr,
|
str.ptr,
|
||||||
state,
|
state,
|
||||||
|
.unsafe,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
error.UnauthorizedPaste => {
|
||||||
|
self.app.opts.confirm_read_clipboard(
|
||||||
|
self.opts.userdata,
|
||||||
|
str.ptr,
|
||||||
|
state,
|
||||||
|
.read,
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -526,8 +539,8 @@ pub const Surface = struct {
|
|||||||
else => log.err("error completing clipboard request err={}", .{err}),
|
else => log.err("error completing clipboard request err={}", .{err}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// We don't defer this because the unsafe paste route preserves
|
// We don't defer this because the clipboard confirmation route
|
||||||
// the clipboard request.
|
// preserves the clipboard request.
|
||||||
alloc.destroy(state);
|
alloc.destroy(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -535,11 +548,13 @@ pub const Surface = struct {
|
|||||||
self: *const Surface,
|
self: *const Surface,
|
||||||
val: [:0]const u8,
|
val: [:0]const u8,
|
||||||
clipboard_type: apprt.Clipboard,
|
clipboard_type: apprt.Clipboard,
|
||||||
|
confirm: bool,
|
||||||
) !void {
|
) !void {
|
||||||
self.app.opts.write_clipboard(
|
self.app.opts.write_clipboard(
|
||||||
self.opts.userdata,
|
self.opts.userdata,
|
||||||
val.ptr,
|
val.ptr,
|
||||||
@intCast(@intFromEnum(clipboard_type)),
|
@intCast(@intFromEnum(clipboard_type)),
|
||||||
|
confirm,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -678,7 +678,9 @@ pub const Surface = struct {
|
|||||||
self: *const Surface,
|
self: *const Surface,
|
||||||
val: [:0]const u8,
|
val: [:0]const u8,
|
||||||
clipboard_type: apprt.Clipboard,
|
clipboard_type: apprt.Clipboard,
|
||||||
|
confirm: bool,
|
||||||
) !void {
|
) !void {
|
||||||
|
_ = confirm;
|
||||||
_ = self;
|
_ = self;
|
||||||
switch (clipboard_type) {
|
switch (clipboard_type) {
|
||||||
.standard => glfw.setClipboardString(val),
|
.standard => glfw.setClipboardString(val),
|
||||||
|
@ -515,7 +515,10 @@ pub fn setClipboardString(
|
|||||||
self: *const Surface,
|
self: *const Surface,
|
||||||
val: [:0]const u8,
|
val: [:0]const u8,
|
||||||
clipboard_type: apprt.Clipboard,
|
clipboard_type: apprt.Clipboard,
|
||||||
|
confirm: bool,
|
||||||
) !void {
|
) !void {
|
||||||
|
// TODO: implement confirmation dialog when clipboard-write is "ask"
|
||||||
|
_ = confirm;
|
||||||
const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type);
|
const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type);
|
||||||
c.gdk_clipboard_set_text(clipboard, val.ptr);
|
c.gdk_clipboard_set_text(clipboard, val.ptr);
|
||||||
}
|
}
|
||||||
|
@ -41,3 +41,18 @@ pub const ClipboardRequest = union(enum) {
|
|||||||
/// A request to write clipboard contents via OSC 52.
|
/// A request to write clipboard contents via OSC 52.
|
||||||
osc_52: u8,
|
osc_52: u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The reason for displaying a clipboard prompt to the user
|
||||||
|
pub const ClipboardPromptReason = enum(i32) {
|
||||||
|
/// For pasting data only. Pasted data contains potentially unsafe
|
||||||
|
/// characters
|
||||||
|
unsafe = 1,
|
||||||
|
|
||||||
|
/// The user must authorize the application to read from the clipboard
|
||||||
|
read = 2,
|
||||||
|
|
||||||
|
/// The user must authorize the application to write to the clipboard
|
||||||
|
write = 3,
|
||||||
|
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
@ -456,9 +456,10 @@ keybind: Keybinds = .{},
|
|||||||
|
|
||||||
/// Whether to allow programs running in the terminal to read/write to
|
/// Whether to allow programs running in the terminal to read/write to
|
||||||
/// the system clipboard (OSC 52, for googling). The default is to
|
/// the system clipboard (OSC 52, for googling). The default is to
|
||||||
/// disallow clipboard reading but allow writing.
|
/// allow clipboard reading after prompting the user and allow writing
|
||||||
@"clipboard-read": bool = false,
|
/// unconditionally.
|
||||||
@"clipboard-write": bool = true,
|
@"clipboard-read": ClipboardRequest = .ask,
|
||||||
|
@"clipboard-write": ClipboardRequest = .allow,
|
||||||
|
|
||||||
/// Trims trailing whitespace on data that is copied to the clipboard.
|
/// Trims trailing whitespace on data that is copied to the clipboard.
|
||||||
/// This does not affect data sent to the clipboard via "clipboard-write".
|
/// This does not affect data sent to the clipboard via "clipboard-write".
|
||||||
@ -467,8 +468,6 @@ keybind: Keybinds = .{},
|
|||||||
/// Require confirmation before pasting text that appears unsafe. This helps
|
/// Require confirmation before pasting text that appears unsafe. This helps
|
||||||
/// prevent a "copy/paste attack" where a user may accidentally execute unsafe
|
/// prevent a "copy/paste attack" where a user may accidentally execute unsafe
|
||||||
/// commands by pasting text with newlines.
|
/// commands by pasting text with newlines.
|
||||||
///
|
|
||||||
/// This currently only works on Linux (GTK).
|
|
||||||
@"clipboard-paste-protection": bool = true,
|
@"clipboard-paste-protection": bool = true,
|
||||||
|
|
||||||
/// If true, bracketed pastes will be considered safe. By default,
|
/// If true, bracketed pastes will be considered safe. By default,
|
||||||
@ -2278,7 +2277,7 @@ pub const ShellIntegrationFeatures = packed struct {
|
|||||||
cursor: bool = true,
|
cursor: bool = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// OSC 10 and 11 default color reporting format.
|
/// OSC 4, 10, 11, and 12 default color reporting format.
|
||||||
pub const OSCColorReportFormat = enum {
|
pub const OSCColorReportFormat = enum {
|
||||||
none,
|
none,
|
||||||
@"8-bit",
|
@"8-bit",
|
||||||
@ -2306,3 +2305,10 @@ pub const MouseShiftCapture = enum {
|
|||||||
always,
|
always,
|
||||||
never,
|
never,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// How to treat requests to write to or read from the clipboard
|
||||||
|
pub const ClipboardRequest = enum {
|
||||||
|
allow,
|
||||||
|
deny,
|
||||||
|
ask,
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user