Merge pull request #818 from mitchellh/macos-paste-protect

macos: paste protection
This commit is contained in:
Mitchell Hashimoto
2023-11-05 09:32:46 -08:00
committed by GitHub
12 changed files with 307 additions and 18 deletions

View File

@ -325,6 +325,7 @@ 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_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e); typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e);
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);
@ -348,6 +349,7 @@ typedef struct {
ghostty_runtime_set_mouse_shape_cb set_mouse_shape_cb; ghostty_runtime_set_mouse_shape_cb set_mouse_shape_cb;
ghostty_runtime_set_mouse_visibility_cb set_mouse_visibility_cb; ghostty_runtime_set_mouse_visibility_cb set_mouse_visibility_cb;
ghostty_runtime_read_clipboard_cb read_clipboard_cb; ghostty_runtime_read_clipboard_cb read_clipboard_cb;
ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb;
ghostty_runtime_write_clipboard_cb write_clipboard_cb; ghostty_runtime_write_clipboard_cb write_clipboard_cb;
ghostty_runtime_new_split_cb new_split_cb; ghostty_runtime_new_split_cb new_split_cb;
ghostty_runtime_new_tab_cb new_tab_cb; ghostty_runtime_new_tab_cb new_tab_cb;
@ -411,7 +413,7 @@ void ghostty_surface_request_close(ghostty_surface_t);
void ghostty_surface_split(ghostty_surface_t, ghostty_split_direction_e); void ghostty_surface_split(ghostty_surface_t, ghostty_split_direction_e);
void ghostty_surface_split_focus(ghostty_surface_t, ghostty_split_focus_direction_e); void ghostty_surface_split_focus(ghostty_surface_t, ghostty_split_focus_direction_e);
bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t); bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t);
void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, uintptr_t, void *); void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, void *, bool);
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
void ghostty_inspector_free(ghostty_surface_t); void ghostty_inspector_free(ghostty_surface_t);

View File

@ -40,6 +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 */; };
A5E112952AF73E8A00C6E0C2 /* PasteProtectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* PasteProtectionController.swift */; };
A5E112972AF7401B00C6E0C2 /* PasteProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* PasteProtectionView.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 */
@ -80,6 +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>"; };
A5E112942AF73E8A00C6E0C2 /* PasteProtectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteProtectionController.swift; sourceTree = "<group>"; };
A5E112962AF7401B00C6E0C2 /* PasteProtectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteProtectionView.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 */
@ -101,6 +107,7 @@
children = ( children = (
A56D58872ACDE6BE00508D2C /* Services */, A56D58872ACDE6BE00508D2C /* Services */,
A59630982AEE1C4400D64628 /* Terminal */, A59630982AEE1C4400D64628 /* Terminal */,
A5E112912AF73E4D00C6E0C2 /* Paste Protection */,
A534263E2A7DCC5800EBB7A2 /* Settings */, A534263E2A7DCC5800EBB7A2 /* Settings */,
); );
path = Features; path = Features;
@ -227,6 +234,16 @@
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A5E112912AF73E4D00C6E0C2 /* Paste Protection */ = {
isa = PBXGroup;
children = (
A5E112922AF73E6E00C6E0C2 /* PasteProtection.xib */,
A5E112942AF73E8A00C6E0C2 /* PasteProtectionController.swift */,
A5E112962AF7401B00C6E0C2 /* PasteProtectionView.swift */,
);
path = "Paste Protection";
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -289,6 +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 */,
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */, A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */,
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */,
); );
@ -324,9 +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 */,
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 */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -0,0 +1,28 @@
<?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="Warning: Potentially Unsafe Paste" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="F0z-JX-Cv5">
<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="3008" height="1667"/>
<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>

View File

@ -0,0 +1,37 @@
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
))
}
}

View File

@ -0,0 +1,60 @@
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)
}
}

View File

