gtk: switch clipboard confirmation to zig-gobject and blueprints (#5968)

This commit is contained in:
Leah Amelia Chen
2025-02-25 23:28:56 +01:00
committed by GitHub
20 changed files with 529 additions and 226 deletions

View File

@ -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,
};
}

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-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(&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");
@ -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").?;

View File

@ -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,

View File

@ -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(
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{0s}.ui\">src/apprt/gtk/ui/{0s}.ui</file>\n",
.{ui_file},
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{0d}.{1d}/{2s}.ui\">src/apprt/gtk/ui/{0d}.{1d}/{2s}.ui</file>\n",
.{ ui_file.major, ui_file.minor, ui_file.name },
);
}
for (extra_ui_files.items) |ui_file| {
try writer.print(
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{s}\">{s}</file>\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(
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{d}.{d}/{s}.ui\">{s}</file>\n",
.{ file.major, file.minor, file.name, ui_file },
);
break;
} else return error.BlueprintNotFound;
}
try writer.writeAll(
\\ </gresource>
@ -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;

View File

@ -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").?;

View File

@ -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;
}
};
}

View File

@ -0,0 +1,36 @@
<?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">
<property name="cursor-visible">false</property>
<property name="editable">false</property>
<property name="monospace">true</property>
<property name="top-margin">8</property>
<property name="left-margin">8</property>
<property name="bottom-margin">8</property>
<property name="right-margin">8</property>
</object>
</child>
</object>
</property>
</object>
</interface>

View File

@ -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;
}
};
}

View File

@ -0,0 +1,36 @@
<?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">
<property name="cursor-visible">false</property>
<property name="editable">false</property>
<property name="monospace">true</property>
<property name="top-margin">8</property>
<property name="left-margin">8</property>
<property name="bottom-margin">8</property>
<property name="right-margin">8</property>
</object>
</child>
</object>
</property>
</object>
</interface>

View File

@ -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;
}
};
}

View File

@ -0,0 +1,36 @@
<?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">
<property name="cursor-visible">false</property>
<property name="editable">false</property>
<property name="monospace">true</property>
<property name="top-margin">8</property>
<property name="left-margin">8</property>
<property name="bottom-margin">8</property>
<property name="right-margin">8</property>
</object>
</child>
</object>
</property>
</object>
</interface>

View File

@ -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;
}
};
}

View File

@ -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;
}
};
}

View File

@ -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;
}
};
}

View File

@ -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.

View File

@ -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);
}