Merge pull request #857 from gpanders/osc52-prompt

Implement user prompts for accessing clipboard via OSC 52
This commit is contained in:
Mitchell Hashimoto
2023-11-11 15:16:12 -08:00
committed by GitHub
21 changed files with 480 additions and 248 deletions

View File

@ -35,6 +35,12 @@ typedef enum {
GHOSTTY_CLIPBOARD_SELECTION,
} ghostty_clipboard_e;
typedef enum {
GHOSTTY_CLIPBOARD_REQUEST_PASTE,
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE,
} ghostty_clipboard_request_e;
typedef enum {
GHOSTTY_SPLIT_RIGHT,
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_visibility_cb)(void *, bool);
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_confirm_read_clipboard_cb)(void *, const char*, void *, ghostty_clipboard_request_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_tab_cb)(void *, ghostty_surface_config_s);
typedef void (*ghostty_runtime_new_window_cb)(void *, ghostty_surface_config_s);

View File

@ -40,9 +40,9 @@
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.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 */; };
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 */; };
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; };
/* End PBXBuildFile section */
@ -83,9 +83,9 @@
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>"; };
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>"; };
A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = "<group>"; };
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.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>"; };
/* End PBXFileReference section */
@ -107,7 +107,7 @@
children = (
A56D58872ACDE6BE00508D2C /* Services */,
A59630982AEE1C4400D64628 /* Terminal */,
A5E112912AF73E4D00C6E0C2 /* Paste Protection */,
A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */,
A534263E2A7DCC5800EBB7A2 /* Settings */,
);
path = Features;
@ -234,14 +234,14 @@
name = Frameworks;
sourceTree = "<group>";
};
A5E112912AF73E4D00C6E0C2 /* Paste Protection */ = {
A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */ = {
isa = PBXGroup;
children = (
A5E112922AF73E6E00C6E0C2 /* PasteProtection.xib */,
A5E112942AF73E8A00C6E0C2 /* PasteProtectionController.swift */,
A5E112962AF7401B00C6E0C2 /* PasteProtectionView.swift */,
A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */,
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */,
A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */,
);
path = "Paste Protection";
path = ClipboardConfirmation;
sourceTree = "<group>";
};
/* End PBXGroup section */
@ -306,7 +306,7 @@
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */,
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */,
A5E112932AF73E6E00C6E0C2 /* PasteProtection.xib in Resources */,
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */,
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */,
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */,
);
@ -342,11 +342,11 @@
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */,
A5E112952AF73E8A00C6E0C2 /* PasteProtectionController.swift in Sources */,
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */,
8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */,
A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */,
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */,
A5E112972AF7401B00C6E0C2 /* PasteProtectionView.swift in Sources */,
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -13,11 +13,11 @@
</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">
<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="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">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>

View File

@ -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 request: Ghostty.ClipboardRequest
let state: UnsafeMutableRawPointer?
weak private var delegate: ClipboardConfirmationViewDelegate? = nil
init(surface: ghostty_surface_t, contents: String, request: Ghostty.ClipboardRequest, state: UnsafeMutableRawPointer?, delegate: ClipboardConfirmationViewDelegate) {
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 }
switch (request) {
case .paste:
window.title = "Warning: Potentially Unsafe Paste"
case .osc_52_read, .osc_52_write:
window.title = "Authorize Clipboard Access"
}
window.contentView = NSHostingView(rootView: ClipboardConfirmationView(
contents: contents,
request: request,
delegate: delegate
))
}
}

View File

@ -0,0 +1,75 @@
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? = nil
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))
.textSelection(.enabled)
.font(.system(.body, design: .monospaced))
.padding(.all, 4)
HStack {
Spacer()
Button(Action.text(.cancel, request)) { onCancel() }
.keyboardShortcut(.cancelAction)
Button(Action.text(.confirm, request)) { onPaste() }
.keyboardShortcut(.defaultAction)
Spacer()
}
.padding(.bottom)
}
}
private func onCancel() {
delegate?.clipboardConfirmationComplete(.cancel, request)
}
private func onPaste() {
delegate?.clipboardConfirmationComplete(.confirm, request)
}
}

