gtk: switch clipboard confirmation to zig-gobject and blueprints

Note that for Debian 12, the blueprints must be compiled on a distro
with a newer version of `blueprint-compiler` and the raw UI XML
committed to git. Debian 12 includes `blueprint-compiler` 0.6.0 which
doesn't support compiling `adw.MessageDialog` even though the version of
`libadwaita` supports it.
This commit is contained in:
Jeffrey C. Ollie
2025-02-24 10:22:41 -06:00
parent 9972eeb673
commit 3f847de964
12 changed files with 309 additions and 182 deletions

View File

@ -4,18 +4,24 @@ const ClipboardConfirmation = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const gtk = @import("gtk");
const adw = @import("adw");
const gobject = @import("gobject");
const gio = @import("gio");
const apprt = @import("../../apprt.zig");
const CoreSurface = @import("../../Surface.zig");
const App = @import("App.zig");
const View = @import("View.zig");
const c = @import("c.zig").c;
const Builder = @import("Builder.zig");
const adwaita = @import("adwaita.zig");
const log = std.log.scoped(.gtk);
app: *App,
window: *c.GtkWindow,
view: PrimaryView,
const DialogType = if (adwaita.versionAtLeast(1, 5, 0)) adw.AlertDialog else adw.MessageDialog;
app: *App,
dialog: *DialogType,
data: [:0]u8,
core_surface: *CoreSurface,
pending_req: apprt.ClipboardRequest,
@ -57,201 +63,92 @@ fn init(
core_surface: *CoreSurface,
request: apprt.ClipboardRequest,
) !void {
// Create the window
const window = c.gtk_window_new();
const gtk_window: *c.GtkWindow = @ptrCast(window);
errdefer c.gtk_window_destroy(gtk_window);
c.gtk_window_set_title(gtk_window, titleText(request));
c.gtk_window_set_default_size(gtk_window, 550, 275);
c.gtk_window_set_resizable(gtk_window, 0);
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "clipboard-confirmation-window");
_ = c.g_signal_connect_data(
window,
"destroy",
c.G_CALLBACK(&gtkDestroy),
self,
null,
c.G_CONNECT_DEFAULT,
);
var builder = switch (DialogType) {
adw.AlertDialog => switch (request) {
.osc_52_read => Builder.init("ccw-osc-52-write-15", .blp),
.osc_52_write => Builder.init("ccw-osc-52-write-15", .blp),
.paste => Builder.init("ccw-paste-15", .blp),
},
adw.MessageDialog => switch (request) {
.osc_52_read => Builder.init("ccw-osc-52-write-12", .ui),
.osc_52_write => Builder.init("ccw-osc-52-write-12", .ui),
.paste => Builder.init("ccw-paste-12", .ui),
},
else => unreachable,
};
builder.deinit();
// Set some state
const dialog = builder.getObject(DialogType, "clipboard_confirmation_window").?;
const copy = try app.core_app.alloc.dupeZ(u8, data);
errdefer app.core_app.alloc.free(copy);
self.* = .{
.app = app,
.window = gtk_window,
.view = undefined,
.data = try app.core_app.alloc.dupeZ(u8, data),
.dialog = dialog,
.data = copy,
.core_surface = core_surface,
.pending_req = request,
};
// Show the window
const view = try PrimaryView.init(self, data);
self.view = view;
c.gtk_window_set_child(@ptrCast(window), view.root);
_ = c.gtk_widget_grab_focus(view.buttons.cancel_button);
const text_view = builder.getObject(gtk.TextView, "text_view").?;
c.gtk_widget_show(window);
const buffer = gtk.TextBuffer.new(null);
errdefer buffer.unref();
buffer.insertAtCursor(copy.ptr, @intCast(copy.len));
text_view.setBuffer(buffer);
// Block the main window from input.
// This will auto-revert when the window is closed.
c.gtk_window_set_modal(gtk_window, 1);
switch (DialogType) {
adw.AlertDialog => {
const parent: ?*gtk.Widget = widget: {
const window = core_surface.rt_surface.container.window() orelse break :widget null;
break :widget @ptrCast(@alignCast(window.window));
};
dialog.choose(parent, null, gtkChoose, self);
},
adw.MessageDialog => {
if (adwaita.versionAtLeast(1, 3, 0)) {
dialog.choose(null, gtkChoose, self);
} else {
_ = adw.MessageDialog.signals.response.connect(
dialog,
*ClipboardConfirmation,
gtkResponse,
self,
.{},
);
dialog.as(gtk.Widget).show();
}
},
else => unreachable,
}
}
fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud orelse return));
self.destroy();
}
const PrimaryView = struct {
root: *c.GtkWidget,
text: *c.GtkTextView,
buttons: ButtonsView,
pub fn init(root: *ClipboardConfirmation, data: []const u8) !PrimaryView {
// All our widgets
const label = c.gtk_label_new(promptText(root.pending_req));
const buf = unsafeBuffer(data);
defer c.g_object_unref(buf);
const buttons = try ButtonsView.init(root);
const text_scroll = c.gtk_scrolled_window_new();
errdefer c.g_object_unref(text_scroll);
const text = c.gtk_text_view_new_with_buffer(buf);
errdefer c.g_object_unref(text);
c.gtk_scrolled_window_set_child(@ptrCast(text_scroll), text);
// Create our view
const view = try View.init(&.{
.{ .name = "label", .widget = label },
.{ .name = "text", .widget = text_scroll },
.{ .name = "buttons", .widget = buttons.root },
}, &vfl);
errdefer view.deinit();
// We can do additional settings once the layout is setup
c.gtk_label_set_wrap(@ptrCast(label), 1);
c.gtk_text_view_set_editable(@ptrCast(text), 0);
c.gtk_text_view_set_cursor_visible(@ptrCast(text), 0);
c.gtk_text_view_set_top_margin(@ptrCast(text), 8);
c.gtk_text_view_set_bottom_margin(@ptrCast(text), 8);
c.gtk_text_view_set_left_margin(@ptrCast(text), 8);
c.gtk_text_view_set_right_margin(@ptrCast(text), 8);
c.gtk_text_view_set_monospace(@ptrCast(text), 1);
return .{ .root = view.root, .text = @ptrCast(text), .buttons = buttons };
}
/// Returns the GtkTextBuffer for the data that was unsafe.
fn unsafeBuffer(data: []const u8) *c.GtkTextBuffer {
const buf = c.gtk_text_buffer_new(null);
errdefer c.g_object_unref(buf);
c.gtk_text_buffer_insert_at_cursor(buf, data.ptr, @intCast(data.len));
return buf;
}
const vfl = [_][*:0]const u8{
"H:|-8-[label]-8-|",
"H:|[text]|",
"H:|[buttons]|",
"V:|[label(<=80)][text(>=100)]-[buttons]-|",
};
};
const ButtonsView = struct {
root: *c.GtkWidget,
confirm_button: *c.GtkWidget,
cancel_button: *c.GtkWidget,
pub fn init(root: *ClipboardConfirmation) !ButtonsView {
const cancel_text, const confirm_text = switch (root.pending_req) {
.paste => .{ "Cancel", "Paste" },
.osc_52_read, .osc_52_write => .{ "Deny", "Allow" },
};
const cancel_button = c.gtk_button_new_with_label(cancel_text);
errdefer c.g_object_unref(cancel_button);
const confirm_button = c.gtk_button_new_with_label(confirm_text);
errdefer c.g_object_unref(confirm_button);
c.gtk_widget_add_css_class(confirm_button, "destructive-action");
c.gtk_widget_add_css_class(cancel_button, "suggested-action");
// Create our view
const view = try View.init(&.{
.{ .name = "cancel", .widget = cancel_button },
.{ .name = "confirm", .widget = confirm_button },
}, &vfl);
// Signals
_ = c.g_signal_connect_data(
cancel_button,
"clicked",
c.G_CALLBACK(&gtkCancelClick),
root,
null,
c.G_CONNECT_DEFAULT,
);
_ = c.g_signal_connect_data(
confirm_button,
"clicked",
c.G_CALLBACK(&gtkConfirmClick),
root,
null,
c.G_CONNECT_DEFAULT,
);
return .{ .root = view.root, .confirm_button = confirm_button, .cancel_button = cancel_button };
}
fn gtkCancelClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud));
c.gtk_window_destroy(@ptrCast(self.window));
}
fn gtkConfirmClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
// Requeue the paste with force.
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud));
fn gtkChoose(dialog_: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void {
const dialog = gobject.ext.cast(DialogType, dialog_.?).?;
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud.?));
const response = dialog.chooseFinish(result);
if (std.mem.orderZ(u8, response, "ok") == .eq) {
self.core_surface.completeClipboardRequest(
self.pending_req,
self.data,
true,
) catch |err| {
std.log.err("Failed to requeue clipboard request: {}", .{err});
log.err("Failed to requeue clipboard request: {}", .{err});
};
c.gtk_window_destroy(@ptrCast(self.window));
}
const vfl = [_][*:0]const u8{
"H:[cancel]-8-[confirm]-8-|",
};
};
/// The title of the window, based on the reason the prompt is being shown.
fn titleText(req: apprt.ClipboardRequest) [:0]const u8 {
return switch (req) {
.paste => "Warning: Potentially Unsafe Paste",
.osc_52_read, .osc_52_write => "Authorize Clipboard Access",
};
self.destroy();
}
/// The text to display in the prompt window, based on the reason the prompt
/// is being shown.
fn promptText(req: apprt.ClipboardRequest) [:0]const u8 {
return switch (req) {
.paste =>
\\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.
,
.osc_52_read =>
\\An application is attempting to read from the clipboard.
\\The current clipboard contents are shown below.
,
.osc_52_write =>
\\An application is attempting to write to the clipboard.
\\The content to write is shown below.
,
};
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.C) void {
if (std.mem.orderZ(u8, response, "ok") == .eq) {
self.core_surface.completeClipboardRequest(
self.pending_req,
self.data,
true,
) catch |err| {
log.err("Failed to requeue clipboard request: {}", .{err});
};
}
self.destroy();
}