@ -4,7 +4,10 @@ import SwiftUI
import GhosttyKit 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, TerminalViewDelegate, TerminalViewModel { class TerminalController: NSWindowController, NSWindowDelegate,
TerminalViewDelegate, TerminalViewModel,
PasteProtectionViewDelegate
{
override var windowNibName: NSNib.Name? { "Terminal" } override var windowNibName: NSNib.Name? { "Terminal" }
/// The app instance that this terminal view will represent. /// The app instance that this terminal view will represent.
@ -28,6 +31,9 @@ class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDele
/// 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.
private var pasteProtection: PasteProtectionController? = 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
super.init(window: nil) super.init(window: nil)
@ -48,6 +54,11 @@ class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDele
selector: #selector(onGotoTab), selector: #selector(onGotoTab),
name: Ghostty.Notification.ghosttyGotoTab, name: Ghostty.Notification.ghosttyGotoTab,
object: nil) object: nil)
center.addObserver(
self,
selector: #selector(onConfirmUnsafePaste),
name: Ghostty.Notification.confirmUnsafePaste,
object: nil)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -309,6 +320,30 @@ class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDele
self.window?.close() self.window?.close()
} }
//MARK: - Paste Protection
func pasteProtectionComplete(_ action: PasteProtectionView.Action) {
// End our paste protection no matter what
guard let pp = self.pasteProtection else { return }
self.pasteProtection = nil
// Close the sheet
if let ppWindow = pp.window {
window?.endSheet(ppWindow)
}
let str: String
switch (action) {
case .cancel:
str = ""
case .paste:
str = pp.contents
}
Ghostty.AppState.completeClipboardRequest(pp.surface, data: str, state: pp.state, confirmed: true)
}
//MARK: - Notifications //MARK: - Notifications
@objc private func onGotoTab(notification: SwiftUI.Notification) { @objc private func onGotoTab(notification: SwiftUI.Notification) {
@ -367,4 +402,33 @@ class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDele
Ghostty.moveFocus(to: focusedSurface) Ghostty.moveFocus(to: focusedSurface)
} }
} }
@objc private func onConfirmUnsafePaste(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard target == self.focusedSurface else { return }
guard let surface = target.surface else { return }
// We need a window
guard let window = self.window else { return }
// Check whether we use non-native fullscreen
guard let str = notification.userInfo?[Ghostty.Notification.UnsafePasteStrKey] as? String else { return }
guard let state = notification.userInfo?[Ghostty.Notification.UnsafePasteStateKey] as? UnsafeMutableRawPointer? else { return }
// If we already have a paste protection view up, we ignore this request.
// This shouldn't be possible...
guard self.pasteProtection == nil else {
Ghostty.AppState.completeClipboardRequest(surface, data: "", state: state, confirmed: true)
return
}
// Show our paste confirmation
self.pasteProtection = PasteProtectionController(
surface: surface,
contents: str,
state: state,
delegate: self
)
window.beginSheet(self.pasteProtection!.window!)
}
} }

View File

@ -4,7 +4,7 @@ import GhosttyKit
/// This delegate is notified of actions and property changes regarding the terminal view. This /// This delegate is notified of actions and property changes regarding the terminal view. This
/// delegate is optional and can be used by a TerminalView caller to react to changes such as /// delegate is optional and can be used by a TerminalView caller to react to changes such as
/// titles being set, cell sizes being changed, etc. /// titles being set, cell sizes being changed, etc.
protocol TerminalViewDelegate: AnyObject, ObservableObject { protocol TerminalViewDelegate: AnyObject {
/// Called when the currently focused surface changed. This can be nil. /// Called when the currently focused surface changed. This can be nil.
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) func focusedSurfaceDidChange(to: Ghostty.SurfaceView?)

View File

@ -141,6 +141,7 @@ 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 ) },
write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) }, write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) },
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) },
@ -390,9 +391,31 @@ extension Ghostty {
completeClipboardRequest(surface, data: str, state: state) completeClipboardRequest(surface, data: str, state: state)
} }
static private func completeClipboardRequest(_ surface: ghostty_surface_t, data: String, state: UnsafeMutableRawPointer?) { static func confirmReadClipboard(
_ userdata: UnsafeMutableRawPointer?,
string: UnsafePointer<CChar>?,
state: UnsafeMutableRawPointer?
) {
guard let surface = self.surfaceUserdata(from: userdata) else { return }
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
NotificationCenter.default.post(
name: Notification.confirmUnsafePaste,
object: surface,
userInfo: [
Notification.UnsafePasteStrKey: valueStr,
Notification.UnsafePasteStateKey: state as Any
]
)
}
static func completeClipboardRequest(
_ surface: ghostty_surface_t,
data: String,
state: UnsafeMutableRawPointer?,
confirmed: Bool = false
) {
data.withCString { ptr in data.withCString { ptr in
ghostty_surface_complete_clipboard_request(surface, ptr, UInt(data.utf8.count), state) ghostty_surface_complete_clipboard_request(surface, ptr, state, confirmed)
} }
} }

View File

@ -108,6 +108,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 UnsafePasteStrKey = confirmUnsafePaste.rawValue + ".str"
static let UnsafePasteStateKey = confirmUnsafePaste.rawValue + ".state"
} }
// Make the input enum hashable. // Make the input enum hashable.

View File

