From 3f847de9644c7e009fd5d87a2c20286dd09f9345 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 24 Feb 2025 10:22:41 -0600 Subject: [PATCH] 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. --- src/apprt/gtk/ClipboardConfirmationWindow.zig | 259 ++++++------------ src/apprt/gtk/Surface.zig | 1 + src/apprt/gtk/gresource.zig | 9 +- src/apprt/gtk/ui/ccw-osc-52-read-12.blp | 23 ++ src/apprt/gtk/ui/ccw-osc-52-read-12.ui | 28 ++ src/apprt/gtk/ui/ccw-osc-52-read-15.blp | 23 ++ src/apprt/gtk/ui/ccw-osc-52-write-12.blp | 23 ++ src/apprt/gtk/ui/ccw-osc-52-write-12.ui | 28 ++ src/apprt/gtk/ui/ccw-osc-52-write-15.blp | 23 ++ src/apprt/gtk/ui/ccw-paste-12.blp | 23 ++ src/apprt/gtk/ui/ccw-paste-12.ui | 28 ++ src/apprt/gtk/ui/ccw-paste-15.blp | 23 ++ 12 files changed, 309 insertions(+), 182 deletions(-) create mode 100644 src/apprt/gtk/ui/ccw-osc-52-read-12.blp create mode 100644 src/apprt/gtk/ui/ccw-osc-52-read-12.ui create mode 100644 src/apprt/gtk/ui/ccw-osc-52-read-15.blp create mode 100644 src/apprt/gtk/ui/ccw-osc-52-write-12.blp create mode 100644 src/apprt/gtk/ui/ccw-osc-52-write-12.ui create mode 100644 src/apprt/gtk/ui/ccw-osc-52-write-15.blp create mode 100644 src/apprt/gtk/ui/ccw-paste-12.blp create mode 100644 src/apprt/gtk/ui/ccw-paste-12.ui create mode 100644 src/apprt/gtk/ui/ccw-paste-15.blp diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig index cf417b668..d1494d0ae 100644 --- a/src/apprt/gtk/ClipboardConfirmationWindow.zig +++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig @@ -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(>kDestroy), - 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(>kCancelClick), - root, - null, - c.G_CONNECT_DEFAULT, - ); - _ = c.g_signal_connect_data( - confirm_button, - "clicked", - c.G_CALLBACK(>kConfirmClick), - 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(); } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 09d1c5a90..06a1576d3 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -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"); diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 4bd08ed0a..32995c924 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -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 { diff --git a/src/apprt/gtk/ui/ccw-osc-52-read-12.blp b/src/apprt/gtk/ui/ccw-osc-52-read-12.blp new file mode 100644 index 000000000..a2ee10e34 --- /dev/null +++ b/src/apprt/gtk/ui/ccw-osc-52-read-12.blp @@ -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 {} + }; +} diff --git a/src/apprt/gtk/ui/ccw-osc-52-read-12.ui b/src/apprt/gtk/ui/ccw-osc-52-read-12.ui new file mode 100644 index 000000000..3d8743221 --- /dev/null +++ b/src/apprt/gtk/ui/ccw-osc-52-read-12.ui @@ -0,0 +1,28 @@ + + + + + + Authorize Clipboard Access + An application is attempting to read from the clipboard. The current clipboard contents are shown below. + + Deny + Allow + + cancel + cancel + + + 500 + 250 + + + + + + + \ No newline at end of file diff --git a/src/apprt/gtk/ui/ccw-osc-52-read-15.blp b/src/apprt/gtk/ui/ccw-osc-52-read-15.blp new file mode 100644 index 000000000..a1230b9c6 --- /dev/null +++ b/src/apprt/gtk/ui/ccw-osc-52-read-15.blp @@ -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 {} + }; +} diff --git a/src/apprt/gtk/ui/ccw-osc-52-write-12.blp b/src/apprt/gtk/ui/ccw-osc-52-write-12.blp new file mode 100644 index 000000000..97368cc21 --- /dev/null +++ b/src/apprt/gtk/ui/ccw-osc-52-write-12.blp @@ -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 {} + }; +} diff --git a/src/apprt/gtk/ui/ccw-osc-52-write-12.ui b/src/apprt/gtk/ui/ccw-osc-52-write-12.ui new file mode 100644 index 000000000..3febbd558 --- /dev/null +++ b/src/apprt/gtk/ui/ccw-osc-52-write-12.ui @@ -0,0 +1,28 @@ + + + + + + Authorize Clipboard Access + An application is attempting to write to the clipboard. The current clipboard contents are shown below. + + Deny + Allow + + cancel + cancel + + + 500 + 250 + + + + + + + \ No newline at end of file diff --git a/src/apprt/gtk/ui/ccw-osc-52-write-15.blp b/src/apprt/gtk/ui/ccw-osc-52-write-15.blp new file mode 100644 index 000000000..df36b9153 --- /dev/null +++ b/src/apprt/gtk/ui/ccw-osc-52-write-15.blp @@ -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 {} + }; +} diff --git a/src/apprt/gtk/ui/ccw-paste-12.blp b/src/apprt/gtk/ui/ccw-paste-12.blp new file mode 100644 index 000000000..3a530f56d --- /dev/null +++ b/src/apprt/gtk/ui/ccw-paste-12.blp @@ -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 {} + }; +} diff --git a/src/apprt/gtk/ui/ccw-paste-12.ui b/src/apprt/gtk/ui/ccw-paste-12.ui new file mode 100644 index 000000000..e2f852f7f --- /dev/null +++ b/src/apprt/gtk/ui/ccw-paste-12.ui @@ -0,0 +1,28 @@ + + + + + + Warning: Potentially Unsafe Paste + Pasting this text into the terminal may be dangerous as it looks like some commands may be executed. + + Cancel + Paste + + cancel + cancel + + + 500 + 250 + + + + + + + \ No newline at end of file diff --git a/src/apprt/gtk/ui/ccw-paste-15.blp b/src/apprt/gtk/ui/ccw-paste-15.blp new file mode 100644 index 000000000..8703071a6 --- /dev/null +++ b/src/apprt/gtk/ui/ccw-paste-15.blp @@ -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 {} + }; +}