View File

@ -8,6 +8,7 @@ const adw = @import("adw");
const gtk = @import("gtk");
const gio = @import("gio");
const gobject = @import("gobject");
const Allocator = std.mem.Allocator;
const build_config = @import("../../build_config.zig");
const build_options = @import("build_options");

View File

@ -53,7 +53,11 @@ const icons = [_]struct {
},
};
pub const ui_files = [_][]const u8{};
pub const ui_files = [_][]const u8{
"ccw-osc-52-read-12",
"ccw-osc-52-write-12",
"ccw-paste-12",
};
pub const VersionedBlueprint = struct {
major: u16,
@ -66,6 +70,9 @@ pub const blueprint_files = [_]VersionedBlueprint{
.{ .major = 1, .minor = 5, .micro = 0, .name = "prompt-title-dialog" },
.{ .major = 1, .minor = 0, .micro = 0, .name = "menu-surface-context_menu" },
.{ .major = 1, .minor = 0, .micro = 0, .name = "menu-window-titlebar_menu" },
.{ .major = 1, .minor = 5, .micro = 0, .name = "ccw-osc-52-read-15" },
.{ .major = 1, .minor = 5, .micro = 0, .name = "ccw-osc-52-write-15" },
.{ .major = 1, .minor = 5, .micro = 0, .name = "ccw-paste-15" },
};
pub fn main() !void {

View File

@ -0,0 +1,23 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.MessageDialog clipboard_confirmation_window {
heading: _("Authorize Clipboard Access");
body: _("An application is attempting to read from the clipboard. The current clipboard contents are shown below.");
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive
]
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
TextView text_view {}
};
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
DO NOT EDIT!
This file was @generated by blueprint-compiler. Instead, edit the
corresponding .blp file and regenerate this file with blueprint-compiler.
-->
<interface domain="com.mitchellh.ghostty">
<requires lib="gtk" version="4.0"/>
<object class="AdwMessageDialog" id="clipboard_confirmation_window">
<property name="heading" translatable="yes">Authorize Clipboard Access</property>
<property name="body" translatable="yes">An application is attempting to read from the clipboard. The current clipboard contents are shown below.</property>
<responses>
<response id="cancel" translatable="yes" appearance="suggested">Deny</response>
<response id="ok" translatable="yes" appearance="destructive">Allow</response>
</responses>
<property name="default-response">cancel</property>
<property name="close-response">cancel</property>
<property name="extra-child">
<object class="GtkScrolledWindow">
<property name="width-request">500</property>
<property name="height-request">250</property>
<child>
<object class="GtkTextView" id="text_view"></object>
</child>
</object>
</property>
</object>
</interface>

View File

@ -0,0 +1,23 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.AlertDialog clipboard_confirmation_window {
heading: _("Authorize Clipboard Access");
body: _("An application is attempting to read from the clipboard. The current clipboard contents are shown below.");
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive
]
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
TextView text_view {}
};
}

