From ef445515222a046416e459594bd23f9a7bb2a558 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 09:20:16 -0800 Subject: [PATCH] apprt/embedded: hook up paste confirmation --- include/ghostty.h | 4 +- .../PasteProtectionController.swift | 10 ++- .../PasteProtectionView.swift | 1 - .../Terminal/TerminalController.swift | 64 +++++++++++++++--- macos/Sources/Ghostty/AppState.swift | 27 +++++++- macos/Sources/Ghostty/Package.swift | 4 ++ src/Surface.zig | 2 +- src/apprt/embedded.zig | 67 +++++++++++++++---- 8 files changed, 149 insertions(+), 30 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 001ffb249..bbb17eed1 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -325,6 +325,7 @@ typedef void (*ghostty_runtime_set_title_cb)(void *, const char *); typedef void (*ghostty_runtime_set_mouse_shape_cb)(void *, ghostty_mouse_shape_e); typedef void (*ghostty_runtime_set_mouse_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_new_split_cb)(void *, ghostty_split_direction_e, ghostty_surface_config_s); typedef void (*ghostty_runtime_new_tab_cb)(void *, ghostty_surface_config_s); @@ -348,6 +349,7 @@ typedef struct { ghostty_runtime_set_mouse_shape_cb set_mouse_shape_cb; ghostty_runtime_set_mouse_visibility_cb set_mouse_visibility_cb; ghostty_runtime_read_clipboard_cb read_clipboard_cb; + ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb; ghostty_runtime_write_clipboard_cb write_clipboard_cb; ghostty_runtime_new_split_cb new_split_cb; ghostty_runtime_new_tab_cb new_tab_cb; @@ -411,7 +413,7 @@ void ghostty_surface_request_close(ghostty_surface_t); void ghostty_surface_split(ghostty_surface_t, ghostty_split_direction_e); void ghostty_surface_split_focus(ghostty_surface_t, ghostty_split_focus_direction_e); bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t); -void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, uintptr_t, void *); +void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, void *, bool); ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); void ghostty_inspector_free(ghostty_surface_t); diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionController.swift b/macos/Sources/Features/Paste Protection/PasteProtectionController.swift index 5da7b174f..35087f428 100644 --- a/macos/Sources/Features/Paste Protection/PasteProtectionController.swift +++ b/macos/Sources/Features/Paste Protection/PasteProtectionController.swift @@ -6,9 +6,15 @@ import GhosttyKit 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(delegate: PasteProtectionViewDelegate) { + 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) } @@ -22,7 +28,7 @@ class PasteProtectionController: NSWindowController { override func windowDidLoad() { guard let window = window else { return } window.contentView = NSHostingView(rootView: PasteProtectionView( - contents: "Hello\nWorld", + contents: contents, delegate: delegate )) } diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionView.swift b/macos/Sources/Features/Paste Protection/PasteProtectionView.swift index 5a4e1c348..ef0aff6f0 100644 --- a/macos/Sources/Features/Paste Protection/PasteProtectionView.swift +++ b/macos/Sources/Features/Paste Protection/PasteProtectionView.swift @@ -46,7 +46,6 @@ struct PasteProtectionView: View { } private func onCancel() { - AppDelegate.logger.warning("PASTE onCancel") delegate?.pasteProtectionComplete(.cancel) } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index b277a1f6d..9897aeda5 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -32,7 +32,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, private var alert: NSAlert? = nil /// The paste protection window, if shown. - private var pasteProtection: NSWindow? = nil + private var pasteProtection: PasteProtectionController? = nil init(_ ghostty: Ghostty.AppState, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) { self.ghostty = ghostty @@ -54,6 +54,11 @@ class TerminalController: NSWindowController, NSWindowDelegate, selector: #selector(onGotoTab), name: Ghostty.Notification.ghosttyGotoTab, object: nil) + center.addObserver( + self, + selector: #selector(onConfirmUnsafePaste), + name: Ghostty.Notification.confirmUnsafePaste, + object: nil) } required init?(coder: NSCoder) { @@ -121,11 +126,6 @@ class TerminalController: NSWindowController, NSWindowDelegate, viewModel: self, delegate: self )) - - // TODO: remove this, just for dev - let pp = PasteProtectionController(delegate: self) - self.pasteProtection = pp.window - window.beginSheet(pp.window!) } // Shows the "+" button in the tab bar, responds to that click. @@ -320,15 +320,28 @@ class TerminalController: NSWindowController, NSWindowDelegate, self.window?.close() } - //MARK: - PasteProtectionViewDelegate + //MARK: - Paste Protection func pasteProtectionComplete(_ action: PasteProtectionView.Action) { - if let pasteWindow = self.pasteProtection { - window?.endSheet(pasteWindow) - self.pasteProtection = nil + // End our paste protection no matter what + guard let pp = self.pasteProtection else { return } + self.pasteProtection = nil + + // Close the sheet + if let ppWindow = pp.window { + window?.endSheet(ppWindow) } - AppDelegate.logger.warning("PASTE action=\(action.rawValue)") + let str: String + switch (action) { + case .cancel: + str = "" + + case .paste: + str = pp.contents + } + + Ghostty.AppState.completeClipboardRequest(pp.surface, data: str, state: pp.state, confirmed: true) } //MARK: - Notifications @@ -389,4 +402,33 @@ class TerminalController: NSWindowController, NSWindowDelegate, Ghostty.moveFocus(to: focusedSurface) } } + + @objc private func onConfirmUnsafePaste(notification: SwiftUI.Notification) { + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard target == self.focusedSurface else { return } + guard let surface = target.surface else { return } + + // We need a window + guard let window = self.window else { return } + + // Check whether we use non-native fullscreen + guard let str = notification.userInfo?[Ghostty.Notification.UnsafePasteStrKey] as? String else { return } + guard let state = notification.userInfo?[Ghostty.Notification.UnsafePasteStateKey] as? UnsafeMutableRawPointer? else { return } + + // If we already have a paste protection view up, we ignore this request. + // This shouldn't be possible... + guard self.pasteProtection == nil else { + Ghostty.AppState.completeClipboardRequest(surface, data: "", state: state, confirmed: true) + return + } + + // Show our paste confirmation + self.pasteProtection = PasteProtectionController( + surface: surface, + contents: str, + state: state, + delegate: self + ) + window.beginSheet(self.pasteProtection!.window!) + } } diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 4f637e3a9..1cac36ae2 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -141,6 +141,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 in AppState.confirmReadClipboard(userdata, string: str, state: state ) }, write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) }, new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) }, new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) }, @@ -390,9 +391,31 @@ extension Ghostty { completeClipboardRequest(surface, data: str, state: state) } - static private func completeClipboardRequest(_ surface: ghostty_surface_t, data: String, state: UnsafeMutableRawPointer?) { + static func confirmReadClipboard( + _ userdata: UnsafeMutableRawPointer?, + string: UnsafePointer?, + state: UnsafeMutableRawPointer? + ) { + guard let surface = self.surfaceUserdata(from: userdata) else { return } + guard let valueStr = String(cString: string!, encoding: .utf8) else { return } + NotificationCenter.default.post( + name: Notification.confirmUnsafePaste, + object: surface, + userInfo: [ + Notification.UnsafePasteStrKey: valueStr, + Notification.UnsafePasteStateKey: state as Any + ] + ) + } + + static func completeClipboardRequest( + _ surface: ghostty_surface_t, + data: String, + state: UnsafeMutableRawPointer?, + confirmed: Bool = false + ) { data.withCString { ptr in - ghostty_surface_complete_clipboard_request(surface, ptr, UInt(data.utf8.count), state) + ghostty_surface_complete_clipboard_request(surface, ptr, state, confirmed) } } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index bd8db5f12..51ba1e70d 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -108,6 +108,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" } // Make the input enum hashable. diff --git a/src/Surface.zig b/src/Surface.zig index 0e4bb47a4..46780be00 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2514,7 +2514,7 @@ fn completeClipboardPaste( // // We do not do this for bracketed pastes because bracketed pastes are // by definition safe since they're framed. - if (!bracketed and + if ((true or !bracketed) and self.config.clipboard_paste_protection and !allow_unsafe and !terminal.isSafePaste(data)) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index b7ae1c6ee..953da5486 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -61,6 +61,15 @@ pub const App = struct { /// value then this should return null. read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.C) void, + /// This may be called after a read clipboard call to request + /// confirmation that the clipboard value is safe to read. The embedder + /// must call complete_clipboard_request with the given request. + confirm_read_clipboard: *const fn ( + SurfaceUD, + [*:0]const u8, + *apprt.ClipboardRequest, + ) callconv(.C) void, + /// Write the clipboard value. write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int) callconv(.C) void, @@ -451,6 +460,45 @@ pub const Surface = struct { ); } + fn completeClipboardRequest( + self: *Surface, + str: [:0]const u8, + state: *apprt.ClipboardRequest, + confirmed: bool, + ) void { + const alloc = self.app.core_app.alloc; + + // If there is no string, then we don't do anything and complete. + if (str.len == 0) { + alloc.destroy(state); + return; + } + + // Attempt to complete the request, but if its unsafe we may request + // confirmation. + self.core_surface.completeClipboardRequest( + state.*, + str, + confirmed, + ) catch |err| switch (err) { + error.UnsafePaste => { + self.app.opts.confirm_read_clipboard( + self.opts.userdata, + str.ptr, + state, + ); + + return; + }, + + else => log.err("error completing clipboard request err={}", .{err}), + }; + + // We don't defer this because the unsafe paste route preserves + // the clipboard request. + alloc.destroy(state); + } + pub fn setClipboardString( self: *const Surface, val: [:0]const u8, @@ -1351,20 +1399,15 @@ pub const CAPI = struct { /// with a request the request pointer will be invalidated. export fn ghostty_surface_complete_clipboard_request( ptr: *Surface, - str_ptr: [*]const u8, - str_len: usize, + str: [*:0]const u8, state: *apprt.ClipboardRequest, + confirmed: bool, ) void { - // The state is unusable after this - defer ptr.core_surface.app.alloc.destroy(state); - - if (str_len == 0) return; - const str = str_ptr[0..str_len]; - // TODO: Support sanaization for MacOS (force: false) - ptr.core_surface.completeClipboardRequest(state.*, str, true) catch |err| { - log.err("error completing clipboard request err={}", .{err}); - return; - }; + ptr.completeClipboardRequest( + std.mem.sliceTo(str, 0), + state, + confirmed, + ); } export fn ghostty_surface_inspector(ptr: *Surface) ?*Inspector {