mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00
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:
@ -73,9 +73,10 @@ pub fn clipboardRequest(
|
|||||||
clipboard_type: apprt.Clipboard,
|
clipboard_type: apprt.Clipboard,
|
||||||
state: apprt.ClipboardRequest,
|
state: apprt.ClipboardRequest,
|
||||||
) !void {
|
) !void {
|
||||||
_ = self;
|
try self.surface.clipboardRequest(
|
||||||
_ = clipboard_type;
|
clipboard_type,
|
||||||
_ = state;
|
state,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setClipboardString(
|
pub fn setClipboardString(
|
||||||
@ -84,10 +85,11 @@ pub fn setClipboardString(
|
|||||||
clipboard_type: apprt.Clipboard,
|
clipboard_type: apprt.Clipboard,
|
||||||
confirm: bool,
|
confirm: bool,
|
||||||
) !void {
|
) !void {
|
||||||
_ = self;
|
self.surface.setClipboardString(
|
||||||
_ = val;
|
val,
|
||||||
_ = clipboard_type;
|
clipboard_type,
|
||||||
_ = confirm;
|
confirm,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
|
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
|
||||||
|
@ -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.
|
/// These will be asserted to exist at runtime.
|
||||||
pub const blueprints: []const Blueprint = &.{
|
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 = "close-confirmation-dialog" },
|
||||||
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
|
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
|
||||||
.{ .major = 1, .minor = 2, .name = "resize-overlay" },
|
.{ .major = 1, .minor = 2, .name = "resize-overlay" },
|
||||||
|
398
src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig
Normal file
398
src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig
Normal 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;
|
||||||
|
};
|
||||||
|
};
|
@ -2,6 +2,7 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const adw = @import("adw");
|
const adw = @import("adw");
|
||||||
const gdk = @import("gdk");
|
const gdk = @import("gdk");
|
||||||
|
const gio = @import("gio");
|
||||||
const glib = @import("glib");
|
const glib = @import("glib");
|
||||||
const gobject = @import("gobject");
|
const gobject = @import("gobject");
|
||||||
const gtk = @import("gtk");
|
const gtk = @import("gtk");
|
||||||
@ -20,6 +21,7 @@ const Common = @import("../class.zig").Common;
|
|||||||
const Application = @import("application.zig").Application;
|
const Application = @import("application.zig").Application;
|
||||||
const Config = @import("config.zig").Config;
|
const Config = @import("config.zig").Config;
|
||||||
const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
|
const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
|
||||||
|
const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog;
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk_ghostty_surface);
|
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 @"mouse-hidden" = struct {
|
||||||
pub const name = "mouse-hidden";
|
pub const name = "mouse-hidden";
|
||||||
const impl = gobject.ext.defineProperty(
|
const impl = gobject.ext.defineProperty(
|
||||||
@ -168,6 +190,30 @@ pub const Surface = extern struct {
|
|||||||
void,
|
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 {
|
const Private = struct {
|
||||||
@ -195,6 +241,10 @@ pub const Surface = extern struct {
|
|||||||
/// The title of this surface, if any has been set.
|
/// The title of this surface, if any has been set.
|
||||||
title: ?[:0]const u8 = null,
|
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
|
/// The overlay we use for things such as the URL hover label
|
||||||
/// or resize box. Bound from the template.
|
/// or resize box. Bound from the template.
|
||||||
overlay: *gtk.Overlay = undefined,
|
overlay: *gtk.Overlay = undefined,
|
||||||
@ -690,6 +740,32 @@ pub const Surface = extern struct {
|
|||||||
return env;
|
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
|
// Virtual Methods
|
||||||
|
|
||||||
@ -704,6 +780,7 @@ pub const Surface = extern struct {
|
|||||||
priv.cursor_pos = .{ .x = 0, .y = 0 };
|
priv.cursor_pos = .{ .x = 0, .y = 0 };
|
||||||
priv.mouse_shape = .text;
|
priv.mouse_shape = .text;
|
||||||
priv.mouse_hidden = false;
|
priv.mouse_hidden = false;
|
||||||
|
priv.focused = true;
|
||||||
priv.size = .{
|
priv.size = .{
|
||||||
// Funky numbers on purpose so they stand out if for some reason
|
// Funky numbers on purpose so they stand out if for some reason
|
||||||
// our size doesn't get properly set.
|
// our size doesn't get properly set.
|
||||||
@ -1215,30 +1292,41 @@ pub const Surface = extern struct {
|
|||||||
|
|
||||||
fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
|
fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
|
||||||
const priv = self.private();
|
const priv = self.private();
|
||||||
|
priv.focused = true;
|
||||||
|
|
||||||
if (priv.im_context) |im_context| {
|
if (priv.im_context) |im_context| {
|
||||||
im_context.as(gtk.IMContext).focusIn();
|
im_context.as(gtk.IMContext).focusIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (priv.core_surface) |surface| {
|
_ = glib.idleAddOnce(idleFocus, self.ref());
|
||||||
surface.focusCallback(true) catch |err| {
|
|
||||||
log.warn("error in focus callback err={}", .{err});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
|
fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
|
||||||
const priv = self.private();
|
const priv = self.private();
|
||||||
|
priv.focused = false;
|
||||||
|
|
||||||
if (priv.im_context) |im_context| {
|
if (priv.im_context) |im_context| {
|
||||||
im_context.as(gtk.IMContext).focusOut();
|
im_context.as(gtk.IMContext).focusOut();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (priv.core_surface) |surface| {
|
_ = glib.idleAddOnce(idleFocus, self.ref());
|
||||||
surface.focusCallback(false) catch |err| {
|
}
|
||||||
log.warn("error in focus callback err={}", .{err});
|
|
||||||
};
|
/// 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(
|
fn gcMouseDown(
|
||||||
@ -1853,6 +1941,7 @@ pub const Surface = extern struct {
|
|||||||
// Properties
|
// Properties
|
||||||
gobject.ext.registerProperties(class, &.{
|
gobject.ext.registerProperties(class, &.{
|
||||||
properties.config.impl,
|
properties.config.impl,
|
||||||
|
properties.focused.impl,
|
||||||
properties.@"mouse-shape".impl,
|
properties.@"mouse-shape".impl,
|
||||||
properties.@"mouse-hidden".impl,
|
properties.@"mouse-hidden".impl,
|
||||||
properties.@"mouse-hover-url".impl,
|
properties.@"mouse-hover-url".impl,
|
||||||
@ -1862,6 +1951,8 @@ pub const Surface = extern struct {
|
|||||||
|
|
||||||
// Signals
|
// Signals
|
||||||
signals.@"close-request".impl.register(.{});
|
signals.@"close-request".impl.register(.{});
|
||||||
|
signals.@"clipboard-read".impl.register(.{});
|
||||||
|
signals.@"clipboard-write".impl.register(.{});
|
||||||
|
|
||||||
// Virtual methods
|
// Virtual methods
|
||||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||||
@ -1903,3 +1994,255 @@ fn translateMouseButton(button: c_uint) input.MouseButton {
|
|||||||
else => .unknown,
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -4,6 +4,9 @@
|
|||||||
* https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.3/styles-and-appearance.html#custom-styles
|
* https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.3/styles-and-appearance.html#custom-styles
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GhosttySurface URL overlay
|
||||||
|
*/
|
||||||
label.url-overlay {
|
label.url-overlay {
|
||||||
padding: 4px 8px 4px 8px;
|
padding: 4px 8px 4px 8px;
|
||||||
outline-style: solid;
|
outline-style: solid;
|
||||||
@ -23,6 +26,9 @@ label.url-overlay.right {
|
|||||||
border-radius: 6px 0px 0px 0px;
|
border-radius: 6px 0px 0px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GhosttySurface resize overlay
|
||||||
|
*/
|
||||||
.size-overlay label {
|
.size-overlay label {
|
||||||
padding: 4px 8px 4px 8px;
|
padding: 4px 8px 4px 8px;
|
||||||
border-radius: 6px 6px 6px 6px;
|
border-radius: 6px 6px 6px 6px;
|
||||||
@ -30,3 +36,36 @@ label.url-overlay.right {
|
|||||||
outline-width: 1px;
|
outline-width: 1px;
|
||||||
outline-color: #555555;
|
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);
|
||||||
|
}
|
||||||
|
82
src/apprt/gtk-ng/ui/1.0/clipboard-confirmation-dialog.blp
Normal file
82
src/apprt/gtk-ng/ui/1.0/clipboard-confirmation-dialog.blp
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
92
src/apprt/gtk-ng/ui/1.4/clipboard-confirmation-dialog.blp
Normal file
92
src/apprt/gtk-ng/ui/1.4/clipboard-confirmation-dialog.blp
Normal 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");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
const build_config = @import("../build_config.zig");
|
||||||
|
|
||||||
/// ContentScale is the ratio between the current DPI and the platform's
|
/// ContentScale is the ratio between the current DPI and the platform's
|
||||||
/// default DPI. This is used to determine how much certain rendered elements
|
/// default DPI. This is used to determine how much certain rendered elements
|
||||||
/// need to be scaled up or down.
|
/// 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.
|
/// A request to write clipboard contents via OSC 52.
|
||||||
osc_52_write: Clipboard,
|
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).
|
/// The color scheme in use (light vs dark).
|
||||||
|
Reference in New Issue
Block a user