From 5f5f5a44ec495207a94652c81f99100d6f5e13e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Nov 2023 20:11:09 -0700 Subject: [PATCH 1/7] macos: paste protection boilerplate --- macos/Ghostty.xcodeproj/project.pbxproj | 20 +++++++++++++ .../Paste Protection/PasteProtection.xib | 28 +++++++++++++++++++ .../PasteProtectionController.swift | 15 ++++++++++ .../PasteProtectionView.swift | 18 ++++++++++++ .../Terminal/TerminalController.swift | 4 +++ 5 files changed, 85 insertions(+) create mode 100644 macos/Sources/Features/Paste Protection/PasteProtection.xib create mode 100644 macos/Sources/Features/Paste Protection/PasteProtectionController.swift create mode 100644 macos/Sources/Features/Paste Protection/PasteProtectionView.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 153b146a9..7c67ecf90 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -40,6 +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 */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; /* End PBXBuildFile section */ @@ -80,6 +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 = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -101,6 +107,7 @@ children = ( A56D58872ACDE6BE00508D2C /* Services */, A59630982AEE1C4400D64628 /* Terminal */, + A5E112912AF73E4D00C6E0C2 /* Paste Protection */, A534263E2A7DCC5800EBB7A2 /* Settings */, ); path = Features; @@ -227,6 +234,16 @@ name = Frameworks; sourceTree = ""; }; + A5E112912AF73E4D00C6E0C2 /* Paste Protection */ = { + isa = PBXGroup; + children = ( + A5E112922AF73E6E00C6E0C2 /* PasteProtection.xib */, + A5E112942AF73E8A00C6E0C2 /* PasteProtectionController.swift */, + A5E112962AF7401B00C6E0C2 /* PasteProtectionView.swift */, + ); + path = "Paste Protection"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -289,6 +306,7 @@ A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */, A5A1F8852A489D6800D1E8BC /* terminfo in Resources */, A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */, + A5E112932AF73E6E00C6E0C2 /* PasteProtection.xib in Resources */, A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */, 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, ); @@ -324,9 +342,11 @@ A55685E029A03A9F004303CE /* AppError.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, + A5E112952AF73E8A00C6E0C2 /* PasteProtectionController.swift in Sources */, 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */, A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, + A5E112972AF7401B00C6E0C2 /* PasteProtectionView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/macos/Sources/Features/Paste Protection/PasteProtection.xib b/macos/Sources/Features/Paste Protection/PasteProtection.xib new file mode 100644 index 000000000..312a3da5a --- /dev/null +++ b/macos/Sources/Features/Paste Protection/PasteProtection.xib @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionController.swift b/macos/Sources/Features/Paste Protection/PasteProtectionController.swift new file mode 100644 index 000000000..bad16bf06 --- /dev/null +++ b/macos/Sources/Features/Paste Protection/PasteProtectionController.swift @@ -0,0 +1,15 @@ +import Foundation +import Cocoa +import SwiftUI +import GhosttyKit + +class PasteProtectionController: NSWindowController { + override var windowNibName: NSNib.Name? { "PasteProtection" } + + //MARK: - NSWindowController + + override func windowDidLoad() { + guard let window = window else { return } + window.contentView = NSHostingView(rootView: PasteProtectionView()) + } +} diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionView.swift b/macos/Sources/Features/Paste Protection/PasteProtectionView.swift new file mode 100644 index 000000000..61d084cbe --- /dev/null +++ b/macos/Sources/Features/Paste Protection/PasteProtectionView.swift @@ -0,0 +1,18 @@ +import SwiftUI + +struct PasteProtectionView: View { + var body: some View { + HStack { + Image("AppIconImage") + .resizable() + .scaledToFit() + .frame(width: 128, height: 128) + + VStack(alignment: .leading) { + Text("Oh, no. 😭").font(.title) + Text("Something went fatally wrong.\nCheck the logs and restart Ghostty.") + } + } + .padding() + } +} diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 66fe9c965..7aa4b7b26 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -115,6 +115,10 @@ class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDele viewModel: self, delegate: self )) + + // TODO: remove this, just for dev + let pp = PasteProtectionController() + window.beginSheet(pp.window!) } // Shows the "+" button in the tab bar, responds to that click. From 04acaf8b200fd4f9b5bea37d2b6bf3a7e2d09a3e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Nov 2023 20:47:16 -0700 Subject: [PATCH 2/7] macos: hook up paste protection delegate, cancel button --- .../PasteProtectionController.swift | 16 ++++- .../PasteProtectionView.swift | 59 +++++++++++++++---- .../Terminal/TerminalController.swift | 22 ++++++- .../Features/Terminal/TerminalView.swift | 2 +- 4 files changed, 85 insertions(+), 14 deletions(-) diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionController.swift b/macos/Sources/Features/Paste Protection/PasteProtectionController.swift index bad16bf06..ef0668c02 100644 --- a/macos/Sources/Features/Paste Protection/PasteProtectionController.swift +++ b/macos/Sources/Features/Paste Protection/PasteProtectionController.swift @@ -6,10 +6,24 @@ import GhosttyKit class PasteProtectionController: NSWindowController { override var windowNibName: NSNib.Name? { "PasteProtection" } + weak private var delegate: PasteProtectionViewDelegate? = nil + + init(delegate: PasteProtectionViewDelegate) { + 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()) + window.contentView = NSHostingView(rootView: PasteProtectionView( + contents: "Hello", + delegate: delegate + )) } } diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionView.swift b/macos/Sources/Features/Paste Protection/PasteProtectionView.swift index 61d084cbe..6c1cd08d2 100644 --- a/macos/Sources/Features/Paste Protection/PasteProtectionView.swift +++ b/macos/Sources/Features/Paste Protection/PasteProtectionView.swift @@ -1,18 +1,57 @@ import SwiftUI +protocol PasteProtectionViewDelegate: AnyObject { + func pasteProtectionComplete(_ action: PasteProtectionView.Action) +} + struct PasteProtectionView: View { + enum Action : String { + case cancel + case paste + } + + let contents: String + weak var delegate: PasteProtectionViewDelegate? = nil + var body: some View { - HStack { - Image("AppIconImage") - .resizable() - .scaledToFit() - .frame(width: 128, height: 128) - - VStack(alignment: .leading) { - Text("Oh, no. 😭").font(.title) - Text("Something went fatally wrong.\nCheck the logs and restart Ghostty.") + 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)) + .disabled(true) + .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) } - .padding() + } + + private func onCancel() { + AppDelegate.logger.warning("PASTE 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 7aa4b7b26..b277a1f6d 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -4,7 +4,10 @@ import SwiftUI import GhosttyKit /// The terminal controller is an NSWindowController that maps 1:1 to a terminal window. -class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDelegate, TerminalViewModel { +class TerminalController: NSWindowController, NSWindowDelegate, + TerminalViewDelegate, TerminalViewModel, + PasteProtectionViewDelegate +{ override var windowNibName: NSNib.Name? { "Terminal" } /// The app instance that this terminal view will represent. @@ -28,6 +31,9 @@ class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDele /// True when an alert is active so we don't overlap multiple. private var alert: NSAlert? = nil + /// The paste protection window, if shown. + private var pasteProtection: NSWindow? = nil + init(_ ghostty: Ghostty.AppState, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) { self.ghostty = ghostty super.init(window: nil) @@ -117,7 +123,8 @@ class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDele )) // TODO: remove this, just for dev - let pp = PasteProtectionController() + let pp = PasteProtectionController(delegate: self) + self.pasteProtection = pp.window window.beginSheet(pp.window!) } @@ -313,6 +320,17 @@ class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDele self.window?.close() } + //MARK: - PasteProtectionViewDelegate + + func pasteProtectionComplete(_ action: PasteProtectionView.Action) { + if let pasteWindow = self.pasteProtection { + window?.endSheet(pasteWindow) + self.pasteProtection = nil + } + + AppDelegate.logger.warning("PASTE action=\(action.rawValue)") + } + //MARK: - Notifications @objc private func onGotoTab(notification: SwiftUI.Notification) { diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index d3b1a66dc..42d15b38f 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -4,7 +4,7 @@ import GhosttyKit /// This delegate is notified of actions and property changes regarding the terminal view. This /// delegate is optional and can be used by a TerminalView caller to react to changes such as /// titles being set, cell sizes being changed, etc. -protocol TerminalViewDelegate: AnyObject, ObservableObject { +protocol TerminalViewDelegate: AnyObject { /// Called when the currently focused surface changed. This can be nil. func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) From 5dac8fba96062391932aec4f29f4c20f2a6193ac Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Nov 2023 20:48:18 -0700 Subject: [PATCH 3/7] macos: paste protection text should be selectable --- .../Features/Paste Protection/PasteProtectionController.swift | 2 +- .../Sources/Features/Paste Protection/PasteProtectionView.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionController.swift b/macos/Sources/Features/Paste Protection/PasteProtectionController.swift index ef0668c02..5da7b174f 100644 --- a/macos/Sources/Features/Paste Protection/PasteProtectionController.swift +++ b/macos/Sources/Features/Paste Protection/PasteProtectionController.swift @@ -22,7 +22,7 @@ class PasteProtectionController: NSWindowController { override func windowDidLoad() { guard let window = window else { return } window.contentView = NSHostingView(rootView: PasteProtectionView( - contents: "Hello", + contents: "Hello\nWorld", delegate: delegate )) } diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionView.swift b/macos/Sources/Features/Paste Protection/PasteProtectionView.swift index 6c1cd08d2..5a4e1c348 100644 --- a/macos/Sources/Features/Paste Protection/PasteProtectionView.swift +++ b/macos/Sources/Features/Paste Protection/PasteProtectionView.swift @@ -29,7 +29,6 @@ struct PasteProtectionView: View { } TextEditor(text: .constant(contents)) - .disabled(true) .textSelection(.enabled) .font(.system(.body, design: .monospaced)) .padding(.all, 4) From ef445515222a046416e459594bd23f9a7bb2a558 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 09:20:16 -0800 Subject: [PATCH 4/7] 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 { From f521b0cb99afaa5a75a15996fc368ee7f918e2f4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 09:20:27 -0800 Subject: [PATCH 5/7] core: not unsafe on bracketed --- src/Surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 46780be00..0e4bb47a4 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 ((true or !bracketed) and + if (!bracketed and self.config.clipboard_paste_protection and !allow_unsafe and !terminal.isSafePaste(data)) From 705f3b52c771c6b16f05f1e24a2b03ba476ec2d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 09:25:06 -0800 Subject: [PATCH 6/7] config: add clipboard-paste-bracketed-safe --- src/Surface.zig | 4 +++- src/config/Config.zig | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 0e4bb47a4..7032ec8ae 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -143,6 +143,7 @@ const DerivedConfig = struct { clipboard_write: bool, clipboard_trim_trailing_spaces: bool, clipboard_paste_protection: bool, + clipboard_paste_bracketed_safe: bool, copy_on_select: configpkg.CopyOnSelect, confirm_close_surface: bool, mouse_interval: u64, @@ -167,6 +168,7 @@ const DerivedConfig = struct { .clipboard_write = config.@"clipboard-write", .clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces", .clipboard_paste_protection = config.@"clipboard-paste-protection", + .clipboard_paste_bracketed_safe = config.@"clipboard-paste-bracketed-safe", .copy_on_select = config.@"copy-on-select", .confirm_close_surface = config.@"confirm-close-surface", .mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms @@ -2514,7 +2516,7 @@ fn completeClipboardPaste( // // We do not do this for bracketed pastes because bracketed pastes are // by definition safe since they're framed. - if (!bracketed and + if ((!self.config.clipboard_paste_bracketed_safe or !bracketed) and self.config.clipboard_paste_protection and !allow_unsafe and !terminal.isSafePaste(data)) diff --git a/src/config/Config.zig b/src/config/Config.zig index fbb92e5ba..0e368c390 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -430,6 +430,12 @@ keybind: Keybinds = .{}, /// This currently only works on Linux (GTK). @"clipboard-paste-protection": bool = true, +/// If true, bracketed pastes will be considered safe. By default, +/// bracketed pastes are considered safe. "Bracketed" pastes are pastes +/// while the running program has bracketed paste mode enabled (a setting +/// set by the running program, not the terminal emulator). +@"clipboard-paste-bracketed-safe": bool = true, + /// The total amount of bytes that can be used for image data (i.e. /// the Kitty image protocol) per terminal scren. The maximum value /// is 4,294,967,295 (4GB). The default is 320MB. If this is set to zero, From 5de8efccccc074ddb45a2739dd470f760169e47f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Nov 2023 09:31:12 -0800 Subject: [PATCH 7/7] macos: comment the new views --- .../Paste Protection/PasteProtectionController.swift | 2 ++ .../Features/Paste Protection/PasteProtectionView.swift | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionController.swift b/macos/Sources/Features/Paste Protection/PasteProtectionController.swift index 35087f428..12514d5b7 100644 --- a/macos/Sources/Features/Paste Protection/PasteProtectionController.swift +++ b/macos/Sources/Features/Paste Protection/PasteProtectionController.swift @@ -3,6 +3,8 @@ 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" } diff --git a/macos/Sources/Features/Paste Protection/PasteProtectionView.swift b/macos/Sources/Features/Paste Protection/PasteProtectionView.swift index ef0aff6f0..aa63ae26f 100644 --- a/macos/Sources/Features/Paste Protection/PasteProtectionView.swift +++ b/macos/Sources/Features/Paste Protection/PasteProtectionView.swift @@ -1,16 +1,21 @@ 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 {