apprt/embedded: hook up paste confirmation

This commit is contained in:
Mitchell Hashimoto
2023-11-05 09:20:16 -08:00
parent 5dac8fba96
commit ef44551522
8 changed files with 149 additions and 30 deletions

View File

@ -325,6 +325,7 @@ typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
typedef void (*ghostty_runtime_set_mouse_shape_cb)(void *, ghostty_mouse_shape_e);
typedef void (*ghostty_runtime_set_mouse_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);

View File

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

View File

@ -46,7 +46,6 @@ struct PasteProtectionView: View {
}
private func onCancel() {
AppDelegate.logger.warning("PASTE onCancel")
delegate?.pasteProtectionComplete(.cancel)
}

View File

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

View File

@ -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<CChar>?,
state: UnsafeMutableRawPointer?
) {
guard let surface = self.surfaceUserdata(from: userdata) else { return }
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
NotificationCenter.default.post(
name: Notification.confirmUnsafePaste,
object: surface,
userInfo: [
Notification.UnsafePasteStrKey: valueStr,
Notification.UnsafePasteStateKey: state as Any
]
)
}
static func completeClipboardRequest(
_ surface: ghostty_surface_t,
data: String,
state: UnsafeMutableRawPointer?,
confirmed: Bool = false
) {
data.withCString { ptr in
ghostty_surface_complete_clipboard_request(surface, ptr, UInt(data.utf8.count), state)
ghostty_surface_complete_clipboard_request(surface, ptr, state, confirmed)
}
}

View File

@ -108,6 +108,10 @@ extension Ghostty.Notification {
/// Notification to show/hide the inspector
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.

View File

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

View File

@ -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 {