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_shape_cb)(void *, ghostty_mouse_shape_e);
|
||||||
typedef void (*ghostty_runtime_set_mouse_visibility_cb)(void *, bool);
|
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_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_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_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_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_shape_cb set_mouse_shape_cb;
|
||||||
ghostty_runtime_set_mouse_visibility_cb set_mouse_visibility_cb;
|
ghostty_runtime_set_mouse_visibility_cb set_mouse_visibility_cb;
|
||||||
ghostty_runtime_read_clipboard_cb read_clipboard_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_write_clipboard_cb write_clipboard_cb;
|
||||||
ghostty_runtime_new_split_cb new_split_cb;
|
ghostty_runtime_new_split_cb new_split_cb;
|
||||||
ghostty_runtime_new_tab_cb new_tab_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(ghostty_surface_t, ghostty_split_direction_e);
|
||||||
void ghostty_surface_split_focus(ghostty_surface_t, ghostty_split_focus_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);
|
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);
|
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
|
||||||
void ghostty_inspector_free(ghostty_surface_t);
|
void ghostty_inspector_free(ghostty_surface_t);
|
||||||
|
@ -6,9 +6,15 @@ import GhosttyKit
|
|||||||
class PasteProtectionController: NSWindowController {
|
class PasteProtectionController: NSWindowController {
|
||||||
override var windowNibName: NSNib.Name? { "PasteProtection" }
|
override var windowNibName: NSNib.Name? { "PasteProtection" }
|
||||||
|
|
||||||
|
let surface: ghostty_surface_t
|
||||||
|
let contents: String
|
||||||
|
let state: UnsafeMutableRawPointer?
|
||||||
weak private var delegate: PasteProtectionViewDelegate? = nil
|
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
|
self.delegate = delegate
|
||||||
super.init(window: nil)
|
super.init(window: nil)
|
||||||
}
|
}
|
||||||
@ -22,7 +28,7 @@ class PasteProtectionController: NSWindowController {
|
|||||||
override func windowDidLoad() {
|
override func windowDidLoad() {
|
||||||
guard let window = window else { return }
|
guard let window = window else { return }
|
||||||
window.contentView = NSHostingView(rootView: PasteProtectionView(
|
window.contentView = NSHostingView(rootView: PasteProtectionView(
|
||||||
contents: "Hello\nWorld",
|
contents: contents,
|
||||||
delegate: delegate
|
delegate: delegate
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,6 @@ struct PasteProtectionView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func onCancel() {
|
private func onCancel() {
|
||||||
AppDelegate.logger.warning("PASTE onCancel")
|
|
||||||
delegate?.pasteProtectionComplete(.cancel)
|
delegate?.pasteProtectionComplete(.cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
private var alert: NSAlert? = nil
|
private var alert: NSAlert? = nil
|
||||||
|
|
||||||
/// The paste protection window, if shown.
|
/// 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) {
|
init(_ ghostty: Ghostty.AppState, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) {
|
||||||
self.ghostty = ghostty
|
self.ghostty = ghostty
|
||||||
@ -54,6 +54,11 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
selector: #selector(onGotoTab),
|
selector: #selector(onGotoTab),
|
||||||
name: Ghostty.Notification.ghosttyGotoTab,
|
name: Ghostty.Notification.ghosttyGotoTab,
|
||||||
object: nil)
|
object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(onConfirmUnsafePaste),
|
||||||
|
name: Ghostty.Notification.confirmUnsafePaste,
|
||||||
|
object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -121,11 +126,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
viewModel: self,
|
viewModel: self,
|
||||||
delegate: 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.
|
// Shows the "+" button in the tab bar, responds to that click.
|
||||||
@ -320,15 +320,28 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
self.window?.close()
|
self.window?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - PasteProtectionViewDelegate
|
//MARK: - Paste Protection
|
||||||
|
|
||||||
func pasteProtectionComplete(_ action: PasteProtectionView.Action) {
|
func pasteProtectionComplete(_ action: PasteProtectionView.Action) {
|
||||||
if let pasteWindow = self.pasteProtection {
|
// End our paste protection no matter what
|
||||||
window?.endSheet(pasteWindow)
|
guard let pp = self.pasteProtection else { return }
|
||||||
self.pasteProtection = nil
|
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
|
//MARK: - Notifications
|
||||||
@ -389,4 +402,33 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
Ghostty.moveFocus(to: focusedSurface)
|
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_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) },
|
||||||
set_mouse_visibility_cb: { userdata, visible in AppState.setMouseVisibility(userdata, visible: visible) },
|
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) },
|
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) },
|
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_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_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) },
|
||||||
@ -390,9 +391,31 @@ extension Ghostty {
|
|||||||
completeClipboardRequest(surface, data: str, state: state)
|
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
|
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
|
/// Notification to show/hide the inspector
|
||||||
static let didControlInspector = Notification.Name("com.mitchellh.ghostty.didControlInspector")
|
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.
|
// Make the input enum hashable.
|
||||||
|
@ -2514,7 +2514,7 @@ fn completeClipboardPaste(
|
|||||||
//
|
//
|
||||||
// We do not do this for bracketed pastes because bracketed pastes are
|
// We do not do this for bracketed pastes because bracketed pastes are
|
||||||
// by definition safe since they're framed.
|
// by definition safe since they're framed.
|
||||||
if (!bracketed and
|
if ((true or !bracketed) and
|
||||||
self.config.clipboard_paste_protection and
|
self.config.clipboard_paste_protection and
|
||||||
!allow_unsafe and
|
!allow_unsafe and
|
||||||
!terminal.isSafePaste(data))
|
!terminal.isSafePaste(data))
|
||||||
|
@ -61,6 +61,15 @@ pub const App = struct {
|
|||||||
/// value then this should return null.
|
/// value then this should return null.
|
||||||
read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.C) void,
|
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 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) 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(
|
pub fn setClipboardString(
|
||||||
self: *const Surface,
|
self: *const Surface,
|
||||||
val: [:0]const u8,
|
val: [:0]const u8,
|
||||||
@ -1351,20 +1399,15 @@ pub const CAPI = struct {
|
|||||||
/// with a request the request pointer will be invalidated.
|
/// with a request the request pointer will be invalidated.
|
||||||
export fn ghostty_surface_complete_clipboard_request(
|
export fn ghostty_surface_complete_clipboard_request(
|
||||||
ptr: *Surface,
|
ptr: *Surface,
|
||||||
str_ptr: [*]const u8,
|
str: [*:0]const u8,
|
||||||
str_len: usize,
|
|
||||||
state: *apprt.ClipboardRequest,
|
state: *apprt.ClipboardRequest,
|
||||||
|
confirmed: bool,
|
||||||
) void {
|
) void {
|
||||||
// The state is unusable after this
|
ptr.completeClipboardRequest(
|
||||||
defer ptr.core_surface.app.alloc.destroy(state);
|
std.mem.sliceTo(str, 0),
|
||||||
|
state,
|
||||||
if (str_len == 0) return;
|
confirmed,
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn ghostty_surface_inspector(ptr: *Surface) ?*Inspector {
|
export fn ghostty_surface_inspector(ptr: *Surface) ?*Inspector {
|
||||||
|
Reference in New Issue
Block a user