View File

@ -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
))
}
}

View File

@ -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)
}
}

View File

@ -6,7 +6,7 @@ import GhosttyKit
/// The terminal controller is an NSWindowController that maps 1:1 to a terminal window.
class TerminalController: NSWindowController, NSWindowDelegate,
TerminalViewDelegate, TerminalViewModel,
PasteProtectionViewDelegate
ClipboardConfirmationViewDelegate
{
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.
private var alert: NSAlert? = nil
/// The paste protection window, if shown.
private var pasteProtection: PasteProtectionController? = nil
/// The clipboard confirmation window, if shown.
private var clipboardConfirmation: ClipboardConfirmationController? = nil
init(_ ghostty: Ghostty.AppState, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) {
self.ghostty = ghostty
@ -56,8 +56,8 @@ class TerminalController: NSWindowController, NSWindowDelegate,
object: nil)
center.addObserver(
self,
selector: #selector(onConfirmUnsafePaste),
name: Ghostty.Notification.confirmUnsafePaste,
selector: #selector(onConfirmClipboardRequest),
name: Ghostty.Notification.confirmClipboard,
object: nil)
}
@ -346,28 +346,36 @@ class TerminalController: NSWindowController, NSWindowDelegate,
self.window?.close()
}
//MARK: - Paste Protection
//MARK: - Clipboard Confirmation
func pasteProtectionComplete(_ action: PasteProtectionView.Action) {
// End our paste protection no matter what
guard let pp = self.pasteProtection else { return }
self.pasteProtection = nil
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.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 ppWindow = pp.window {
window?.endSheet(ppWindow)
if let ccWindow = cc.window {
window?.endSheet(ccWindow)
}
let str: String
switch (action) {
case .cancel:
str = ""
case .paste:
str = pp.contents
switch (request) {
case .osc_52_write:
guard case .confirm = action else { break }
let pb = NSPasteboard.general
pb.declareTypes([.string], owner: nil)
pb.setString(cc.contents, forType: .string)
case .osc_52_read, .paste:
let str: String
switch (action) {
case .cancel:
str = ""
case .confirm:
str = cc.contents
}
Ghostty.AppState.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true)
}
Ghostty.AppState.completeClipboardRequest(pp.surface, data: str, state: pp.state, confirmed: true)
}
//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 target == self.focusedSurface else { return }
guard let surface = target.surface else { return }
@ -438,23 +446,25 @@ class TerminalController: NSWindowController, NSWindowDelegate,
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 }
guard let str = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStrKey] as? String else { return }
guard let state = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStateKey] as? UnsafeMutableRawPointer? else { return }
guard let request = notification.userInfo?[Ghostty.Notification.ConfirmClipboardRequestKey] as? Ghostty.ClipboardRequest 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...
guard self.pasteProtection == nil else {
guard self.clipboardConfirmation == nil else {
Ghostty.AppState.completeClipboardRequest(surface, data: "", state: state, confirmed: true)
return
}
// Show our paste confirmation
self.pasteProtection = PasteProtectionController(
self.clipboardConfirmation = ClipboardConfirmationController(
surface: surface,
contents: str,
request: request,
state: state,
delegate: self
)
window.beginSheet(self.pasteProtection!.window!)
window.beginSheet(self.clipboardConfirmation!.window!)
}
}

View File

