From 86245ff0cf5cea4c2779e21f36897ed1068cd64f Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Thu, 9 Nov 2023 20:45:50 -0600 Subject: [PATCH 1/9] macos: add option to prompt user for confirmation on OSC 52 commands --- include/ghostty.h | 10 ++- macos/Ghostty.xcodeproj/project.pbxproj | 30 ++++---- .../ClipboardConfirmation.xib} | 4 +- .../ClipboardConfirmationController.swift | 49 ++++++++++++ .../ClipboardConfirmationView.swift | 77 +++++++++++++++++++ .../PasteProtectionController.swift | 37 --------- .../PasteProtectionView.swift | 60 --------------- .../Terminal/TerminalController.swift | 70 +++++++++-------- macos/Sources/Ghostty/AppState.swift | 36 ++++++--- macos/Sources/Ghostty/Package.swift | 62 ++++++++++++++- src/Surface.zig | 54 ++++++++----- src/apprt/embedded.zig | 23 +++++- src/apprt/glfw.zig | 2 + src/apprt/gtk/Surface.zig | 3 + src/apprt/structs.zig | 15 ++++ src/config/Config.zig | 18 +++-- 16 files changed, 362 insertions(+), 188 deletions(-) rename macos/Sources/Features/{Paste Protection/PasteProtection.xib => ClipboardConfirmation/ClipboardConfirmation.xib} (83%) create mode 100644 macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift create mode 100644 macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift delete mode 100644 macos/Sources/Features/Paste Protection/PasteProtectionController.swift delete mode 100644 macos/Sources/Features/Paste Protection/PasteProtectionView.swift diff --git a/include/ghostty.h b/include/ghostty.h index f261f1919..c2d7e6990 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -35,6 +35,12 @@ typedef enum { GHOSTTY_CLIPBOARD_SELECTION, } 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 { 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_prompt_reason_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); diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 7c67ecf90..0099a59b0 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -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 = ""; }; A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; - A5E112922AF73E6E00C6E0C2 /* PasteProtection.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PasteProtection.xib; sourceTree = ""; }; - A5E112942AF73E8A00C6E0C2 /* PasteProtectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteProtectionController.swift; sourceTree = ""; }; - A5E112962AF7401B00C6E0C2 /* PasteProtectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteProtectionView.swift; sourceTree = ""; }; + A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = ""; }; + A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = ""; }; + A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; /* 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 = ""; }; - 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 = ""; }; /* 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; }; diff --git a/macos/Sources/Features/Paste Protection/PasteProtection.xib b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmation.xib similarity index 83% rename from macos/Sources/Features/Paste Protection/PasteProtection.xib rename to macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmation.xib index 312a3da5a..32838cefe 100644 --- a/macos/Sources/Features/Paste Protection/PasteProtection.xib +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmation.xib @@ -13,11 +13,11 @@ - + - + diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift new file mode 100644 index 000000000..bd68668a2 --- /dev/null +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift @@ -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 + )) + } +} diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift new file mode 100644 index 000000000..7f7faa8de --- /dev/null +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift @@ -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) + } +} diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionController.swift b/macos/Sources/Features/Paste Protection/PasteProtectionController.swift deleted file mode 100644 index 12514d5b7..000000000 --- a/macos/Sources/Features/Paste Protection/PasteProtectionController.swift +++ /dev/null @@ -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 - )) - } -} diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionView.swift b/macos/Sources/Features/Paste Protection/PasteProtectionView.swift deleted file mode 100644 index aa63ae26f..000000000 --- a/macos/Sources/Features/Paste Protection/PasteProtectionView.swift +++ /dev/null @@ -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) - } -} diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 000cd6e65..fe7069ad4 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -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, _ reason: Ghostty.ClipboardPromptReason) { + // 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 (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 + 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 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... - 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, + reason: reason, state: state, delegate: self ) - window.beginSheet(self.pasteProtection!.window!) + window.beginSheet(self.clipboardConfirmation!.window!) } } diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 0ecfd819e..7cbdac8e5 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -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, reason in AppState.confirmReadClipboard(userdata, string: str, state: state, reason: reason ) }, + 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?, - state: UnsafeMutableRawPointer? + state: UnsafeMutableRawPointer?, + reason: ghostty_clipboard_prompt_reason_e ) { guard let surface = self.surfaceUserdata(from: userdata) 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( - 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.ConfirmClipboardReasonKey: reason, ] ) } @@ -458,14 +461,27 @@ extension Ghostty { } } - static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, location: ghostty_clipboard_e) { + static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, 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) + } 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? { diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 9b532ea3f..1fa17d96a 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -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 { @@ -142,9 +197,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 ConfirmClipboardReasonKey = confirmClipboard.rawValue + ".reason" /// Notification sent to the active split view to resize the split. static let didResizeSplit = Notification.Name("com.mitchellh.ghostty.didResizeSplit") diff --git a/src/Surface.zig b/src/Surface.zig index 86d6310e3..e5e154ecd 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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.Config.ClipboardRequest, + clipboard_write: configpkg.Config.ClipboardRequest, clipboard_trim_trailing_spaces: bool, clipboard_paste_protection: 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), .clipboard_read => |kind| { - if (!self.config.clipboard_read) { - log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{}); + if (self.config.clipboard_read == .deny) { + log.info("application attempted to read clipboard, but 'clipboard-read' is set to deny", .{}); 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 { - 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,8 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) }; 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}); return; }; @@ -901,7 +902,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 +2280,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 +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 /// 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 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( self: *Surface, req: apprt.ClipboardRequest, data: []const u8, - allow_unsafe: bool, + 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 => |kind| try self.completeClipboardReadOSC52(data, kind, confirmed), } } @@ -2534,9 +2541,9 @@ fn startClipboardRequest( ) !void { switch (req) { .paste => {}, // always allowed - .osc_52 => if (!self.config.clipboard_read) { + .osc_52 => 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; @@ -2635,7 +2642,16 @@ 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, 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 // 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. diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index d3950d65e..7b9d2f6f4 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -17,6 +17,7 @@ const CoreInspector = @import("../inspector/main.zig").Inspector; const CoreSurface = @import("../Surface.zig"); const configpkg = @import("../config.zig"); const Config = configpkg.Config; +const ClipboardPromptReason = @import("../apprt/structs.zig").ClipboardPromptReason; const log = std.log.scoped(.embedded_window); @@ -68,10 +69,11 @@ pub const App = struct { SurfaceUD, [*:0]const u8, *apprt.ClipboardRequest, + ClipboardPromptReason, ) 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,7 +508,7 @@ 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.*, @@ -518,6 +520,17 @@ pub const Surface = struct { self.opts.userdata, str.ptr, state, + .unsafe, + ); + + return; + }, + error.UnauthorizedPaste => { + self.app.opts.confirm_read_clipboard( + self.opts.userdata, + str.ptr, + state, + .read, ); return; @@ -526,8 +539,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 +548,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, ); } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 7dadcab55..9f1bb5a4d 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -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), diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 6fa58825c..0e967771e 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -515,7 +515,10 @@ pub fn setClipboardString( self: *const Surface, val: [:0]const u8, clipboard_type: apprt.Clipboard, + confirm: bool, ) !void { + // TODO: implement confirmation dialog when clipboard-write is "ask" + _ = confirm; const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type); c.gdk_clipboard_set_text(clipboard, val.ptr); } diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 93a2b7cbb..013441857 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -41,3 +41,18 @@ pub const ClipboardRequest = union(enum) { /// A request to write clipboard contents via OSC 52. 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, + + _, +}; diff --git a/src/config/Config.zig b/src/config/Config.zig index cdf7d5cdc..f41affbea 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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": ClipboardRequest = .ask, +@"clipboard-write": ClipboardRequest = .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 ClipboardRequest = enum { + allow, + deny, + ask, +}; From 960a1bb091cc2081e25eb076fe2ad505873b88dc Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Fri, 10 Nov 2023 12:04:53 -0600 Subject: [PATCH 2/9] gtk: implement OSC 52 prompts --- src/Surface.zig | 44 +++++--- src/apprt/glfw.zig | 5 +- src/apprt/gtk/App.zig | 6 +- ...ow.zig => ClipboardConfirmationWindow.zig} | 101 +++++++++++++----- src/apprt/gtk/Surface.zig | 27 +++-- src/apprt/structs.zig | 5 +- src/apprt/surface.zig | 8 +- src/termio/Exec.zig | 19 +++- 8 files changed, 151 insertions(+), 64 deletions(-) rename src/apprt/gtk/{UnsafePasteWindow.zig => ClipboardConfirmationWindow.zig} (63%) diff --git a/src/Surface.zig b/src/Surface.zig index e5e154ecd..01e70d3bf 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -688,21 +688,21 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .cell_size => |size| try self.setCellSize(size), - .clipboard_read => |kind| { + .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); }, }, @@ -856,6 +856,9 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) }; assert(buf[buf.len] == 0); + // 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}); @@ -2516,19 +2519,21 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool /// data is defined as data that contains newlines, though this definition /// may change later to detect other scenarios. /// -/// - For OSC 52 pastes no prompt is shown to the user if `confirmed` is true. +/// - For OSC 52 reads and writes 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. +/// 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, + data: [:0]const u8, confirmed: bool, ) !void { switch (req) { .paste => try self.completeClipboardPaste(data, confirmed), - .osc_52 => |kind| try self.completeClipboardReadOSC52(data, kind, confirmed), + .osc_52_read => |clipboard| try self.completeClipboardReadOSC52(data, clipboard, confirmed), + .osc_52_write => |clipboard| try self.rt_surface.setClipboardString(data, clipboard, !confirmed), } } @@ -2541,13 +2546,16 @@ fn startClipboardRequest( ) !void { switch (req) { .paste => {}, // always allowed - .osc_52 => if (self.config.clipboard_read == .deny) { + .osc_52_read => if (self.config.clipboard_read == .deny) { log.info( "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); @@ -2642,7 +2650,12 @@ fn completeClipboardPaste( try self.io_thread.wakeup.notify(); } -fn completeClipboardReadOSC52(self: *Surface, data: []const u8, kind: u8, confirmed: bool) !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); @@ -2660,6 +2673,11 @@ fn completeClipboardReadOSC52(self: *Surface, data: []const u8, kind: u8, confir 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); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 9f1bb5a4d..7bf5a9e98 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -656,15 +656,14 @@ 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 if (comptime builtin.os.tag != .linux) break :selection ""; - const raw = glfwNative.getX11SelectionString() orelse + break :selection glfwNative.getX11SelectionString() orelse return glfw.mustGetErrorCode(); - break :selection std.mem.span(raw); }, }; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 2456d2ada..b70cae258 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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, diff --git a/src/apprt/gtk/UnsafePasteWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig similarity index 63% rename from src/apprt/gtk/UnsafePasteWindow.zig rename to src/apprt/gtk/ClipboardConfirmationWindow.zig index e3582e892..321a51128 100644 --- a/src/apprt/gtk/UnsafePasteWindow.zig +++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig @@ -1,11 +1,12 @@ -/// Unsafe Paste Window -const UnsafePaste = @This(); +/// Clipboard Confirmation Window +const ClipboardConfirmation = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; const CoreSurface = @import("../../Surface.zig"); const ClipboardRequest = @import("../structs.zig").ClipboardRequest; +const ClipboardPromptReason = @import("../structs.zig").ClipboardPromptReason; const App = @import("App.zig"); const View = @import("View.zig"); const c = @import("c.zig"); @@ -16,9 +17,10 @@ app: *App, window: *c.GtkWindow, view: PrimaryView, -data: []u8, +data: [:0]u8, core_surface: CoreSurface, pending_req: ClipboardRequest, +reason: ClipboardPromptReason, pub fn create( app: *App, @@ -26,40 +28,49 @@ pub fn create( core_surface: CoreSurface, request: 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); + + const reason: ClipboardPromptReason = switch (request) { + .paste => .unsafe, + .osc_52_read => .read, + .osc_52_write => .write, + }; + try self.init( app, data, core_surface, request, + reason, ); - 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.destroy(self); } fn init( - self: *UnsafePaste, + self: *ClipboardConfirmation, app: *App, data: []const u8, core_surface: CoreSurface, request: ClipboardRequest, + reason: ClipboardPromptReason, ) !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(reason)); c.gtk_window_set_default_size(gtk_window, 550, 275); c.gtk_window_set_resizable(gtk_window, 0); _ = c.g_signal_connect_data( @@ -76,9 +87,10 @@ 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, + .reason = reason, }; // Show the window @@ -93,7 +105,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 +113,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.reason)); const buf = unsafeBuffer(data); defer c.g_object_unref(buf); const buttons = try ButtonsView.init(root); @@ -157,20 +166,26 @@ 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.reason) { + .unsafe => .{ "Cancel", "Paste" }, + .read, .write => .{ "Deny", "Allow" }, + _ => unreachable, + }; + + 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 +198,9 @@ const ButtonsView = struct { c.G_CONNECT_DEFAULT, ); _ = c.g_signal_connect_data( - paste_button, + confirm_button, "clicked", - c.G_CALLBACK(>kPasteClick), + c.G_CALLBACK(>kConfirmClick), root, null, c.G_CONNECT_DEFAULT, @@ -195,13 +210,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 +229,34 @@ 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(reason: ClipboardPromptReason) [:0]const u8 { + return switch (reason) { + .unsafe => "Warning: Potentially Unsafe Paste", + .read, .write => "Authorize Clipboard Access", + _ => unreachable, + }; +} + +/// The text to display in the prompt window, based on the reason the prompt +/// is being shown. +fn promptText(reason: ClipboardPromptReason) [:0]const u8 { + return switch (reason) { + .unsafe => + \\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed. + , + .read => + \\An appliclication is attempting to read from the clipboard. + \\The current clipboard contents are shown below. + , + .write => + \\An application is attempting to write to the clipboard. + \\The content to write is shown below. + , + _ => unreachable, + }; +} diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 0e967771e..9cc253b63 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -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"); @@ -517,10 +517,19 @@ pub fn setClipboardString( clipboard_type: apprt.Clipboard, confirm: bool, ) !void { - // TODO: implement confirmation dialog when clipboard-write is "ask" - _ = confirm; - 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); + } else { + 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 { @@ -557,15 +566,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; }, diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 013441857..438a96376 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -38,8 +38,11 @@ pub const ClipboardRequest = union(enum) { /// 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, }; /// The reason for displaying a clipboard prompt to the user diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 58e3cea9b..b86fbf54b 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -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 diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 7a409e8f7..670848cea 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -2094,20 +2094,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 = {} }); } From 9d3385703d00e085a05a917c77174f74dc85b852 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Fri, 10 Nov 2023 12:25:55 -0600 Subject: [PATCH 3/9] gtk: fix memory leak in GTK clipboard confirmation window --- src/apprt/gtk/ClipboardConfirmationWindow.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig index 321a51128..30f231093 100644 --- a/src/apprt/gtk/ClipboardConfirmationWindow.zig +++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig @@ -55,6 +55,7 @@ pub fn create( fn destroy(self: *ClipboardConfirmation) void { const alloc = self.app.core_app.alloc; self.app.clipboard_confirmation_window = null; + alloc.free(self.data); alloc.destroy(self); } From 2a64180ebd228183e22ccd0cdf3f0eebed8a5be6 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Fri, 10 Nov 2023 16:34:04 -0600 Subject: [PATCH 4/9] config: rename ClipboardRequest to ClipboardAccess --- src/Surface.zig | 4 ++-- src/config/Config.zig | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 01e70d3bf..b2743921a 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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: configpkg.Config.ClipboardRequest, - clipboard_write: configpkg.Config.ClipboardRequest, + clipboard_read: configpkg.Config.ClipboardAccess, + clipboard_write: configpkg.Config.ClipboardAccess, clipboard_trim_trailing_spaces: bool, clipboard_paste_protection: bool, clipboard_paste_bracketed_safe: bool, diff --git a/src/config/Config.zig b/src/config/Config.zig index f41affbea..5b84414f3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -458,8 +458,8 @@ keybind: Keybinds = .{}, /// the system clipboard (OSC 52, for googling). The default is to /// allow clipboard reading after prompting the user and allow writing /// unconditionally. -@"clipboard-read": ClipboardRequest = .ask, -@"clipboard-write": ClipboardRequest = .allow, +@"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". @@ -2307,7 +2307,7 @@ pub const MouseShiftCapture = enum { }; /// How to treat requests to write to or read from the clipboard -pub const ClipboardRequest = enum { +pub const ClipboardAccess = enum { allow, deny, ask, From 98b43007a025ebd2c86631da2b1a0694fcf1c6af Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Fri, 10 Nov 2023 18:06:53 -0600 Subject: [PATCH 5/9] core: use ClipboardRequestType instead of ClipboardPromptReason Instead of making a separate enum that must be translated from the ClipboardRequest type, simply re-use ClipboardRequest to determine the clipboard confirmation reason. --- include/ghostty.h | 10 ++-- .../ClipboardConfirmationController.swift | 14 ++--- .../ClipboardConfirmationView.swift | 38 +++++++------- .../Terminal/TerminalController.swift | 12 ++--- macos/Sources/Ghostty/AppState.swift | 10 ++-- macos/Sources/Ghostty/Package.swift | 52 ++++++++----------- src/apprt/embedded.zig | 21 +++----- src/apprt/gtk/ClipboardConfirmationWindow.zig | 50 +++++++----------- src/apprt/structs.zig | 23 +++----- 9 files changed, 94 insertions(+), 136 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index c2d7e6990..59ceef503 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -36,10 +36,10 @@ typedef enum { } 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; + 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, @@ -346,7 +346,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_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 *, ghostty_clipboard_prompt_reason_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); diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift index bd68668a2..2040dcfae 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift @@ -11,14 +11,14 @@ class ClipboardConfirmationController: NSWindowController { let surface: ghostty_surface_t let contents: String - let reason: Ghostty.ClipboardPromptReason + let request: Ghostty.ClipboardRequest let state: UnsafeMutableRawPointer? weak private var delegate: ClipboardConfirmationViewDelegate? = nil - init(surface: ghostty_surface_t, contents: String, reason: Ghostty.ClipboardPromptReason, state: UnsafeMutableRawPointer?, delegate: ClipboardConfirmationViewDelegate) { + init(surface: ghostty_surface_t, contents: String, request: Ghostty.ClipboardRequest, state: UnsafeMutableRawPointer?, delegate: ClipboardConfirmationViewDelegate) { self.surface = surface self.contents = contents - self.reason = reason + self.request = request self.state = state self.delegate = delegate super.init(window: nil) @@ -33,16 +33,16 @@ class ClipboardConfirmationController: NSWindowController { override func windowDidLoad() { guard let window = window else { return } - switch (reason) { - case .unsafe: + switch (request) { + case .paste: window.title = "Warning: Potentially Unsafe Paste" - case .read, .write: + case .osc_52_read, .osc_52_write: window.title = "Authorize Clipboard Access" } window.contentView = NSHostingView(rootView: ClipboardConfirmationView( contents: contents, - reason: reason, + request: request, delegate: delegate )) } diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift index 7f7faa8de..239b493c2 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift @@ -2,7 +2,7 @@ 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) + func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) } /// The SwiftUI view for showing a clipboard confirmation dialog. @@ -11,18 +11,16 @@ struct ClipboardConfirmationView: View { 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" - } + 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" } } } @@ -30,8 +28,8 @@ struct ClipboardConfirmationView: View { /// The contents of the paste. let contents: String - /// The reason for displaying the view - let reason: Ghostty.ClipboardPromptReason + /// 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 @@ -45,7 +43,7 @@ struct ClipboardConfirmationView: View { .padding() .frame(alignment: .center) - Text(reason.text()) + Text(request.text()) .frame(maxWidth: .infinity, alignment: .leading) .padding() } @@ -57,9 +55,9 @@ struct ClipboardConfirmationView: View { HStack { Spacer() - Button(Action.text(.cancel, reason)) { onCancel() } + Button(Action.text(.cancel, request)) { onCancel() } .keyboardShortcut(.cancelAction) - Button(Action.text(.confirm, reason)) { onPaste() } + Button(Action.text(.confirm, request)) { onPaste() } .keyboardShortcut(.defaultAction) Spacer() } @@ -68,10 +66,10 @@ struct ClipboardConfirmationView: View { } private func onCancel() { - delegate?.clipboardConfirmationComplete(.cancel, reason) + delegate?.clipboardConfirmationComplete(.cancel, request) } private func onPaste() { - delegate?.clipboardConfirmationComplete(.confirm, reason) + delegate?.clipboardConfirmationComplete(.confirm, request) } } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index fe7069ad4..61853fa71 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -348,7 +348,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, //MARK: - Clipboard Confirmation - func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ reason: Ghostty.ClipboardPromptReason) { + 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 @@ -358,13 +358,13 @@ class TerminalController: NSWindowController, NSWindowDelegate, window?.endSheet(ccWindow) } - switch (reason) { - case .write: + 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 .read, .unsafe: + case .osc_52_read, .paste: let str: String switch (action) { case .cancel: @@ -448,7 +448,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, // Check whether we use non-native fullscreen 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 reason = notification.userInfo?[Ghostty.Notification.ConfirmClipboardReasonKey] as? Ghostty.ClipboardPromptReason else { return } + guard let request = notification.userInfo?[Ghostty.Notification.ConfirmClipboardRequestKey] as? Ghostty.ClipboardRequest else { return } // If we already have a clipboard confirmation view up, we ignore this request. // This shouldn't be possible... @@ -461,7 +461,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, self.clipboardConfirmation = ClipboardConfirmationController( surface: surface, contents: str, - reason: reason, + request: request, state: state, delegate: self ) diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 7cbdac8e5..f7c3d3bff 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -150,7 +150,7 @@ 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, reason in AppState.confirmReadClipboard(userdata, string: str, state: state, reason: reason ) }, + 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) }, @@ -434,18 +434,18 @@ extension Ghostty { _ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, state: UnsafeMutableRawPointer?, - reason: ghostty_clipboard_prompt_reason_e + 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 reason = Ghostty.ClipboardPromptReason.from(reason: reason) else { return } + guard let request = Ghostty.ClipboardRequest.from(request: request) else { return } NotificationCenter.default.post( name: Notification.confirmClipboard, object: surface, userInfo: [ Notification.ConfirmClipboardStrKey: valueStr, Notification.ConfirmClipboardStateKey: state as Any, - Notification.ConfirmClipboardReasonKey: reason, + Notification.ConfirmClipboardRequestKey: request, ] ) } @@ -478,7 +478,7 @@ extension Ghostty { object: surface, userInfo: [ Notification.ConfirmClipboardStrKey: valueStr, - Notification.ConfirmClipboardReasonKey: Ghostty.ClipboardPromptReason.write, + Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write, ] ) } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 1fa17d96a..49ab84ab6 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -95,29 +95,30 @@ 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 + /// 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 - case read + /// 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 - case write + /// 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 .unsafe: + case .paste: return """ Pasting this text to the terminal may be dangerous as it looks like some commands may be executed. """ - case .read: + case .osc_52_read: return """ An application is attempting to read from the clipboard. The current clipboard contents are shown below. """ - case .write: + case .osc_52_write: return """ An application is attempting to write to the clipboard. The content to write is shown below. @@ -125,29 +126,18 @@ extension Ghostty { } } - 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 + 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 } } - - 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 - } - } } } @@ -200,7 +190,7 @@ extension Ghostty.Notification { static let confirmClipboard = Notification.Name("com.mitchellh.ghostty.confirmClipboard") static let ConfirmClipboardStrKey = confirmClipboard.rawValue + ".str" static let ConfirmClipboardStateKey = confirmClipboard.rawValue + ".state" - static let ConfirmClipboardReasonKey = confirmClipboard.rawValue + ".reason" + 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") diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 7b9d2f6f4..60faae7a8 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -17,7 +17,6 @@ const CoreInspector = @import("../inspector/main.zig").Inspector; const CoreSurface = @import("../Surface.zig"); const configpkg = @import("../config.zig"); const Config = configpkg.Config; -const ClipboardPromptReason = @import("../apprt/structs.zig").ClipboardPromptReason; const log = std.log.scoped(.embedded_window); @@ -69,7 +68,7 @@ pub const App = struct { SurfaceUD, [*:0]const u8, *apprt.ClipboardRequest, - ClipboardPromptReason, + apprt.ClipboardRequestType, ) callconv(.C) void, /// Write the clipboard value. @@ -508,6 +507,8 @@ pub const Surface = struct { ) void { const alloc = self.app.core_app.alloc; + const request_type = @as(apprt.ClipboardRequestType, state.*); + // Attempt to complete the request, but we may request // confirmation. self.core_surface.completeClipboardRequest( @@ -515,22 +516,14 @@ pub const Surface = struct { str, confirmed, ) catch |err| switch (err) { - error.UnsafePaste => { + error.UnsafePaste, + error.UnauthorizedPaste, + => { self.app.opts.confirm_read_clipboard( self.opts.userdata, str.ptr, state, - .unsafe, - ); - - return; - }, - error.UnauthorizedPaste => { - self.app.opts.confirm_read_clipboard( - self.opts.userdata, - str.ptr, - state, - .read, + request_type, ); return; diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig index 30f231093..e4ad080e3 100644 --- a/src/apprt/gtk/ClipboardConfirmationWindow.zig +++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig @@ -4,9 +4,8 @@ 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 ClipboardPromptReason = @import("../structs.zig").ClipboardPromptReason; const App = @import("App.zig"); const View = @import("View.zig"); const c = @import("c.zig"); @@ -19,14 +18,13 @@ view: PrimaryView, data: [:0]u8, core_surface: CoreSurface, -pending_req: ClipboardRequest, -reason: ClipboardPromptReason, +pending_req: apprt.ClipboardRequest, pub fn create( app: *App, data: []const u8, core_surface: CoreSurface, - request: ClipboardRequest, + request: apprt.ClipboardRequest, ) !void { if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists; @@ -34,18 +32,11 @@ pub fn create( const self = try alloc.create(ClipboardConfirmation); errdefer alloc.destroy(self); - const reason: ClipboardPromptReason = switch (request) { - .paste => .unsafe, - .osc_52_read => .read, - .osc_52_write => .write, - }; - try self.init( app, data, core_surface, request, - reason, ); app.clipboard_confirmation_window = self; @@ -64,14 +55,13 @@ fn init( app: *App, data: []const u8, core_surface: CoreSurface, - request: ClipboardRequest, - reason: ClipboardPromptReason, + 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, titleText(reason)); + 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( @@ -91,7 +81,6 @@ fn init( .data = try app.core_app.alloc.dupeZ(u8, data), .core_surface = core_surface, .pending_req = request, - .reason = reason, }; // Show the window @@ -116,7 +105,7 @@ const PrimaryView = struct { pub fn init(root: *ClipboardConfirmation, data: []const u8) !PrimaryView { // All our widgets - const label = c.gtk_label_new(promptText(root.reason)); + 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); @@ -168,10 +157,9 @@ const ButtonsView = struct { root: *c.GtkWidget, pub fn init(root: *ClipboardConfirmation) !ButtonsView { - const cancel_text, const confirm_text = switch (root.reason) { - .unsafe => .{ "Cancel", "Paste" }, - .read, .write => .{ "Deny", "Allow" }, - _ => unreachable, + 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); @@ -235,29 +223,27 @@ const ButtonsView = struct { }; /// The title of the window, based on the reason the prompt is being shown. -fn titleText(reason: ClipboardPromptReason) [:0]const u8 { - return switch (reason) { - .unsafe => "Warning: Potentially Unsafe Paste", - .read, .write => "Authorize Clipboard Access", - _ => unreachable, +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(reason: ClipboardPromptReason) [:0]const u8 { - return switch (reason) { - .unsafe => +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. , - .read => + .osc_52_read => \\An appliclication is attempting to read from the clipboard. \\The current clipboard contents are shown below. , - .write => + .osc_52_write => \\An application is attempting to write to the clipboard. \\The content to write is shown below. , - _ => unreachable, }; } diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 438a96376..d452bc8eb 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -32,9 +32,15 @@ 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, @@ -44,18 +50,3 @@ pub const ClipboardRequest = union(enum) { /// A request to write clipboard contents via OSC 52. osc_52_write: Clipboard, }; - -/// 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, - - _, -}; From 593cfa256c4026030d596256fc8de9fa7b2431b6 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Sat, 11 Nov 2023 17:21:10 -0500 Subject: [PATCH 6/9] glfw: fix compile error --- src/apprt/glfw.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 7bf5a9e98..a4178dfb5 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -662,8 +662,9 @@ pub const Surface = struct { // Not supported except on Linux if (comptime builtin.os.tag != .linux) break :selection ""; - break :selection glfwNative.getX11SelectionString() orelse + const raw = glfwNative.getX11SelectionString() orelse return glfw.mustGetErrorCode(); + break :selection std.mem.span(raw); }, }; From 06cdbc1a96b4737a368d0328e1422f39a75c16e4 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Sat, 11 Nov 2023 17:25:48 -0500 Subject: [PATCH 7/9] config: export ClipboardAccess --- src/Surface.zig | 4 ++-- src/config.zig | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index b2743921a..7ce729a97 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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: configpkg.Config.ClipboardAccess, - clipboard_write: configpkg.Config.ClipboardAccess, + clipboard_read: configpkg.ClipboardAccess, + clipboard_write: configpkg.ClipboardAccess, clipboard_trim_trailing_spaces: bool, clipboard_paste_protection: bool, clipboard_paste_bracketed_safe: bool, diff --git a/src/config.zig b/src/config.zig index 4fda2e5b5..6834291e7 100644 --- a/src/config.zig +++ b/src/config.zig @@ -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"); From e7bc9958daa13d5aa8e0b0686870daad2ae5e182 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Sat, 11 Nov 2023 17:26:59 -0500 Subject: [PATCH 8/9] Prefer explicit type syntax over @as --- src/apprt/embedded.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 60faae7a8..7d239e99f 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -507,7 +507,7 @@ pub const Surface = struct { ) void { const alloc = self.app.core_app.alloc; - const request_type = @as(apprt.ClipboardRequestType, state.*); + const request_type: apprt.ClipboardRequestType = state.*; // Attempt to complete the request, but we may request // confirmation. From 2489ef4c13e8e0a8e3d724cf942e86a1329d1061 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 11 Nov 2023 15:15:52 -0800 Subject: [PATCH 9/9] stylistic tweaks --- macos/Sources/Ghostty/AppState.swift | 19 ++++++++++--------- src/Surface.zig | 14 ++++++++++++-- src/apprt/embedded.zig | 4 +--- src/apprt/gtk/Surface.zig | 19 ++++++++++--------- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index f7c3d3bff..0b61fbcb6 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -472,16 +472,17 @@ extension Ghostty { let pb = NSPasteboard.general pb.declareTypes([.string], owner: nil) pb.setString(valueStr, forType: .string) - } else { - NotificationCenter.default.post( - name: Notification.confirmClipboard, - object: surface, - userInfo: [ - Notification.ConfirmClipboardStrKey: valueStr, - Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write, - ] - ) + 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? { diff --git a/src/Surface.zig b/src/Surface.zig index 7ce729a97..38d1c6bcb 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2532,8 +2532,18 @@ pub fn completeClipboardRequest( ) !void { switch (req) { .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), + + .osc_52_read => |clipboard| try self.completeClipboardReadOSC52( + data, + clipboard, + confirmed, + ), + + .osc_52_write => |clipboard| try self.rt_surface.setClipboardString( + data, + clipboard, + !confirmed, + ), } } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 7d239e99f..2e417a92e 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -507,8 +507,6 @@ pub const Surface = struct { ) void { const alloc = self.app.core_app.alloc; - const request_type: apprt.ClipboardRequestType = state.*; - // Attempt to complete the request, but we may request // confirmation. self.core_surface.completeClipboardRequest( @@ -523,7 +521,7 @@ pub const Surface = struct { self.opts.userdata, str.ptr, state, - request_type, + state.*, ); return; diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 9cc253b63..ec7e99196 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -520,16 +520,17 @@ pub fn setClipboardString( if (!confirm) { const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type); c.gdk_clipboard_set_text(clipboard, val.ptr); - } else { - 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}); - }; + 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 {