diff --git a/src/apprt/gtk/Builder.zig b/src/apprt/gtk/Builder.zig
index 473abc0f7..028629200 100644
--- a/src/apprt/gtk/Builder.zig
+++ b/src/apprt/gtk/Builder.zig
@@ -9,45 +9,101 @@ const gobject = @import("gobject");
resource_name: [:0]const u8,
builder: ?*gtk.Builder,
-pub fn init(comptime name: []const u8, comptime kind: enum { blp, ui }) Builder {
- comptime {
+pub fn init(
+ /// The "name" of the resource.
+ comptime name: []const u8,
+ /// The major version of the minimum Adwaita version that is required to use
+ /// this resource.
+ comptime major: u16,
+ /// The minor version of the minimum Adwaita version that is required to use
+ /// this resource.
+ comptime minor: u16,
+ /// `blp` signifies that the resource is a Blueprint that has been compiled
+ /// to GTK Builder XML at compile time. `ui` signifies that the resource is
+ /// a GTK Builder XML file that is included in the Ghostty source (perhaps
+ /// because the Blueprint compiler on some target platforms cannot compile a
+ /// Blueprint that generates the necessary resources).
+ comptime kind: enum { blp, ui },
+) Builder {
+ const resource_path = comptime resource_path: {
+ const gresource = @import("gresource.zig");
switch (kind) {
.blp => {
- // Use @embedFile to make sure that the file exists at compile
- // time. Zig _should_ discard the data so that it doesn't end
- // up in the final executable. At runtime we will load the data
- // from a GResource.
- _ = @embedFile("ui/" ++ name ++ ".blp");
-
// Check to make sure that our file is listed as a
// `blueprint_file` in `gresource.zig`. If it isn't Ghostty
// could crash at runtime when we try and load a nonexistent
// GResource.
- const gresource = @import("gresource.zig");
- for (gresource.blueprint_files) |blueprint_file| {
- if (std.mem.eql(u8, blueprint_file.name, name)) break;
+ for (gresource.blueprint_files) |file| {
+ if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
+ // Use @embedFile to make sure that the `.blp` file exists
+ // at compile time. Zig _should_ discard the data so that
+ // it doesn't end up in the final executable. At runtime we
+ // will load the data from a GResource.
+ const blp_filename = std.fmt.comptimePrint(
+ "ui/{d}.{d}/{s}.blp",
+ .{
+ file.major,
+ file.minor,
+ file.name,
+ },
+ );
+ _ = @embedFile(blp_filename);
+ break :resource_path std.fmt.comptimePrint(
+ "/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
+ .{
+ file.major,
+ file.minor,
+ file.name,
+ },
+ );
} else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
},
.ui => {
- // Use @embedFile to make sure that the file exists at compile
- // time. Zig _should_ discard the data so that it doesn't end
- // up in the final executable. At runtime we will load the data
- // from a GResource.
- _ = @embedFile("ui/" ++ name ++ ".ui");
-
// Check to make sure that our file is listed as a `ui_file` in
// `gresource.zig`. If it isn't Ghostty could crash at runtime
// when we try and load a nonexistent GResource.
- const gresource = @import("gresource.zig");
- for (gresource.ui_files) |ui_file| {
- if (std.mem.eql(u8, ui_file, name)) break;
+ for (gresource.ui_files) |file| {
+ if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
+ // Use @embedFile to make sure that the `.ui` file exists
+ // at compile time. Zig _should_ discard the data so that
+ // it doesn't end up in the final executable. At runtime we
+ // will load the data from a GResource.
+ const ui_filename = std.fmt.comptimePrint(
+ "ui/{d}.{d}/{s}.ui",
+ .{
+ file.major,
+ file.minor,
+ file.name,
+ },
+ );
+ _ = @embedFile(ui_filename);
+ // Also use @embedFile to make sure that a matching `.blp`
+ // file exists at compile time. Zig _should_ discard the
+ // data so that it doesn't end up in the final executable.
+ const blp_filename = std.fmt.comptimePrint(
+ "ui/{d}.{d}/{s}.blp",
+ .{
+ file.major,
+ file.minor,
+ file.name,
+ },
+ );
+ _ = @embedFile(blp_filename);
+ break :resource_path std.fmt.comptimePrint(
+ "/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
+ .{
+ file.major,
+ file.minor,
+ file.name,
+ },
+ );
} else @compileError("missing ui file '" ++ name ++ "' in gresource.zig");
},
}
- }
+ };
return .{
- .resource_name = "/com/mitchellh/ghostty/ui/" ++ name ++ ".ui",
+ .resource_name = resource_path,
.builder = null,
};
}
diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig
index cf417b668..632f64fa3 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-read", 1, 5, .blp),
+ .osc_52_write => Builder.init("ccw-osc-52-write", 1, 5, .blp),
+ .paste => Builder.init("ccw-paste", 1, 5, .blp),
+ },
+ adw.MessageDialog => switch (request) {
+ .osc_52_read => Builder.init("ccw-osc-52-read", 1, 2, .ui),
+ .osc_52_write => Builder.init("ccw-osc-52-write", 1, 2, .ui),
+ .paste => Builder.init("ccw-paste", 1, 2, .ui),
+ },
+ else => unreachable,
+ };
+ defer 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..dc8b11d31 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");
@@ -1049,7 +1050,7 @@ pub fn promptTitle(self: *Surface) !void {
if (!adwaita.versionAtLeast(1, 5, 0)) return;
const window = self.container.window() orelse return;
- var builder = Builder.init("prompt-title-dialog", .blp);
+ var builder = Builder.init("prompt-title-dialog", 1, 5, .blp);
defer builder.deinit();
const entry = builder.getObject(gtk.Entry, "title_entry").?;
diff --git a/src/apprt/gtk/blueprint_compiler.zig b/src/apprt/gtk/blueprint_compiler.zig
index 15dd574e5..7a0442e92 100644
--- a/src/apprt/gtk/blueprint_compiler.zig
+++ b/src/apprt/gtk/blueprint_compiler.zig
@@ -15,14 +15,10 @@ pub fn main() !void {
const major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10);
const minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10);
- const micro = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMicroVersion, 10);
const output = it.next() orelse return error.NoOutput;
const input = it.next() orelse return error.NoInput;
- if (c.ADW_MAJOR_VERSION < major or
- (c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION < minor) or
- (c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION == minor and c.ADW_MICRO_VERSION < micro))
- {
+ if (c.ADW_MAJOR_VERSION < major or (c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION < minor)) {
// If the Adwaita version is too old, generate an "empty" file.
const file = try std.fs.createFileAbsolute(output, .{
.truncate = true,
diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig
index 4bd08ed0a..44a1e00bf 100644
--- a/src/apprt/gtk/gresource.zig
+++ b/src/apprt/gtk/gresource.zig
@@ -53,19 +53,31 @@ const icons = [_]struct {
},
};
-pub const ui_files = [_][]const u8{};
+pub const VersionedBuilderXML = struct {
+ major: u16,
+ minor: u16,
+ name: []const u8,
+};
+
+pub const ui_files = [_]VersionedBuilderXML{
+ .{ .major = 1, .minor = 2, .name = "ccw-osc-52-read" },
+ .{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" },
+ .{ .major = 1, .minor = 2, .name = "ccw-paste" },
+};
pub const VersionedBlueprint = struct {
major: u16,
minor: u16,
- micro: u16,
name: []const u8,
};
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, .name = "prompt-title-dialog" },
+ .{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" },
+ .{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" },
+ .{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" },
+ .{ .major = 1, .minor = 5, .name = "ccw-osc-52-write" },
+ .{ .major = 1, .minor = 5, .name = "ccw-paste" },
};
pub fn main() !void {
@@ -119,15 +131,20 @@ pub fn main() !void {
);
for (ui_files) |ui_file| {
try writer.print(
- " src/apprt/gtk/ui/{0s}.ui\n",
- .{ui_file},
+ " src/apprt/gtk/ui/{0d}.{1d}/{2s}.ui\n",
+ .{ ui_file.major, ui_file.minor, ui_file.name },
);
}
for (extra_ui_files.items) |ui_file| {
- try writer.print(
- " {s}\n",
- .{ std.fs.path.basename(ui_file), ui_file },
- );
+ const stem = std.fs.path.stem(ui_file);
+ for (blueprint_files) |file| {
+ if (!std.mem.eql(u8, file.name, stem)) continue;
+ try writer.print(
+ " {s}\n",
+ .{ file.major, file.minor, file.name, ui_file },
+ );
+ break;
+ } else return error.BlueprintNotFound;
}
try writer.writeAll(
\\
@@ -149,11 +166,19 @@ pub const dependencies = deps: {
index += 1;
}
for (ui_files) |ui_file| {
- deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.ui", .{ui_file});
+ deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{d}.{d}/{s}.ui", .{
+ ui_file.major,
+ ui_file.minor,
+ ui_file.name,
+ });
index += 1;
}
for (blueprint_files) |blueprint_file| {
- deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.blp", .{blueprint_file.name});
+ deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{d}.{d}/{s}.blp", .{
+ blueprint_file.major,
+ blueprint_file.minor,
+ blueprint_file.name,
+ });
index += 1;
}
break :deps deps;
diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig
index ef70df1b7..4700321bc 100644
--- a/src/apprt/gtk/menu.zig
+++ b/src/apprt/gtk/menu.zig
@@ -41,7 +41,7 @@ pub fn Menu(
else => unreachable,
};
- var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, .blp);
+ var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, 1, 0, .blp);
defer builder.deinit();
const menu_model = builder.getObject(gio.MenuModel, "menu").?;
diff --git a/src/apprt/gtk/ui/menu-surface-context_menu.blp b/src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp
similarity index 100%
rename from src/apprt/gtk/ui/menu-surface-context_menu.blp
rename to src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp
diff --git a/src/apprt/gtk/ui/menu-window-titlebar_menu.blp b/src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp
similarity index 100%
rename from src/apprt/gtk/ui/menu-window-titlebar_menu.blp
rename to src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp
diff --git a/src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp b/src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp
new file mode 100644
index 000000000..eafa92e1b
--- /dev/null
+++ b/src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp
@@ -0,0 +1,31 @@
+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 {
+ cursor-visible: false;
+ editable: false;
+ monospace: true;
+ top-margin: 8;
+ left-margin: 8;
+ bottom-margin: 8;
+ right-margin: 8;
+ }
+ };
+}
diff --git a/src/apprt/gtk/ui/1.2/ccw-osc-52-read.ui b/src/apprt/gtk/ui/1.2/ccw-osc-52-read.ui
new file mode 100644
index 000000000..9c89835a0
--- /dev/null
+++ b/src/apprt/gtk/ui/1.2/ccw-osc-52-read.ui
@@ -0,0 +1,36 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp b/src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp
new file mode 100644
index 000000000..ecd58929b
--- /dev/null
+++ b/src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp
@@ -0,0 +1,31 @@
+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 {
+ cursor-visible: false;
+ editable: false;
+ monospace: true;
+ top-margin: 8;
+ left-margin: 8;
+ bottom-margin: 8;
+ right-margin: 8;
+ }
+ };
+}
diff --git a/src/apprt/gtk/ui/1.2/ccw-osc-52-write.ui b/src/apprt/gtk/ui/1.2/ccw-osc-52-write.ui
new file mode 100644
index 000000000..a8904de0d
--- /dev/null
+++ b/src/apprt/gtk/ui/1.2/ccw-osc-52-write.ui
@@ -0,0 +1,36 @@
+
+
+
+
+
+ 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
+
+
+ false
+ false
+ true
+ 8
+ 8
+ 8
+ 8
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/apprt/gtk/ui/1.2/ccw-paste.blp b/src/apprt/gtk/ui/1.2/ccw-paste.blp
new file mode 100644
index 000000000..916860368
--- /dev/null
+++ b/src/apprt/gtk/ui/1.2/ccw-paste.blp
@@ -0,0 +1,31 @@
+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 {
+ cursor-visible: false;
+ editable: false;
+ monospace: true;
+ top-margin: 8;
+ left-margin: 8;
+ bottom-margin: 8;
+ right-margin: 8;
+ }
+ };
+}
diff --git a/src/apprt/gtk/ui/1.2/ccw-paste.ui b/src/apprt/gtk/ui/1.2/ccw-paste.ui
new file mode 100644
index 000000000..33e75b3e8
--- /dev/null
+++ b/src/apprt/gtk/ui/1.2/ccw-paste.ui
@@ -0,0 +1,36 @@
+
+
+
+
+
+ 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
+
+
+ false
+ false
+ true
+ 8
+ 8
+ 8
+ 8
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp b/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp
new file mode 100644
index 000000000..3319bc597
--- /dev/null
+++ b/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp
@@ -0,0 +1,31 @@
+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 {
+ cursor-visible: false;
+ editable: false;
+ monospace: true;
+ top-margin: 8;
+ left-margin: 8;
+ bottom-margin: 8;
+ right-margin: 8;
+ }
+ };
+}
diff --git a/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp b/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp
new file mode 100644
index 000000000..8372f0ccb
--- /dev/null
+++ b/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp
@@ -0,0 +1,31 @@
+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 {
+ cursor-visible: false;
+ editable: false;
+ monospace: true;
+ top-margin: 8;
+ left-margin: 8;
+ bottom-margin: 8;
+ right-margin: 8;
+ }
+ };
+}
diff --git a/src/apprt/gtk/ui/1.5/ccw-paste.blp b/src/apprt/gtk/ui/1.5/ccw-paste.blp
new file mode 100644
index 000000000..57aaabfd1
--- /dev/null
+++ b/src/apprt/gtk/ui/1.5/ccw-paste.blp
@@ -0,0 +1,31 @@
+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 {
+ cursor-visible: false;
+ editable: false;
+ monospace: true;
+ top-margin: 8;
+ left-margin: 8;
+ bottom-margin: 8;
+ right-margin: 8;
+ }
+ };
+}
diff --git a/src/apprt/gtk/ui/prompt-title-dialog.blp b/src/apprt/gtk/ui/1.5/prompt-title-dialog.blp
similarity index 100%
rename from src/apprt/gtk/ui/prompt-title-dialog.blp
rename to src/apprt/gtk/ui/1.5/prompt-title-dialog.blp
diff --git a/src/apprt/gtk/ui/README.md b/src/apprt/gtk/ui/README.md
new file mode 100644
index 000000000..08f3f367c
--- /dev/null
+++ b/src/apprt/gtk/ui/README.md
@@ -0,0 +1,21 @@
+# GTK UI files
+
+This directory is for storing GTK resource definitions. With one exception, the
+files should be be in the Blueprint markup language.
+
+Resource files should be stored in directories that represent the minimum
+Adwaita version needed to use that resource. Resource files should also be
+formatted using `blueprint-compiler format` as well to ensure consistency.
+
+The one exception to files being in Blueprint markup language is when Adwaita
+features are used that the `blueprint-compiler` on a supported platform does not
+compile. For example, Debian 12 includes Adwaita 1.2 and `blueprint-compiler`
+0.6.0. Adwaita 1.2 includes support for `MessageDialog` but `blueprint-compiler`
+0.6.0 does not. In cases like that the Blueprint markup should be compiled on a
+platform that provides a new enough `blueprint-compiler` and the resulting `.ui`
+file should be committed to the Ghostty source code. Care should be taken that
+the `.blp` file and the `.ui` file remain in sync.
+
+In all other cases only the `.blp` should be committed to the Ghostty source
+code. The build process will use `blueprint-compiler` to generate the `.ui`
+files necessary at runtime.
diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig
index 65b2b47da..38d4787d6 100644
--- a/src/build/SharedDeps.zig
+++ b/src/build/SharedDeps.zig
@@ -514,10 +514,23 @@ pub fn add(
blueprint_compiler.addArgs(&.{
b.fmt("{d}", .{blueprint_file.major}),
b.fmt("{d}", .{blueprint_file.minor}),
- b.fmt("{d}", .{blueprint_file.micro}),
});
- const ui_file = blueprint_compiler.addOutputFileArg(b.fmt("{s}.ui", .{blueprint_file.name}));
- blueprint_compiler.addFileArg(b.path(b.fmt("src/apprt/gtk/ui/{s}.blp", .{blueprint_file.name})));
+ const ui_file = blueprint_compiler.addOutputFileArg(b.fmt(
+ "{d}.{d}/{s}.ui",
+ .{
+ blueprint_file.major,
+ blueprint_file.minor,
+ blueprint_file.name,
+ },
+ ));
+ blueprint_compiler.addFileArg(b.path(b.fmt(
+ "src/apprt/gtk/ui/{d}.{d}/{s}.blp",
+ .{
+ blueprint_file.major,
+ blueprint_file.minor,
+ blueprint_file.name,
+ },
+ )));
generate.addFileArg(ui_file);
}