From 222c3c888d556965aecfda82aee4ef3e960e2ca0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Jul 2025 09:51:23 -0700 Subject: [PATCH] apprt/gtk-ng: clipboard confirmation dialog --- .../class/clipboard_confirmation_dialog.zig | 108 +++++++++++++++++- src/apprt/gtk-ng/class/surface.zig | 18 +-- src/apprt/gtk-ng/css/style.css | 25 ++++ .../ui/1.2/clipboard-confirmation-dialog.blp | 25 ++-- 4 files changed, 158 insertions(+), 18 deletions(-) diff --git a/src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig b/src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig index 021072197..a54e343b4 100644 --- a/src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig +++ b/src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig @@ -7,6 +7,7 @@ const gtk = @import("gtk"); const apprt = @import("../../../apprt.zig"); const gresource = @import("../build/gresource.zig"); +const i18n = @import("../../../os/main.zig").i18n; const Common = @import("../class.zig").Common; const Dialog = @import("dialog.zig").Dialog; @@ -25,6 +26,26 @@ pub const ClipboardConfirmationDialog = extern struct { }); pub const properties = struct { + pub const @"can-remember" = struct { + pub const name = "can-remember"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Can Remember", + .blurb = "Allow remembering the choice.", + .default = false, + .accessor = gobject.ext.privateFieldAccessor( + Self, + Private, + &Private.offset, + "can_remember", + ), + }, + ); + }; + pub const request = struct { pub const name = "request"; const impl = gobject.ext.defineProperty( @@ -43,12 +64,37 @@ pub const ClipboardConfirmationDialog = extern struct { }, ); }; + + pub const @"clipboard-contents" = struct { + pub const name = "clipboard-contents"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*gtk.TextBuffer, + .{ + .nick = "Clipboard Contents", + .blurb = "The clipboard contents being read/written.", + .accessor = gobject.ext.privateFieldAccessor( + Self, + Private, + &Private.offset, + "clipboard_contents", + ), + }, + ); + }; }; const Private = struct { /// The request that this dialog is for. request: ?*apprt.ClipboardRequest = null, + /// The clipboard contents being read/written. + clipboard_contents: ?*gtk.TextBuffer = null, + + /// Whether the user can remember the choice. + can_remember: bool = false, + pub var offset: c_int = 0; }; @@ -58,16 +104,60 @@ pub const ClipboardConfirmationDialog = extern struct { fn init(self: *Self, _: *Class) callconv(.C) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); + + // Some property signals + _ = gobject.Object.signals.notify.connect( + self, + ?*anyopaque, + &propRequest, + null, + .{ .detail = "request" }, + ); + + // Trigger initial values + self.propRequest(undefined, null); } pub fn present(self: *Self, parent: ?*gtk.Widget) void { self.as(Dialog).present(parent); } + //--------------------------------------------------------------- + // Signal Handlers + + fn propRequest( + self: *Self, + _: *gobject.ParamSpec, + _: ?*anyopaque, + ) callconv(.c) void { + const priv = self.private(); + const req = priv.request orelse return; + switch (req.*) { + .osc_52_write => { + self.as(Dialog.Parent).setHeading(i18n._("Authorize Clipboard Access")); + self.as(Dialog.Parent).setBody(i18n._("An application is attempting to write to the clipboard. The current clipboard contents are shown below.")); + }, + .osc_52_read => { + self.as(Dialog.Parent).setHeading(i18n._("Authorize Clipboard Access")); + self.as(Dialog.Parent).setBody(i18n._("An application is attempting to read from the clipboard. The current clipboard contents are shown below.")); + }, + .paste => { + self.as(Dialog.Parent).setHeading(i18n._("Warning: Potentially Unsafe Paste")); + self.as(Dialog.Parent).setBody(i18n._("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.")); + }, + } + } + //--------------------------------------------------------------- // Virtual methods fn dispose(self: *Self) callconv(.C) void { + const priv = self.private(); + if (priv.clipboard_contents) |v| { + v.unref(); + priv.clipboard_contents = null; + } + gtk.Widget.disposeTemplate( self.as(gtk.Widget), getGObjectType(), @@ -79,6 +169,19 @@ pub const ClipboardConfirmationDialog = extern struct { ); } + fn finalize(self: *Self) callconv(.C) void { + const priv = self.private(); + if (priv.request) |v| { + glib.ext.destroy(v); + priv.request = null; + } + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + const C = Common(Self, Private); pub const as = C.as; pub const ref = C.ref; @@ -101,15 +204,18 @@ pub const ClipboardConfirmationDialog = extern struct { ); // Bindings - //class.bindTemplateChildPrivate("label", .{}); + //class.bindTemplateChildPrivate("remember_bin", .{}); // Properties gobject.ext.registerProperties(class, &.{ + properties.@"can-remember".impl, + properties.@"clipboard-contents".impl, properties.request.impl, }); // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); + gobject.Object.virtual_methods.finalize.implement(class, &finalize); } pub const as = C.Class.as; diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 6e8e717cb..2542d54b0 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -2001,19 +2001,21 @@ const Clipboard = struct { return; } + // Build a text buffer for our contents + const contents_buf: *gtk.TextBuffer = .new(null); + defer contents_buf.unref(); + contents_buf.insertAtCursor(val, @intCast(val.len)); + // Confirm const diag = gobject.ext.newInstance( ClipboardConfirmationDialog, - .{ .request = &apprt.ClipboardRequest{ - .osc_52_write = clipboard_type, - } }, + .{ + .request = &apprt.ClipboardRequest{ .osc_52_write = clipboard_type }, + .@"can-remember" = true, + .@"clipboard-contents" = contents_buf, + }, ); - - // We need to trigger the dialog - diag.present(self.as(gtk.Widget)); - - log.warn("TODO: confirmation window", .{}); } /// Request data from the clipboard (read the clipboard). This diff --git a/src/apprt/gtk-ng/css/style.css b/src/apprt/gtk-ng/css/style.css index 99515ec4a..8a7c939f7 100644 --- a/src/apprt/gtk-ng/css/style.css +++ b/src/apprt/gtk-ng/css/style.css @@ -4,6 +4,9 @@ * https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.3/styles-and-appearance.html#custom-styles */ +/* + * GhosttySurface URL overlay + */ label.url-overlay { padding: 4px 8px 4px 8px; outline-style: solid; @@ -23,6 +26,9 @@ label.url-overlay.right { border-radius: 6px 0px 0px 0px; } +/* + * GhosttySurface resize overlay + */ .size-overlay label { padding: 4px 8px 4px 8px; border-radius: 6px 6px 6px 6px; @@ -30,3 +36,22 @@ label.url-overlay.right { outline-width: 1px; outline-color: #555555; } + +/* + * GhosttyClipboardConfirmationDialog + * + * Based on boxed-list-separate: + * https://gitlab.gnome.org/GNOME/libadwaita/-/blob/ad446167acf3e6d1ee693f98ca636268be8592a1/src/stylesheet/widgets/_lists.scss#L548 + */ +.clipboard-confirmation-dialog list { + background: none; +} + +.clipboard-confirmation-dialog list > row { + border: none; + margin-bottom: 12px; +} + +.clipboard-confirmation-dialog list > row:last-child { + margin-bottom: 0; +} diff --git a/src/apprt/gtk-ng/ui/1.2/clipboard-confirmation-dialog.blp b/src/apprt/gtk-ng/ui/1.2/clipboard-confirmation-dialog.blp index efd7c5014..1f1c3e836 100644 --- a/src/apprt/gtk-ng/ui/1.2/clipboard-confirmation-dialog.blp +++ b/src/apprt/gtk-ng/ui/1.2/clipboard-confirmation-dialog.blp @@ -3,8 +3,13 @@ using Gtk 4.0; using Adw 1; template $GhosttyClipboardConfirmationDialog: $GhosttyDialog { + styles [ + "clipboard-confirmation-dialog", + ] + heading: _("Authorize Clipboard Access"); - body: _("An application is attempting to write to the clipboard. The current clipboard contents are shown below."); + // Not localized because this is a placeholder users never see. + body: "If you see this text, there is a bug in Ghostty. Please report it."; responses [ cancel: _("Deny") suggested, @@ -17,10 +22,6 @@ template $GhosttyClipboardConfirmationDialog: $GhosttyDialog { extra-child: ListBox { selection-mode: none; - styles [ - "boxed-list-separate", - ] - Overlay { styles [ "osd", @@ -32,6 +33,10 @@ template $GhosttyClipboardConfirmationDialog: $GhosttyDialog { height-request: 200; TextView text_view { + styles [ + "clipboard-content-view", + ] + cursor-visible: false; editable: false; monospace: true; @@ -39,10 +44,7 @@ template $GhosttyClipboardConfirmationDialog: $GhosttyDialog { left-margin: 8; bottom-margin: 8; right-margin: 8; - - styles [ - "clipboard-content-view", - ] + buffer: bind template.clipboard-contents; } } @@ -74,6 +76,11 @@ template $GhosttyClipboardConfirmationDialog: $GhosttyDialog { } Adw.SwitchRow remember_choice { + styles [ + "card", + ] + + visible: bind template.can-remember; title: _("Remember choice for this split"); subtitle: _("Reload configuration to show this prompt again"); }