apprt/gtk-ng: clipboard confirmation dialog

This commit is contained in:
Mitchell Hashimoto
2025-07-22 09:51:23 -07:00
parent a91ed99054
commit 222c3c888d
4 changed files with 158 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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