View File

@ -0,0 +1,23 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.MessageDialog clipboard_confirmation_window {
heading: _("Authorize Clipboard Access");
body: _("An application is attempting to write to the clipboard. The current clipboard contents are shown below.");
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive
]
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
TextView text_view {}
};
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
DO NOT EDIT!
This file was @generated by blueprint-compiler. Instead, edit the
corresponding .blp file and regenerate this file with blueprint-compiler.
-->
<interface domain="com.mitchellh.ghostty">
<requires lib="gtk" version="4.0"/>
<object class="AdwMessageDialog" id="clipboard_confirmation_window">
<property name="heading" translatable="yes">Authorize Clipboard Access</property>
<property name="body" translatable="yes">An application is attempting to write to the clipboard. The current clipboard contents are shown below.</property>
<responses>
<response id="cancel" translatable="yes" appearance="suggested">Deny</response>
<response id="ok" translatable="yes" appearance="destructive">Allow</response>
</responses>
<property name="default-response">cancel</property>
<property name="close-response">cancel</property>
<property name="extra-child">
<object class="GtkScrolledWindow">
<property name="width-request">500</property>
<property name="height-request">250</property>
<child>
<object class="GtkTextView" id="text_view"></object>
</child>
</object>
</property>
</object>
</interface>

