mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
apprt/embedded: hook up paste confirmation
This commit is contained in:
@ -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);
|
||||
|
@ -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
|
||||
))
|
||||
}
|
||||
|
@ -46,7 +46,6 @@ struct PasteProtectionView: View {
|
||||
}
|
||||
|
||||
private func onCancel() {
|
||||
AppDelegate.logger.warning("PASTE onCancel")
|
||||
delegate?.pasteProtectionComplete(.cancel)
|
||||
}
|
||||
|
||||
|
@ -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!)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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))
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user