@ -143,6 +143,7 @@ const DerivedConfig = struct {
clipboard_write: bool, clipboard_write: bool,
clipboard_trim_trailing_spaces: bool, clipboard_trim_trailing_spaces: bool,
clipboard_paste_protection: bool, clipboard_paste_protection: bool,
clipboard_paste_bracketed_safe: bool,
copy_on_select: configpkg.CopyOnSelect, copy_on_select: configpkg.CopyOnSelect,
confirm_close_surface: bool, confirm_close_surface: bool,
mouse_interval: u64, mouse_interval: u64,
@ -167,6 +168,7 @@ const DerivedConfig = struct {
.clipboard_write = config.@"clipboard-write", .clipboard_write = config.@"clipboard-write",
.clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces", .clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces",
.clipboard_paste_protection = config.@"clipboard-paste-protection", .clipboard_paste_protection = config.@"clipboard-paste-protection",
.clipboard_paste_bracketed_safe = config.@"clipboard-paste-bracketed-safe",
.copy_on_select = config.@"copy-on-select", .copy_on_select = config.@"copy-on-select",
.confirm_close_surface = config.@"confirm-close-surface", .confirm_close_surface = config.@"confirm-close-surface",
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms .mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
@ -2514,7 +2516,7 @@ fn completeClipboardPaste(
// //
// We do not do this for bracketed pastes because bracketed pastes are // We do not do this for bracketed pastes because bracketed pastes are
// by definition safe since they're framed. // by definition safe since they're framed.
if (!bracketed and if ((!self.config.clipboard_paste_bracketed_safe or !bracketed) and
self.config.clipboard_paste_protection and self.config.clipboard_paste_protection and
!allow_unsafe and !allow_unsafe and
!terminal.isSafePaste(data)) !terminal.isSafePaste(data))

View File

@ -61,6 +61,15 @@ pub const App = struct {
/// value then this should return null. /// value then this should return null.
read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.C) void, read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.C) void,
/// This may be called after a read clipboard call to request
/// confirmation that the clipboard value is safe to read. The embedder
/// must call complete_clipboard_request with the given request.
confirm_read_clipboard: *const fn (
SurfaceUD,
[*:0]const u8,
*apprt.ClipboardRequest,
) 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) callconv(.C) void,
@ -451,6 +460,45 @@ pub const Surface = struct {
); );
} }
fn completeClipboardRequest(
self: *Surface,
str: [:0]const u8,
state: *apprt.ClipboardRequest,
confirmed: bool,
) void {
const alloc = self.app.core_app.alloc;
// If there is no string, then we don't do anything and complete.
if (str.len == 0) {
alloc.destroy(state);
return;
}
// Attempt to complete the request, but if its unsafe we may request
// confirmation.
self.core_surface.completeClipboardRequest(
state.*,
str,
confirmed,
) catch |err| switch (err) {
error.UnsafePaste => {
self.app.opts.confirm_read_clipboard(
self.opts.userdata,
str.ptr,
state,
);
return;
},
else => log.err("error completing clipboard request err={}", .{err}),
};
// We don't defer this because the unsafe paste route preserves
// the clipboard request.
alloc.destroy(state);
}
pub fn setClipboardString( pub fn setClipboardString(
self: *const Surface, self: *const Surface,
val: [:0]const u8, val: [:0]const u8,
@ -1351,20 +1399,15 @@ pub const CAPI = struct {
/// with a request the request pointer will be invalidated. /// with a request the request pointer will be invalidated.
export fn ghostty_surface_complete_clipboard_request( export fn ghostty_surface_complete_clipboard_request(
ptr: *Surface, ptr: *Surface,
str_ptr: [*]const u8, str: [*:0]const u8,
str_len: usize,
state: *apprt.ClipboardRequest, state: *apprt.ClipboardRequest,
confirmed: bool,
) void { ) void {
// The state is unusable after this ptr.completeClipboardRequest(
defer ptr.core_surface.app.alloc.destroy(state); std.mem.sliceTo(str, 0),
state,
if (str_len == 0) return; confirmed,
const str = str_ptr[0..str_len]; );
// TODO: Support sanaization for MacOS (force: false)
ptr.core_surface.completeClipboardRequest(state.*, str, true) catch |err| {
log.err("error completing clipboard request err={}", .{err});
return;
};
} }
export fn ghostty_surface_inspector(ptr: *Surface) ?*Inspector { export fn ghostty_surface_inspector(ptr: *Surface) ?*Inspector {

View File

@ -430,6 +430,12 @@ keybind: Keybinds = .{},
/// This currently only works on Linux (GTK). /// 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,
/// bracketed pastes are considered safe. "Bracketed" pastes are pastes
/// while the running program has bracketed paste mode enabled (a setting
/// set by the running program, not the terminal emulator).
@"clipboard-paste-bracketed-safe": bool = true,
/// The total amount of bytes that can be used for image data (i.e. /// The total amount of bytes that can be used for image data (i.e.
/// the Kitty image protocol) per terminal scren. The maximum value /// the Kitty image protocol) per terminal scren. The maximum value
/// is 4,294,967,295 (4GB). The default is 320MB. If this is set to zero, /// is 4,294,967,295 (4GB). The default is 320MB. If this is set to zero,