View File

@ -0,0 +1,23 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.AlertDialog clipboard_confirmation_window {
heading: _("Authorize Clipboard Access");
body: _("An application is attempting to write to the clipboard. The current clipboard contents are shown below.");
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive
]
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
TextView text_view {}
};
}

View File

@ -0,0 +1,23 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.MessageDialog clipboard_confirmation_window {
heading: _("Warning: Potentially Unsafe Paste");
body: _("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.");
responses [
cancel: _("Cancel") suggested,
ok: _("Paste") destructive
]
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
TextView text_view {}
};
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
DO NOT EDIT!
This file was @generated by blueprint-compiler. Instead, edit the
corresponding .blp file and regenerate this file with blueprint-compiler.
-->
<interface domain="com.mitchellh.ghostty">
<requires lib="gtk" version="4.0"/>
<object class="AdwMessageDialog" id="clipboard_confirmation_window">
<property name="heading" translatable="yes">Warning: Potentially Unsafe Paste</property>
<property name="body" translatable="yes">Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.</property>
<responses>
<response id="cancel" translatable="yes" appearance="suggested">Cancel</response>
<response id="ok" translatable="yes" appearance="destructive">Paste</response>
</responses>
<property name="default-response">cancel</property>
<property name="close-response">cancel</property>
<property name="extra-child">
<object class="GtkScrolledWindow">
<property name="width-request">500</property>
<property name="height-request">250</property>
<child>
<object class="GtkTextView" id="text_view"></object>
</child>
</object>
</property>
</object>
</interface>

View File

@ -0,0 +1,23 @@
using Gtk 4.0;
using Adw 1;
translation-domain "com.mitchellh.ghostty";
Adw.AlertDialog clipboard_confirmation_window {
heading: _("Warning: Potentially Unsafe Paste");
body: _("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.");
responses [
cancel: _("Cancel") suggested,
ok: _("Paste") destructive
]
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
TextView text_view {}
};
}