apprt/gtk-ng: clipboard support (#8030)

This ports over read/write clipboard to gtk-ng.

This was a surprisingly massive amount of work! The clipboard
confirmation dialog is non-trivial: it supports multiple read/write
types, blurring, remember choice, and spans multiple Adw versions. I was
able to port all of the functionality into a single
`CloseConfirmationDialog` class and make use of a good amount of
Blueprint binds to simplify some stuff.
This commit is contained in:
Mitchell Hashimoto
2025-07-22 14:45:26 -07:00
committed by GitHub
8 changed files with 987 additions and 17 deletions

View File

@ -73,9 +73,10 @@ pub fn clipboardRequest(
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) !void {
_ = self;
_ = clipboard_type;
_ = state;
try self.surface.clipboardRequest(
clipboard_type,
state,
);
}
pub fn setClipboardString(
@ -84,10 +85,11 @@ pub fn setClipboardString(
clipboard_type: apprt.Clipboard,
confirm: bool,
) !void {
_ = self;
_ = val;
_ = clipboard_type;
_ = confirm;
self.surface.setClipboardString(
val,
clipboard_type,
confirm,
);
}
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {

View File

@ -33,6 +33,8 @@ pub const icon_sizes: []const comptime_int = &.{ 16, 32, 128, 256, 512, 1024 };
///
/// These will be asserted to exist at runtime.
pub const blueprints: []const Blueprint = &.{
.{ .major = 1, .minor = 0, .name = "clipboard-confirmation-dialog" },
.{ .major = 1, .minor = 4, .name = "clipboard-confirmation-dialog" },
.{ .major = 1, .minor = 2, .name = "close-confirmation-dialog" },
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
.{ .major = 1, .minor = 2, .name = "resize-overlay" },

View File

@ -0,0 +1,398 @@
const std = @import("std");
const assert = std.debug.assert;
const adw = @import("adw");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const apprt = @import("../../../apprt.zig");
const gresource = @import("../build/gresource.zig");
const i18n = @import("../../../os/main.zig").i18n;
const adw_version = @import("../adw_version.zig");
const Common = @import("../class.zig").Common;
const Dialog = @import("dialog.zig").Dialog;
const log = std.log.scoped(.gtk_ghostty_clipboard_confirmation);
/// Whether we're able to have the remember switch
const can_remember = adw_version.supportsSwitchRow();
pub const ClipboardConfirmationDialog = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = Dialog;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyClipboardConfirmationDialog",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
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(
name,
Self,
?*apprt.ClipboardRequest,
.{
.nick = "Request",
.blurb = "The clipboard request.",
.accessor = gobject.ext.privateFieldAccessor(
Self,
Private,
&Private.offset,
"request",
),
},
);
};
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",
),
},
);
};
pub const blur = struct {
pub const name = "blur";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.nick = "Blur",
.blurb = "Blur the contents, allowing the user to reveal.",
.default = false,
.accessor = gobject.ext.privateFieldAccessor(
Self,
Private,
&Private.offset,
"blur",
),
},
);
};
};
pub const signals = struct {
pub const deny = struct {
pub const name = "deny";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{bool},
void,
);
};
pub const confirm = struct {
pub const name = "confirm";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{bool},
void,
);
};
};
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 contents should be blurred.
blur: bool = false,
/// Whether the user can remember the choice.
can_remember: bool = false,
// Template bindings
text_view_scroll: *gtk.ScrolledWindow,
text_view: *gtk.TextView,
reveal_button: *gtk.Button,
hide_button: *gtk.Button,
remember_choice: if (can_remember) *adw.SwitchRow else void,
pub var offset: c_int = 0;
};
pub fn new() *Self {
return gobject.ext.newInstance(Self, .{});
}
fn init(self: *Self, _: *Class) callconv(.C) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
const priv = self.private();
// Signals
_ = gtk.Button.signals.clicked.connect(
priv.reveal_button,
*Self,
revealButtonClicked,
self,
.{},
);
_ = gtk.Button.signals.clicked.connect(
priv.hide_button,
*Self,
hideButtonClicked,
self,
.{},
);
// Some property signals
_ = gobject.Object.signals.notify.connect(
self,
?*anyopaque,
&propBlur,
null,
.{ .detail = "blur" },
);
_ = gobject.Object.signals.notify.connect(
self,
?*anyopaque,
&propRequest,
null,
.{ .detail = "request" },
);
// Trigger initial values
self.propBlur(undefined, null);
self.propRequest(undefined, null);
}
pub fn present(self: *Self, parent: ?*gtk.Widget) void {
self.as(Dialog).present(parent);
}
/// Get the clipboard request without copying.
pub fn getRequest(self: *Self) ?*apprt.ClipboardRequest {
return self.private().request;
}
/// Get the clipboard contents without copying.
pub fn getClipboardContents(self: *Self) ?*gtk.TextBuffer {
return self.private().clipboard_contents;
}
//---------------------------------------------------------------
// Signal Handlers
fn propBlur(
self: *Self,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const priv = self.private();
if (priv.blur) {
priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
priv.text_view.as(gtk.Widget).addCssClass("blurred");
priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(false));
} else {
priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));
priv.text_view.as(gtk.Widget).removeCssClass("blurred");
priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false));
priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(false));
}
}
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."));
},
}
}
fn revealButtonClicked(_: *gtk.Button, self: *Self) callconv(.c) void {
const priv = self.private();
priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));
priv.text_view.as(gtk.Widget).removeCssClass("blurred");
priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(true));
priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false));
}
fn hideButtonClicked(_: *gtk.Button, self: *Self) callconv(.c) void {
const priv = self.private();
priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
priv.text_view.as(gtk.Widget).addCssClass("blurred");
priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(false));
priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
}
//---------------------------------------------------------------
// Virtual methods
fn response(
self: *Self,
response_id: [*:0]const u8,
) callconv(.C) void {
const remember: bool = if (comptime can_remember) remember: {
const priv = self.private();
break :remember priv.remember_choice.getActive() != 0;
} else false;
if (std.mem.orderZ(u8, response_id, "cancel") == .eq) {
signals.deny.impl.emit(
self,
null,
.{remember},
null,
);
} else if (std.mem.orderZ(u8, response_id, "ok") == .eq) {
signals.confirm.impl.emit(
self,
null,
.{remember},
null,
);
}
}
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(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
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;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
if (comptime adw_version.atLeast(1, 4, 0))
comptime gresource.blueprint(.{
.major = 1,
.minor = 4,
.name = "clipboard-confirmation-dialog",
})
else
comptime gresource.blueprint(.{
.major = 1,
.minor = 0,
.name = "clipboard-confirmation-dialog",
}),
);
// Bindings
class.bindTemplateChildPrivate("text_view_scroll", .{});
class.bindTemplateChildPrivate("text_view", .{});
class.bindTemplateChildPrivate("hide_button", .{});
class.bindTemplateChildPrivate("reveal_button", .{});
if (comptime can_remember) {
class.bindTemplateChildPrivate("remember_choice", .{});
}
// Properties
gobject.ext.registerProperties(class, &.{
properties.blur.impl,
properties.@"can-remember".impl,
properties.@"clipboard-contents".impl,
properties.request.impl,
});
// Signals
signals.confirm.impl.register(.{});
signals.deny.impl.register(.{});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
Dialog.virtual_methods.response.implement(class, &response);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
};
};