@ -150,8 +150,8 @@ extension Ghostty {
set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) },
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) },
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) },
confirm_read_clipboard_cb: { userdata, str, state, request in AppState.confirmReadClipboard(userdata, string: str, state: state, request: request ) },
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_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) },
new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) },
@ -433,16 +433,19 @@ extension Ghostty {
static func confirmReadClipboard(
_ userdata: UnsafeMutableRawPointer?,
string: UnsafePointer<CChar>?,
state: UnsafeMutableRawPointer?
state: UnsafeMutableRawPointer?,
request: ghostty_clipboard_request_e
) {
guard let surface = self.surfaceUserdata(from: userdata) else { return }
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
guard let request = Ghostty.ClipboardRequest.from(request: request) else { return }
NotificationCenter.default.post(
name: Notification.confirmUnsafePaste,
name: Notification.confirmClipboard,
object: surface,
userInfo: [
Notification.UnsafePasteStrKey: valueStr,
Notification.UnsafePasteStateKey: state as Any
Notification.ConfirmClipboardStrKey: valueStr,
Notification.ConfirmClipboardStateKey: state as Any,
Notification.ConfirmClipboardRequestKey: request,
]
)
}
@ -458,14 +461,28 @@ 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
if (location != GHOSTTY_CLIPBOARD_STANDARD) { return }
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
let pb = NSPasteboard.general
pb.declareTypes([.string], owner: nil)
pb.setString(valueStr, forType: .string)
if !confirm {
let pb = NSPasteboard.general
pb.declareTypes([.string], owner: nil)
pb.setString(valueStr, forType: .string)
return
}
NotificationCenter.default.post(
name: Notification.confirmClipboard,
object: surface,
userInfo: [
Notification.ConfirmClipboardStrKey: valueStr,
Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write,
]
)
}
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {

View File

@ -94,6 +94,51 @@ extension Ghostty {
}
}
}
/// The type of a clipboard request
enum ClipboardRequest {
/// A direct paste of clipboard contents
case paste
/// An application is attempting to read from the clipboard using OSC 52
case osc_52_read
/// An applciation is attempting to write to the clipboard using OSC 52
case osc_52_write
/// The text to show in the clipboard confirmation prompt for a given request type
func text() -> String {
switch (self) {
case .paste:
return """
Pasting this text to the terminal may be dangerous as it looks like some commands may be executed.
"""
case .osc_52_read:
return """
An application is attempting to read from the clipboard.
The current clipboard contents are shown below.
"""
case .osc_52_write:
return """
An application is attempting to write to the clipboard.
The content to write is shown below.
"""
}
}
static func from(request: ghostty_clipboard_request_e) -> ClipboardRequest? {
switch (request) {
case GHOSTTY_CLIPBOARD_REQUEST_PASTE:
return .paste
case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ:
return .osc_52_read
case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE:
return .osc_52_write
default:
return nil
}
}
}
}
extension Ghostty.Notification {
@ -142,9 +187,10 @@ extension Ghostty.Notification {
/// Notification to show/hide the inspector
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"
static let confirmClipboard = Notification.Name("com.mitchellh.ghostty.confirmClipboard")
static let ConfirmClipboardStrKey = confirmClipboard.rawValue + ".str"
static let ConfirmClipboardStateKey = confirmClipboard.rawValue + ".state"
static let ConfirmClipboardRequestKey = confirmClipboard.rawValue + ".request"
/// Notification sent to the active split view to resize the split.
static let didResizeSplit = Notification.Name("com.mitchellh.ghostty.didResizeSplit")

View File

@ -140,8 +140,8 @@ const DerivedConfig = struct {
/// For docs for these, see the associated config they are derived from.
original_font_size: u8,
keybind: configpkg.Keybinds,
clipboard_read: bool,
clipboard_write: bool,
clipboard_read: configpkg.ClipboardAccess,
clipboard_write: configpkg.ClipboardAccess,
clipboard_trim_trailing_spaces: bool,
clipboard_paste_protection: bool,
clipboard_paste_bracketed_safe: bool,
@ -688,21 +688,21 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.cell_size => |size| try self.setCellSize(size),
.clipboard_read => |kind| {
if (!self.config.clipboard_read) {
log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{});
.clipboard_read => |clipboard| {
if (self.config.clipboard_read == .deny) {
log.info("application attempted to read clipboard, but 'clipboard-read' is set to deny", .{});
return;
}
try self.startClipboardRequest(.standard, .{ .osc_52 = kind });
try self.startClipboardRequest(.standard, .{ .osc_52_read = clipboard });
},
.clipboard_write => |req| switch (req) {
.small => |v| try self.clipboardWrite(v.data[0..v.len], .standard),
.stable => |v| try self.clipboardWrite(v, .standard),
.clipboard_write => |w| switch (w.req) {
.small => |v| try self.clipboardWrite(v.data[0..v.len], w.clipboard_type),
.stable => |v| try self.clipboardWrite(v, w.clipboard_type),
.alloc => |v| {
defer v.alloc.free(v.data);
try self.clipboardWrite(v.data, .standard);
try self.clipboardWrite(v.data, w.clipboard_type);
},
},
@ -822,8 +822,8 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
}
fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) !void {
if (!self.config.clipboard_write) {
log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{});
if (self.config.clipboard_write == .deny) {
log.info("application attempted to write clipboard, but 'clipboard-write' is set to deny", .{});
return;
}
@ -856,7 +856,11 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard)
};
assert(buf[buf.len] == 0);
self.rt_surface.setClipboardString(buf, loc) catch |err| {
// When clipboard-write is "ask" a prompt is displayed to the user asking
// them to confirm the clipboard access. Each app runtime handles this
// differently.
const confirm = self.config.clipboard_write == .ask;
self.rt_surface.setClipboardString(buf, loc, confirm) catch |err| {
log.err("error setting clipboard string err={}", .{err});
return;
};
@ -901,7 +905,7 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) void {
};
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});
return;
};
@ -2279,7 +2283,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
};
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});
return true;
};
@ -2509,19 +2513,37 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
/// only be called once for each request. The data is immediately copied so
/// it is safe to free the data after this call.
///
/// If "allow_unsafe" is false, then the data is checked for "safety" prior.
/// If unsafe data is detected, this will return error.UnsafePaste. Unsafe
/// data is defined as data that contains newlines, though this definition
/// may change later to detect other scenarios.
/// If `confirmed` is true then any clipboard confirmation prompts are skipped:
///
/// - For "regular" pasting this means that unsafe pastes are allowed. Unsafe
/// data is defined as data that contains newlines, though this definition
/// may change later to detect other scenarios.
///
/// - For OSC 52 reads and writes no prompt is shown to the user if
/// `confirmed` is true.
///
/// If `confirmed` is false then this may return either an UnsafePaste or
/// UnauthorizedPaste error, depending on the type of clipboard request.
pub fn completeClipboardRequest(
self: *Surface,
req: apprt.ClipboardRequest,
data: []const u8,
allow_unsafe: bool,
data: [:0]const u8,
confirmed: bool,
) !void {
switch (req) {
.paste => try self.completeClipboardPaste(data, allow_unsafe),
.osc_52 => |kind| try self.completeClipboardReadOSC52(data, kind),
.paste => try self.completeClipboardPaste(data, confirmed),
.osc_52_read => |clipboard| try self.completeClipboardReadOSC52(
data,
clipboard,
confirmed,
),
.osc_52_write => |clipboard| try self.rt_surface.setClipboardString(
data,
clipboard,
!confirmed,
),
}
}
@ -2534,13 +2556,16 @@ fn startClipboardRequest(
) !void {
switch (req) {
.paste => {}, // always allowed
.osc_52 => if (!self.config.clipboard_read) {
.osc_52_read => if (self.config.clipboard_read == .deny) {
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;
},
// No clipboard write code paths travel through this function
.osc_52_write => unreachable,
}
try self.rt_surface.clipboardRequest(loc, req);
@ -2635,7 +2660,21 @@ fn completeClipboardPaste(
try self.io_thread.wakeup.notify();
}
fn completeClipboardReadOSC52(self: *Surface, data: []const u8, kind: u8) !void {
fn completeClipboardReadOSC52(
self: *Surface,
data: []const u8,
clipboard_type: apprt.Clipboard,
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
// 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.
@ -2644,6 +2683,11 @@ fn completeClipboardReadOSC52(self: *Surface, data: []const u8, kind: u8) !void
var buf = try self.alloc.alloc(u8, size + 9); // const for OSC
defer self.alloc.free(buf);
const kind: u8 = switch (clipboard_type) {
.standard => 'c',
.selection => 's',
};
// Wrap our data with the OSC code
const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind});
assert(prefix.len == 7);

View File

@ -68,10 +68,11 @@ pub const App = struct {
SurfaceUD,
[*:0]const u8,
*apprt.ClipboardRequest,
apprt.ClipboardRequestType,
) callconv(.C) void,
/// 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
/// views then this can be null.
@ -506,18 +507,21 @@ pub const Surface = struct {
) void {
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.
self.core_surface.completeClipboardRequest(
state.*,
str,
confirmed,
) catch |err| switch (err) {
error.UnsafePaste => {
error.UnsafePaste,
error.UnauthorizedPaste,
=> {
self.app.opts.confirm_read_clipboard(
self.opts.userdata,
str.ptr,
state,
state.*,
);
return;
@ -526,8 +530,8 @@ pub const Surface = struct {
else => log.err("error completing clipboard request err={}", .{err}),
};
// We don't defer this because the unsafe paste route preserves
// the clipboard request.
// We don't defer this because the clipboard confirmation route
// preserves the clipboard request.
alloc.destroy(state);
}
@ -535,11 +539,13 @@ pub const Surface = struct {
self: *const Surface,
val: [:0]const u8,
clipboard_type: apprt.Clipboard,
confirm: bool,
) !void {
self.app.opts.write_clipboard(
self.opts.userdata,
val.ptr,
@intCast(@intFromEnum(clipboard_type)),
confirm,
);
}

View File

@ -656,7 +656,7 @@ pub const Surface = struct {
state: apprt.ClipboardRequest,
) !void {
// GLFW can read clipboards immediately so just do that.
const str: []const u8 = switch (clipboard_type) {
const str: [:0]const u8 = switch (clipboard_type) {
.standard => glfw.getClipboardString() orelse return glfw.mustGetErrorCode(),
.selection => selection: {
// Not supported except on Linux
@ -678,7 +678,9 @@ pub const Surface = struct {
self: *const Surface,
val: [:0]const u8,
clipboard_type: apprt.Clipboard,
confirm: bool,
) !void {
_ = confirm;
_ = self;
switch (clipboard_type) {
.standard => glfw.setClipboardString(val),

View File

@ -24,7 +24,7 @@ const build_options = @import("build_options");
const Surface = @import("Surface.zig");
const Window = @import("Window.zig");
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
const UnsafePasteWindow = @import("UnsafePasteWindow.zig");
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
const c = @import("c.zig");
const inspector = @import("inspector.zig");
const key = @import("key.zig");
@ -49,8 +49,8 @@ menu: ?*c.GMenu = null,
/// The configuration errors window, if it is currently open.
config_errors_window: ?*ConfigErrorsWindow = null,
/// The unsafe paste window, if it is currently open.
unsafe_paste_window: ?*UnsafePasteWindow = null,
/// The clipboard confirmation window, if it is currently open.
clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
/// This is set to false when the main loop should exit.
running: bool = true,

View File

@ -1,11 +1,11 @@
/// Unsafe Paste Window
const UnsafePaste = @This();
/// Clipboard Confirmation Window
const ClipboardConfirmation = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const apprt = @import("../../apprt.zig");
const CoreSurface = @import("../../Surface.zig");
const ClipboardRequest = @import("../structs.zig").ClipboardRequest;
const App = @import("App.zig");
const View = @import("View.zig");
const c = @import("c.zig");
@ -16,21 +16,22 @@ app: *App,
window: *c.GtkWindow,
view: PrimaryView,
data: []u8,
data: [:0]u8,
core_surface: CoreSurface,
pending_req: ClipboardRequest,
pending_req: apprt.ClipboardRequest,
pub fn create(
app: *App,
data: []const u8,
core_surface: CoreSurface,
request: ClipboardRequest,
request: apprt.ClipboardRequest,
) !void {
if (app.unsafe_paste_window != null) return error.WindowAlreadyExists;
if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists;
const alloc = app.core_app.alloc;
const self = try alloc.create(UnsafePaste);
const self = try alloc.create(ClipboardConfirmation);
errdefer alloc.destroy(self);
try self.init(
app,
data,
@ -38,28 +39,29 @@ pub fn create(
request,
);
app.unsafe_paste_window = self;
app.clipboard_confirmation_window = self;
}
/// Not public because this should be called by the GTK lifecycle.
fn destroy(self: *UnsafePaste) void {
fn destroy(self: *ClipboardConfirmation) void {
const alloc = self.app.core_app.alloc;
self.app.unsafe_paste_window = null;
self.app.clipboard_confirmation_window = null;
alloc.free(self.data);
alloc.destroy(self);
}
fn init(
self: *UnsafePaste,
self: *ClipboardConfirmation,
app: *App,
data: []const u8,
core_surface: CoreSurface,
request: ClipboardRequest,
request: apprt.ClipboardRequest,
) !void {
// Create the window
const window = c.gtk_window_new();
const gtk_window: *c.GtkWindow = @ptrCast(window);
errdefer c.gtk_window_destroy(gtk_window);
c.gtk_window_set_title(gtk_window, "Warning: Potentially Unsafe Paste");
c.gtk_window_set_title(gtk_window, titleText(request));
c.gtk_window_set_default_size(gtk_window, 550, 275);
c.gtk_window_set_resizable(gtk_window, 0);
_ = c.g_signal_connect_data(
@ -76,7 +78,7 @@ fn init(
.app = app,
.window = gtk_window,
.view = undefined,
.data = try app.core_app.alloc.dupe(u8, data),
.data = try app.core_app.alloc.dupeZ(u8, data),
.core_surface = core_surface,
.pending_req = request,
};
@ -93,7 +95,7 @@ fn init(
}
fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const self: *UnsafePaste = @ptrCast(@alignCast(ud orelse return));
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud orelse return));
self.destroy();
}
@ -101,12 +103,9 @@ const PrimaryView = struct {
root: *c.GtkWidget,
text: *c.GtkTextView,
pub fn init(root: *UnsafePaste, data: []const u8) !PrimaryView {
pub fn init(root: *ClipboardConfirmation, data: []const u8) !PrimaryView {
// All our widgets
const label = c.gtk_label_new(
"Pasting this text into the terminal may be dangerous as " ++
"it looks like some commands may be executed.",
);
const label = c.gtk_label_new(promptText(root.pending_req));
const buf = unsafeBuffer(data);
defer c.g_object_unref(buf);
const buttons = try ButtonsView.init(root);
@ -157,20 +156,25 @@ const PrimaryView = struct {
const ButtonsView = struct {
root: *c.GtkWidget,
pub fn init(root: *UnsafePaste) !ButtonsView {
const cancel_button = c.gtk_button_new_with_label("Cancel");
pub fn init(root: *ClipboardConfirmation) !ButtonsView {
const cancel_text, const confirm_text = switch (root.pending_req) {
.paste => .{ "Cancel", "Paste" },
.osc_52_read, .osc_52_write => .{ "Deny", "Allow" },
};
const cancel_button = c.gtk_button_new_with_label(cancel_text);
errdefer c.g_object_unref(cancel_button);
const paste_button = c.gtk_button_new_with_label("Paste");
errdefer c.g_object_unref(paste_button);
const confirm_button = c.gtk_button_new_with_label(confirm_text);
errdefer c.g_object_unref(confirm_button);
// TODO: Focus on the paste button
// c.gtk_widget_grab_focus(paste_button);
// c.gtk_widget_grab_focus(confirm_button);
// Create our view
const view = try View.init(&.{
.{ .name = "cancel", .widget = cancel_button },
.{ .name = "paste", .widget = paste_button },
.{ .name = "confirm", .widget = confirm_button },
}, &vfl);
// Signals
@ -183,9 +187,9 @@ const ButtonsView = struct {
c.G_CONNECT_DEFAULT,
);
_ = c.g_signal_connect_data(
paste_button,
confirm_button,
"clicked",
c.G_CALLBACK(&gtkPasteClick),
c.G_CALLBACK(&gtkConfirmClick),
root,
null,
c.G_CONNECT_DEFAULT,
@ -195,13 +199,13 @@ const ButtonsView = struct {
}
fn gtkCancelClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const self: *UnsafePaste = @ptrCast(@alignCast(ud));
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud));
c.gtk_window_destroy(@ptrCast(self.window));
}
fn gtkPasteClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
fn gtkConfirmClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
// Requeue the paste with force.
const self: *UnsafePaste = @ptrCast(@alignCast(ud));
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud));
self.core_surface.completeClipboardRequest(
self.pending_req,
self.data,
@ -214,6 +218,32 @@ const ButtonsView = struct {
}
const vfl = [_][*:0]const u8{
"H:[cancel]-8-[paste]-8-|",
"H:[cancel]-8-[confirm]-8-|",
};
};
/// The title of the window, based on the reason the prompt is being shown.
fn titleText(req: apprt.ClipboardRequest) [:0]const u8 {
return switch (req) {
.paste => "Warning: Potentially Unsafe Paste",
.osc_52_read, .osc_52_write => "Authorize Clipboard Access",
};
}
/// The text to display in the prompt window, based on the reason the prompt
/// is being shown.
fn promptText(req: apprt.ClipboardRequest) [:0]const u8 {
return switch (req) {
.paste =>
\\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.
,
.osc_52_read =>
\\An appliclication is attempting to read from the clipboard.
\\The current clipboard contents are shown below.
,
.osc_52_write =>
\\An application is attempting to write to the clipboard.
\\The content to write is shown below.
,
};
}

View File

@ -13,7 +13,7 @@ const CoreSurface = @import("../../Surface.zig");
const App = @import("App.zig");
const Window = @import("Window.zig");
const UnsafePasteWindow = @import("UnsafePasteWindow.zig");
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
const inspector = @import("inspector.zig");
const gtk_key = @import("key.zig");
const c = @import("c.zig");
@ -515,9 +515,22 @@ pub fn setClipboardString(
self: *const Surface,
val: [:0]const u8,
clipboard_type: apprt.Clipboard,
confirm: bool,
) !void {
const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type);
c.gdk_clipboard_set_text(clipboard, val.ptr);
if (!confirm) {
const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type);
c.gdk_clipboard_set_text(clipboard, val.ptr);
return;
}
ClipboardConfirmationWindow.create(
self.app,
val,
self.core_surface,
.{ .osc_52_write = clipboard_type },
) catch |window_err| {
log.err("failed to create clipboard confirmation window err={}", .{window_err});
};
}
const ClipboardRequest = struct {
@ -554,15 +567,17 @@ fn gtkClipboardRead(
str,
false,
) catch |err| switch (err) {
error.UnsafePaste => {
error.UnsafePaste,
error.UnauthorizedPaste,
=> {
// Create a dialog and ask the user if they want to paste anyway.
UnsafePasteWindow.create(
ClipboardConfirmationWindow.create(
self.app,
str,
self.core_surface,
req.state,
) catch |window_err| {
log.err("failed to create unsafe paste window err={}", .{window_err});
log.err("failed to create clipboard confirmation window err={}", .{window_err});
};
return;
},

View File

@ -32,12 +32,21 @@ pub const Clipboard = enum(u1) {
selection = 1, // also known as the "primary" clipboard
};
pub const ClipboardRequestType = enum(u8) {
paste,
osc_52_read,
osc_52_write,
};
/// Clipboard request. This is used to request clipboard contents and must
/// be sent as a response to a ClipboardRequest event.
pub const ClipboardRequest = union(enum) {
pub const ClipboardRequest = union(ClipboardRequestType) {
/// A direct paste of clipboard contents.
paste: void,
/// A request to read clipboard contents via OSC 52.
osc_52_read: Clipboard,
/// A request to write clipboard contents via OSC 52.
osc_52: u8,
osc_52_write: Clipboard,
};

View File

@ -1,3 +1,4 @@
const apprt = @import("../apprt.zig");
const App = @import("../App.zig");
const Surface = @import("../Surface.zig");
const renderer = @import("../renderer.zig");
@ -24,10 +25,13 @@ pub const Message = union(enum) {
cell_size: renderer.CellSize,
/// Read the clipboard and write to the pty.
clipboard_read: u8,
clipboard_read: apprt.Clipboard,
/// Write the clipboard contents.
clipboard_write: WriteReq,
clipboard_write: struct {
clipboard_type: apprt.Clipboard,
req: WriteReq,
},
/// Change the configuration to the given configuration. The pointer is
/// not valid after receiving this message so any config must be used

View File

@ -10,6 +10,7 @@ pub const MouseShiftCapture = Config.MouseShiftCapture;
pub const NonNativeFullscreen = Config.NonNativeFullscreen;
pub const OptionAsAlt = Config.OptionAsAlt;
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
pub const ClipboardAccess = Config.ClipboardAccess;
// Alternate APIs
pub const CAPI = @import("config/CAPI.zig");

View File

@ -456,9 +456,10 @@ keybind: Keybinds = .{},
/// Whether to allow programs running in the terminal to read/write to
/// the system clipboard (OSC 52, for googling). The default is to
/// disallow clipboard reading but allow writing.
@"clipboard-read": bool = false,
@"clipboard-write": bool = true,
/// allow clipboard reading after prompting the user and allow writing
/// unconditionally.
@"clipboard-read": ClipboardAccess = .ask,
@"clipboard-write": ClipboardAccess = .allow,
/// Trims trailing whitespace on data that is copied to the clipboard.
/// 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
/// prevent a "copy/paste attack" where a user may accidentally execute unsafe
/// commands by pasting text with newlines.
///
/// This currently only works on Linux (GTK).
@"clipboard-paste-protection": bool = true,
/// If true, bracketed pastes will be considered safe. By default,
@ -2278,7 +2277,7 @@ pub const ShellIntegrationFeatures = packed struct {
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 {
none,
@"8-bit",
@ -2306,3 +2305,10 @@ pub const MouseShiftCapture = enum {
always,
never,
};
/// How to treat requests to write to or read from the clipboard
pub const ClipboardAccess = enum {
allow,
deny,
ask,
};

View File

@ -2108,20 +2108,29 @@ const StreamHandler = struct {
// iTerm also appears to do this but other terminals seem to only allow
// certain. Let's investigate more.
const clipboard_type: apprt.Clipboard = switch (kind) {
'c' => .standard,
's' => .selection,
else => .standard,
};
// Get clipboard contents
if (data.len == 1 and data[0] == '?') {
_ = self.ev.surface_mailbox.push(.{
.clipboard_read = kind,
.clipboard_read = clipboard_type,
}, .{ .forever = {} });
return;
}
// Write clipboard contents
_ = self.ev.surface_mailbox.push(.{
.clipboard_write = try apprt.surface.Message.WriteReq.init(
self.alloc,
data,
),
.clipboard_write = .{
.req = try apprt.surface.Message.WriteReq.init(
self.alloc,
data,
),
.clipboard_type = clipboard_type,
},
}, .{ .forever = {} });
}