View File

@ -2,6 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gdk = @import("gdk");
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
@ -20,6 +21,7 @@ const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application;
const Config = @import("config.zig").Config;
const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog;
const log = std.log.scoped(.gtk_ghostty_surface);
@ -55,6 +57,26 @@ pub const Surface = extern struct {
);
};
pub const focused = struct {
pub const name = "focused";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.nick = "Focused",
.blurb = "The focused state of the surface.",
.default = false,
.accessor = gobject.ext.privateFieldAccessor(
Self,
Private,
&Private.offset,
"focused",
),
},
);
};
pub const @"mouse-hidden" = struct {
pub const name = "mouse-hidden";
const impl = gobject.ext.defineProperty(
@ -168,6 +190,30 @@ pub const Surface = extern struct {
void,
);
};
/// Emitted whenever the clipboard has been written.
pub const @"clipboard-write" = struct {
pub const name = "clipboard-write";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
/// Emitted whenever the surface reads the clipboard.
pub const @"clipboard-read" = struct {
pub const name = "clipboard-read";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
};
const Private = struct {
@ -195,6 +241,10 @@ pub const Surface = extern struct {
/// The title of this surface, if any has been set.
title: ?[:0]const u8 = null,
/// The current focus state of the terminal based on the
/// focus events.
focused: bool = true,
/// The overlay we use for things such as the URL hover label
/// or resize box. Bound from the template.
overlay: *gtk.Overlay = undefined,
@ -690,6 +740,32 @@ pub const Surface = extern struct {
return env;
}
pub fn clipboardRequest(
self: *Self,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) !void {
try Clipboard.request(
self,
clipboard_type,
state,
);
}
pub fn setClipboardString(
self: *Self,
val: [:0]const u8,
clipboard_type: apprt.Clipboard,
confirm: bool,
) void {
Clipboard.set(
self,
val,
clipboard_type,
confirm,
);
}
//---------------------------------------------------------------
// Virtual Methods
@ -704,6 +780,7 @@ pub const Surface = extern struct {
priv.cursor_pos = .{ .x = 0, .y = 0 };
priv.mouse_shape = .text;
priv.mouse_hidden = false;
priv.focused = true;
priv.size = .{
// Funky numbers on purpose so they stand out if for some reason
// our size doesn't get properly set.
@ -1215,31 +1292,42 @@ pub const Surface = extern struct {
fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
const priv = self.private();
priv.focused = true;
if (priv.im_context) |im_context| {
im_context.as(gtk.IMContext).focusIn();
}
if (priv.core_surface) |surface| {
surface.focusCallback(true) catch |err| {
log.warn("error in focus callback err={}", .{err});
};
}
_ = glib.idleAddOnce(idleFocus, self.ref());
}
fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
const priv = self.private();
priv.focused = false;
if (priv.im_context) |im_context| {
im_context.as(gtk.IMContext).focusOut();
}
if (priv.core_surface) |surface| {
surface.focusCallback(false) catch |err| {
_ = glib.idleAddOnce(idleFocus, self.ref());
}
/// The focus callback must be triggered on an idle loop source because
/// there are actions within libghostty callbacks (such as showing close
/// confirmation dialogs) that can trigger focus loss and cause a deadlock
/// because the lock may be held during the callback.
///
/// Userdata should be a `*Surface`. This will unref once.
fn idleFocus(ud: ?*anyopaque) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(ud orelse return));
defer self.unref();
const priv = self.private();
const surface = priv.core_surface orelse return;
surface.focusCallback(priv.focused) catch |err| {
log.warn("error in focus callback err={}", .{err});
};
}
}
fn gcMouseDown(
gesture: *gtk.GestureClick,
@ -1853,6 +1941,7 @@ pub const Surface = extern struct {
// Properties
gobject.ext.registerProperties(class, &.{
properties.config.impl,
properties.focused.impl,
properties.@"mouse-shape".impl,
properties.@"mouse-hidden".impl,
properties.@"mouse-hover-url".impl,
@ -1862,6 +1951,8 @@ pub const Surface = extern struct {
// Signals
signals.@"close-request".impl.register(.{});
signals.@"clipboard-read".impl.register(.{});
signals.@"clipboard-write".impl.register(.{});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
@ -1903,3 +1994,255 @@ fn translateMouseButton(button: c_uint) input.MouseButton {
else => .unknown,
};
}
/// A namespace for our clipboard-related functions so Surface isn't SO large.
const Clipboard = struct {
/// Get the specific type of clipboard for a widget.
pub fn get(
widget: *gtk.Widget,
clipboard: apprt.Clipboard,
) ?*gdk.Clipboard {
return switch (clipboard) {
.standard => widget.getClipboard(),
.selection, .primary => widget.getPrimaryClipboard(),
};
}
/// Set the clipboard contents.
pub fn set(
self: *Surface,
val: [:0]const u8,
clipboard_type: apprt.Clipboard,
confirm: bool,
) void {
const priv = self.private();
// If no confirmation is necessary, set the clipboard.
if (!confirm) {
const clipboard = get(
priv.gl_area.as(gtk.Widget),
clipboard_type,
) orelse return;
clipboard.setText(val);
Surface.signals.@"clipboard-write".impl.emit(
self,
null,
.{},
null,
);
return;
}
showClipboardConfirmation(
self,
.{ .osc_52_write = clipboard_type },
val,
);
}
/// Request data from the clipboard (read the clipboard). This
/// completes asynchronously and will call the `completeClipboardRequest`
/// core surface API when done.
pub fn request(
self: *Surface,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) Allocator.Error!void {
// Get our requested clipboard
const clipboard = get(
self.private().gl_area.as(gtk.Widget),
clipboard_type,
) orelse return;
// Allocate our userdata
const alloc = Application.default().allocator();
const ud = try alloc.create(Request);
errdefer alloc.destroy(ud);
ud.* = .{
// Important: we ref self here so that we can't free memory
// while we have an outstanding clipboard read.
.self = self.ref(),
.state = state,
};
errdefer self.unref();
// Read
clipboard.readTextAsync(
null,
clipboardReadText,
ud,
);
}
fn showClipboardConfirmation(
self: *Surface,
req: apprt.ClipboardRequest,
str: [:0]const u8,
) void {
// Build a text buffer for our contents
const contents_buf: *gtk.TextBuffer = .new(null);
defer contents_buf.unref();
contents_buf.insertAtCursor(str, @intCast(str.len));
// Confirm
const dialog = gobject.ext.newInstance(
ClipboardConfirmationDialog,
.{
.request = &req,
.@"can-remember" = switch (req) {
.osc_52_read, .osc_52_write => true,
.paste => false,
},
.@"clipboard-contents" = contents_buf,
},
);
_ = ClipboardConfirmationDialog.signals.confirm.connect(
dialog,
*Surface,
clipboardConfirmationConfirm,
self,
.{},
);
_ = ClipboardConfirmationDialog.signals.deny.connect(
dialog,
*Surface,
clipboardConfirmationDeny,
self,
.{},
);
dialog.present(self.as(gtk.Widget));
}
fn clipboardConfirmationConfirm(
dialog: *ClipboardConfirmationDialog,
remember: bool,
self: *Surface,
) callconv(.c) void {
const priv = self.private();
const surface = priv.core_surface orelse return;
const req = dialog.getRequest() orelse return;
// Handle remember
if (remember) switch (req.*) {
.osc_52_read => surface.config.clipboard_read = .allow,
.osc_52_write => surface.config.clipboard_write = .allow,
.paste => {},
};
// Get our text
const text_buf = dialog.getClipboardContents() orelse return;
var text_val = gobject.ext.Value.new(?[:0]const u8);
defer text_val.unset();
gobject.Object.getProperty(
text_buf.as(gobject.Object),
"text",
&text_val,
);
const text = gobject.ext.Value.get(
&text_val,
?[:0]const u8,
) orelse return;
surface.completeClipboardRequest(
req.*,
text,
true,
) catch |err| {
log.warn("failed to complete clipboard request: {}", .{err});
};
}
fn clipboardConfirmationDeny(
dialog: *ClipboardConfirmationDialog,
remember: bool,
self: *Surface,
) callconv(.c) void {
const priv = self.private();
const surface = priv.core_surface orelse return;
const req = dialog.getRequest() orelse return;
// Handle remember
if (remember) switch (req.*) {
.osc_52_read => surface.config.clipboard_read = .deny,
.osc_52_write => surface.config.clipboard_write = .deny,
.paste => @panic("paste should not be able to be remembered"),
};
}
fn clipboardReadText(
source: ?*gobject.Object,
res: *gio.AsyncResult,
ud: ?*anyopaque,
) callconv(.c) void {
const clipboard = gobject.ext.cast(
gdk.Clipboard,
source orelse return,
) orelse return;
const req: *Request = @ptrCast(@alignCast(ud orelse return));
const alloc = Application.default().allocator();
defer alloc.destroy(req);
const self = req.self;
defer self.unref();
var gerr: ?*glib.Error = null;
const cstr_ = clipboard.readTextFinish(res, &gerr);
if (gerr) |err| {
defer err.free();
log.warn(
"failed to read clipboard err={s}",
.{err.f_message orelse "(no message)"},
);
return;
}
const cstr = cstr_ orelse return;
defer glib.free(cstr);
const str = std.mem.sliceTo(cstr, 0);
const surface = self.private().core_surface orelse return;
surface.completeClipboardRequest(
req.state,
str,
false,
) catch |err| switch (err) {
error.UnsafePaste,
error.UnauthorizedPaste,
=> {
showClipboardConfirmation(
self,
req.state,
str,
);
return;
},
else => {
log.warn(
"failed to complete clipboard request err={}",
.{err},
);
return;
},
};
Surface.signals.@"clipboard-read".impl.emit(
self,
null,
.{},
null,
);
}
/// The request we send as userdata to the clipboard read.
const Request = struct {
/// "Self" is reffed so we can't dispose it until the clipboard
/// read is complete. Callers must unref when done.
self: *Surface,
state: apprt.ClipboardRequest,
};
};

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,36 @@ 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;
}
.clipboard-confirmation-dialog .clipboard-overlay {
border-radius: 10px;
}
.clipboard-confirmation-dialog .clipboard-contents {
filter: blur(0px);
transition: filter 0.3s ease;
border-radius: 10px;
}
.clipboard-confirmation-dialog .clipboard-contents.blurred {
filter: blur(5px);
}

View File

@ -0,0 +1,82 @@
using Gtk 4.0;
// This is unused but if we remove it we get a blueprint-compiler error.
using Adw 1;
template $GhosttyClipboardConfirmationDialog: $GhosttyDialog {
styles [
"clipboard-confirmation-dialog",
]
heading: _("Authorize Clipboard Access");
// 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,
ok: _("Allow") destructive,
]
default-response: "cancel";
close-response: "cancel";
extra-child: ListBox {
selection-mode: none;
Overlay {
styles [
"osd",
"clipboard-overlay",
]
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 200;
TextView text_view {
styles [
"clipboard-contents",
]
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
buffer: bind template.clipboard-contents;
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque",
]
Image {
icon-name: "view-conceal-symbolic";
}
}
}
};
}

View File

@ -0,0 +1,92 @@
using Gtk 4.0;
// This is unused but if we remove it we get a blueprint-compiler error.
using Adw 1;
template $GhosttyClipboardConfirmationDialog: $GhosttyDialog {
styles [
"clipboard-confirmation-dialog",
]
heading: _("Authorize Clipboard Access");
// 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,
ok: _("Allow") destructive,
]
default-response: "cancel";
close-response: "cancel";
extra-child: ListBox {
selection-mode: none;
Overlay {
styles [
"osd",
"clipboard-overlay",
]
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 200;
TextView text_view {
styles [
"clipboard-contents",
]
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
buffer: bind template.clipboard-contents;
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque",
]
Image {
icon-name: "view-conceal-symbolic";
}
}
}
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");
}
};
}

View File

@ -1,3 +1,5 @@
const build_config = @import("../build_config.zig");
/// ContentScale is the ratio between the current DPI and the platform's
/// default DPI. This is used to determine how much certain rendered elements
/// need to be scaled up or down.
@ -50,6 +52,16 @@ pub const ClipboardRequest = union(ClipboardRequestType) {
/// A request to write clipboard contents via OSC 52.
osc_52_write: Clipboard,
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
ClipboardRequest,
.{ .name = "GhosttyClipboardRequest" },
),
.none => void,
};
};
/// The color scheme in use (light vs dark).