gtk: autogenerate GTK resource file from blueprints

Fixes #6760

Remove all GTK resource definitions (`.ui` files) from the repo.
Building from a git checkout requires `blueprint-compiler` version
0.16.0 at build time to compile the Blueprints (`.blp` files) into GTK
resource definitions.

A script (accessed by `nix run .#compile-blueprints`) is provided
to compile all of the blueprints in the repository for inclusion in
generated tarballs. A CI job is also added to ensure that the Blueprint
files are valid and formatter properly.

Workflows for tag and tip will generate source tarballs that contain
generated GTK resource definitions for use on distributions that do not
provide a new enough `blueprint-compiler` (I'm looking at you Debian
12).

Building from generated tarballs can use the pre-generated GTK resource
definitions by adding the `-fno-sys=blueprint-compiler` flag to the
Zig build.
This commit is contained in:
Jeffrey C. Ollie
2025-03-16 21:48:23 -05:00
parent c344c320eb
commit 59b5f8d3cc
28 changed files with 344 additions and 529 deletions

View File

@ -89,10 +89,13 @@ jobs:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: compile blueprints
run: nix run .#compile-blueprints
- name: Create Tarball
run: |
git archive --format=tgz --prefix="ghostty-${GHOSTTY_VERSION}/" -o "ghostty-${GHOSTTY_VERSION}.tar.gz" HEAD
git archive --format=tgz --prefix=ghostty-source/ -o ghostty-source.tar.gz HEAD
git archive --format=tgz $(find src -name \*.ui -print0 | xargs --null --replace=UI echo --add-virtual-file=ghostty-${GHOSTTY_VERSION}/UI:UI) --prefix="ghostty-${GHOSTTY_VERSION}/" -o "ghostty-${GHOSTTY_VERSION}.tar.gz" HEAD
git archive --format=tgz $(find src -name \*.ui -print0 | xargs --null --replace=UI echo --add-virtual-file=ghostty-source/UI:UI --prefix=ghostty-source/) -o ghostty-source.tar.gz HEAD
- name: Sign Tarball
run: |

View File

@ -110,8 +110,10 @@ jobs:
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: compile blueprints
run: nix run .#compile-blueprints
- name: Create Tarball
run: git archive --format=tgz --prefix=ghostty-source/ -o ghostty-source.tar.gz HEAD
run: git archive --format=tgz $(find src -name \*.ui -print0 | xargs --null --replace=UI echo --add-virtual-file=ghostty-source/UI:UI --prefix=ghostty-source/) -o ghostty-source.tar.gz HEAD
- name: Sign Tarball
run: |
echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key

View File

@ -27,6 +27,7 @@ jobs:
- prettier
- alejandra
- typos
- blueprint-compiler
- translations
- test-pkg-linux
- test-debian-12
@ -609,6 +610,35 @@ jobs:
- name: prettier check
run: nix develop -c prettier --check .
blueprint-compiler:
if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-sm
timeout-minutes: 60
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@v4 # Check out repo so we can lint it
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.0
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@v30
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v15
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
skipPush: true
useDaemon: false # sometimes fails on short jobs
- name: compile blueprints
run: nix run .#compile-blueprints
- name: check unchanged
run: git diff --exit-code
alejandra:
if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-sm
@ -742,6 +772,26 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.0
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v30
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v15
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
# use Nix to compile blueprints since Debian 12 doesn't have a new enough version
- name: compile blueprints
run: nix run .#compile-blueprints
- name: Install and configure Namespace CLI
uses: namespacelabs/nscloud-setup@v0

1
.gitignore vendored
View File

@ -14,6 +14,7 @@ zig-out/
example/*.wasm
test/ghostty
test/cases/**/*.actual.png
src/apprt/gtk/ui/**/*.ui
glad.zip
/Box_test.ppm

View File

@ -52,6 +52,8 @@
zig = zig.packages.${system}."0.14.0";
wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
zon2nix = zon2nix;
# revert once blueprint-compiler 0.16.0 is in nixpkgs-stable
blueprint-compiler = pkgs-unstable.blueprint-compiler;
};
packages.${system} = let
@ -99,6 +101,10 @@
x11-gnome = runVM ./nix/vm/x11-gnome.nix;
x11-plasma6 = runVM ./nix/vm/x11-plasma6.nix;
x11-xfce = runVM ./nix/vm/x11-xfce.nix;
compile-blueprints = import ./nix/build-support/compile-blueprints.nix {
# change once Zig 0.14 and blueprints-compiler 0.16 are in a stable release of nixpkgs
pkgs = pkgs-unstable;
};
};
}
# Our supported systems are the same supported systems as the Zig binaries.

BIN
ghostty-source.tar.gz Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
{pkgs}: let
# this needs to be kept in sync with deps from devShell.nix and package.nix
gi_typelib_path = pkgs.lib.makeSearchPath "lib/girepository-1.0" (map (pkg: pkgs.lib.getOutput "lib" pkg) [
pkgs.cairo
pkgs.gdk-pixbuf
pkgs.glib
pkgs.gobject-introspection
pkgs.graphene
pkgs.gtk4
pkgs.gtk4-layer-shell
pkgs.harfbuzz
pkgs.libadwaita
pkgs.pango
]);
program = pkgs.writeShellScript "compile-blueprints" ''
set -e
${pkgs.findutils}/bin/find . -name \*.blp -print0 | ${pkgs.findutils}/bin/xargs --null --replace=BLP -- ${pkgs.lib.getExe pkgs.blueprint-compiler} format --fix BLP
${pkgs.findutils}/bin/find . -name \*.blp -print0 | ${pkgs.findutils}/bin/xargs --null --replace=BLP -- sh -c "export B=BLP; ${pkgs.lib.getExe pkgs.blueprint-compiler} compile --typelib-path=${gi_typelib_path} --output \''${B%.*}.ui \$B"
'';
in {
type = "app";
program = "${program}";
}

View File

@ -63,7 +63,23 @@
wayland-protocols,
zon2nix,
system,
cairo,
gdk-pixbuf,
graphene,
pango,
}: let
gi_typelib_path = [
cairo
gdk-pixbuf
glib
gobject-introspection
graphene
gtk4
gtk4-layer-shell
harfbuzz
libadwaita
pango
];
# See package.nix. Keep in sync.
rpathLibs =
[
@ -187,6 +203,7 @@ in
# This should be set onto the rpath of the ghostty binary if you want
# it to be "portable" across the system.
LD_LIBRARY_PATH = lib.makeLibraryPath rpathLibs;
GI_TYPELIB_PATH = lib.makeSearchPath "lib/girepository-1.0" (map (pkg: lib.getOutput "lib" pkg) gi_typelib_path);
shellHook =
(lib.optionalString stdenv.hostPlatform.isLinux ''

View File

@ -18,17 +18,9 @@ pub fn init(
/// 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 => {
// 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
@ -57,49 +49,6 @@ pub fn init(
},
);
} else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
},
.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.
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 .{

View File

@ -72,14 +72,14 @@ fn init(
) !void {
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),
.osc_52_read => Builder.init("ccw-osc-52-read", 1, 5),
.osc_52_write => Builder.init("ccw-osc-52-write", 1, 5),
.paste => Builder.init("ccw-paste", 1, 5),
},
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),
.osc_52_read => Builder.init("ccw-osc-52-read", 1, 2),
.osc_52_write => Builder.init("ccw-osc-52-write", 1, 2),
.paste => Builder.init("ccw-paste", 1, 2),
},
else => unreachable,
};

View File

@ -1062,7 +1062,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", 1, 5, .blp);
var builder = Builder.init("prompt-title-dialog", 1, 5);
defer builder.deinit();
const entry = builder.getObject(gtk.Entry, "title_entry").?;

View File

@ -4,9 +4,12 @@ pub const c = @cImport({
@cInclude("adwaita.h");
});
const required_version = "0.16.0";
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
const alloc = debug_allocator.allocator();
var it = try std.process.argsWithAllocator(alloc);
defer it.deinit();
@ -19,20 +22,81 @@ pub fn main() !void {
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)) {
// If the Adwaita version is too old, generate an "empty" file.
const file = try std.fs.createFileAbsolute(output, .{
.truncate = true,
});
try file.writeAll(
\\<?xml version="1.0" encoding="UTF-8"?>
\\<interface domain="com.mitchellh.ghostty"/>
);
defer file.close();
return;
std.log.err(
\\`libadwaita` is too old.
\\
\\Ghostty requires a version {d}.{d} or newer of `libadwaita` to
\\compile this blueprint. Please install it, ensure that it is
\\available on your PATH, and then retry building Ghostty.
, .{ major, minor });
std.posix.exit(1);
}
var compiler = std.process.Child.init(
{
var stdout: std.ArrayListUnmanaged(u8) = .empty;
defer stdout.deinit(alloc);
var stderr: std.ArrayListUnmanaged(u8) = .empty;
defer stderr.deinit(alloc);
var blueprint_compiler = std.process.Child.init(
&.{
"blueprint-compiler",
"--version",
},
alloc,
);
blueprint_compiler.stdout_behavior = .Pipe;
blueprint_compiler.stderr_behavior = .Pipe;
try blueprint_compiler.spawn();
try blueprint_compiler.collectOutput(
alloc,
&stdout,
&stderr,
std.math.maxInt(u16),
);
const term = blueprint_compiler.wait() catch |err| switch (err) {
error.FileNotFound => {
std.log.err(
\\`blueprint-compiler` not found.
\\
\\Ghostty requires `blueprint-compiler` version {s} as
\\a build-time dependency starting from version 1.2. Please
\\install it, ensure that it is available on your PATH, and
\\then retry building Ghostty.
\\
, .{required_version});
std.posix.exit(1);
},
else => return err,
};
switch (term) {
.Exited => |rc| {
if (rc != 0) std.process.exit(1);
},
else => std.process.exit(1),
}
const version = std.mem.trim(u8, stdout.items, &std.ascii.whitespace);
if (!std.mem.eql(u8, version, "0.16.0")) {
std.log.err(
\\`blueprint-compiler` is the wrong version.
\\
\\Ghostty requires `blueprint-compiler` version {s} as
\\a build-time dependency starting from version 1.2. Please
\\install it, ensure that it is available on your PATH, and
\\then retry building Ghostty.
\\
, .{required_version});
std.posix.exit(1);
}
}
{
var stdout: std.ArrayListUnmanaged(u8) = .empty;
defer stdout.deinit(alloc);
var stderr: std.ArrayListUnmanaged(u8) = .empty;
defer stderr.deinit(alloc);
var blueprint_compiler = std.process.Child.init(
&.{
"blueprint-compiler",
"compile",
@ -42,24 +106,41 @@ pub fn main() !void {
},
alloc,
);
const term = compiler.spawnAndWait() catch |err| switch (err) {
blueprint_compiler.stdout_behavior = .Pipe;
blueprint_compiler.stderr_behavior = .Pipe;
try blueprint_compiler.spawn();
try blueprint_compiler.collectOutput(
alloc,
&stdout,
&stderr,
std.math.maxInt(u16),
);
const term = blueprint_compiler.wait() catch |err| switch (err) {
error.FileNotFound => {
std.log.err(
\\`blueprint-compiler` not found.
\\
\\Ghostty requires `blueprint-compiler` as a build-time dependency starting from version 1.2.
\\Please install it, ensure that it is available on your PATH, and then retry building Ghostty.
, .{});
\\Ghostty requires `blueprint-compiler` version {s} as
\\a build-time dependency starting from version 1.2. Please
\\install it, ensure that it is available on your PATH, and
\\then retry building Ghostty.
\\
, .{required_version});
std.posix.exit(1);
},
else => return err,
};
switch (term) {
.Exited => |rc| {
if (rc != 0) std.process.exit(1);
if (rc != 0) {
std.debug.print("{s}", .{stderr.items});
std.process.exit(1);
}
},
else => std.process.exit(1),
else => {
std.debug.print("{s}", .{stderr.items});
std.process.exit(1);
},
}
}
}

View File

@ -1,32 +0,0 @@
const std = @import("std");
const build_options = @import("build_options");
const gtk = @import("gtk");
const adw = @import("adw");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
const filename = filename: {
var it = try std.process.argsWithAllocator(alloc);
defer it.deinit();
_ = it.next() orelse return error.NoFilename;
break :filename try alloc.dupeZ(u8, it.next() orelse return error.NoFilename);
};
defer alloc.free(filename);
const data = try std.fs.cwd().readFileAllocOptions(alloc, filename, std.math.maxInt(u16), null, 1, 0);
defer alloc.free(data);
if (gtk.initCheck() == 0) {
std.debug.print("{s}: skipping builder check because we can't connect to display!\n", .{filename});
return;
}
adw.init();
const builder = gtk.Builder.newFromString(data.ptr, @intCast(data.len));
defer builder.unref();
}

View File

@ -53,18 +53,6 @@ const icons = [_]struct {
},
};
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,
@ -75,27 +63,33 @@ pub const blueprint_files = [_]VersionedBlueprint{
.{ .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 = 2, .name = "ccw-osc-52-read" },
.{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" },
.{ .major = 1, .minor = 2, .name = "ccw-paste" },
.{ .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 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
const alloc = debug_allocator.allocator();
var extra_ui_files = std.ArrayList([]const u8).init(alloc);
var extra_ui_files: std.ArrayListUnmanaged([]const u8) = .empty;
defer {
for (extra_ui_files.items) |item| alloc.free(item);
extra_ui_files.deinit();
extra_ui_files.deinit(alloc);
}
var it = try std.process.argsWithAllocator(alloc);
defer it.deinit();
_ = it.next();
while (it.next()) |argument| {
if (std.mem.eql(u8, std.fs.path.extension(argument), ".ui")) {
try extra_ui_files.append(try alloc.dupe(u8, argument));
try extra_ui_files.append(alloc, try alloc.dupe(u8, argument));
}
}
@ -129,16 +123,11 @@ pub fn main() !void {
\\ <gresource prefix="/com/mitchellh/ghostty/ui">
\\
);
for (ui_files) |ui_file| {
try writer.print(
" <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| {
const stem = std.fs.path.stem(ui_file);
for (blueprint_files) |file| {
if (!std.mem.eql(u8, file.name, stem)) continue;
const expected = try std.fmt.allocPrint(alloc, "/{d}.{d}/{s}.ui", .{ file.major, file.minor, file.name });
defer alloc.free(expected);
if (!std.mem.endsWith(u8, ui_file, expected)) 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 },
@ -154,7 +143,7 @@ pub fn main() !void {
}
pub const dependencies = deps: {
const total = css_files.len + icons.len + ui_files.len + blueprint_files.len;
const total = css_files.len + icons.len;
var deps: [total][]const u8 = undefined;
var index: usize = 0;
for (css_files) |css_file| {
@ -165,21 +154,5 @@ pub const dependencies = deps: {
deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
index += 1;
}
for (ui_files) |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/{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, 1, 0, .blp);
var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, 1, 0);
defer builder.deinit();
const menu_model = builder.getObject(gio.MenuModel, "menu").?;

View File

@ -8,7 +8,7 @@ Adw.MessageDialog clipboard_confirmation_window {
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive
ok: _("Allow") destructive,
]
default-response: "cancel";
@ -16,7 +16,7 @@ Adw.MessageDialog clipboard_confirmation_window {
extra-child: Overlay {
styles [
"osd"
"osd",
]
ScrolledWindow text_view_scroll {
@ -33,7 +33,7 @@ Adw.MessageDialog clipboard_confirmation_window {
right-margin: 8;
styles [
"clipboard-content-view"
"clipboard-content-view",
]
}
}
@ -60,7 +60,7 @@ Adw.MessageDialog clipboard_confirmation_window {
margin-top: 12;
styles [
"opaque"
"opaque",
]
Image {

View File

@ -1,77 +0,0 @@
<?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="true">Authorize Clipboard Access</property>
<property name="body" translatable="true">An application is attempting to read from the clipboard. The current clipboard contents are shown below.</property>
<responses>
<response id="cancel" translatable="true" appearance="suggested">Deny</response>
<response id="ok" translatable="true" appearance="destructive">Allow</response>
</responses>
<property name="default-response">cancel</property>
<property name="close-response">cancel</property>
<property name="extra-child">
<object class="GtkOverlay">
<style>
<class name="osd"/>
</style>
<child>
<object class="GtkScrolledWindow" id="text_view_scroll">
<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>
<style>
<class name="clipboard-content-view"/>
</style>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="reveal_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<child>
<object class="GtkImage">
<property name="icon-name">view-reveal-symbolic</property>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="hide_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<style>
<class name="opaque"/>
</style>
<child>
<object class="GtkImage">
<property name="icon-name">view-conceal-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</interface>

View File

@ -8,7 +8,7 @@ Adw.MessageDialog clipboard_confirmation_window {
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive
ok: _("Allow") destructive,
]
default-response: "cancel";
@ -16,7 +16,7 @@ Adw.MessageDialog clipboard_confirmation_window {
extra-child: Overlay {
styles [
"osd"
"osd",
]
ScrolledWindow text_view_scroll {
@ -33,7 +33,7 @@ Adw.MessageDialog clipboard_confirmation_window {
right-margin: 8;
styles [
"clipboard-content-view"
"clipboard-content-view",
]
}
}
@ -60,7 +60,7 @@ Adw.MessageDialog clipboard_confirmation_window {
margin-top: 12;
styles [
"opaque"
"opaque",
]
Image {

View File

@ -1,77 +0,0 @@
<?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="true">Authorize Clipboard Access</property>
<property name="body" translatable="true">An application is attempting to write to the clipboard. The current clipboard contents are shown below.</property>
<responses>
<response id="cancel" translatable="true" appearance="suggested">Deny</response>
<response id="ok" translatable="true" appearance="destructive">Allow</response>
</responses>
<property name="default-response">cancel</property>
<property name="close-response">cancel</property>
<property name="extra-child">
<object class="GtkOverlay">
<style>
<class name="osd"/>
</style>
<child>
<object class="GtkScrolledWindow" id="text_view_scroll">
<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>
<style>
<class name="clipboard-content-view"/>
</style>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="reveal_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<child>
<object class="GtkImage">
<property name="icon-name">view-reveal-symbolic</property>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="hide_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<style>
<class name="opaque"/>
</style>
<child>
<object class="GtkImage">
<property name="icon-name">view-conceal-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</interface>

View File

@ -8,7 +8,7 @@ Adw.MessageDialog clipboard_confirmation_window {
responses [
cancel: _("Cancel") suggested,
ok: _("Paste") destructive
ok: _("Paste") destructive,
]
default-response: "cancel";
@ -16,7 +16,7 @@ Adw.MessageDialog clipboard_confirmation_window {
extra-child: Overlay {
styles [
"osd"
"osd",
]
ScrolledWindow text_view_scroll {
@ -33,7 +33,7 @@ Adw.MessageDialog clipboard_confirmation_window {
right-margin: 8;
styles [
"clipboard-content-view"
"clipboard-content-view",
]
}
}
@ -60,7 +60,7 @@ Adw.MessageDialog clipboard_confirmation_window {
margin-top: 12;
styles [
"opaque"
"opaque",
]
Image {

View File

@ -1,77 +0,0 @@
<?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="true">Warning: Potentially Unsafe Paste</property>
<property name="body" translatable="true">Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.</property>
<responses>
<response id="cancel" translatable="true" appearance="suggested">Cancel</response>
<response id="ok" translatable="true" appearance="destructive">Paste</response>
</responses>
<property name="default-response">cancel</property>
<property name="close-response">cancel</property>
<property name="extra-child">
<object class="GtkOverlay">
<style>
<class name="osd"/>
</style>
<child>
<object class="GtkScrolledWindow" id="text_view_scroll">
<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>
<style>
<class name="clipboard-content-view"/>
</style>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="reveal_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<child>
<object class="GtkImage">
<property name="icon-name">view-reveal-symbolic</property>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="hide_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<style>
<class name="opaque"/>
</style>
<child>
<object class="GtkImage">
<property name="icon-name">view-conceal-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</interface>

View File

@ -8,7 +8,7 @@ Adw.AlertDialog clipboard_confirmation_window {
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive
ok: _("Allow") destructive,
]
default-response: "cancel";
@ -16,7 +16,7 @@ Adw.AlertDialog clipboard_confirmation_window {
extra-child: Overlay {
styles [
"osd"
"osd",
]
ScrolledWindow text_view_scroll {
@ -33,7 +33,7 @@ Adw.AlertDialog clipboard_confirmation_window {
right-margin: 8;
styles [
"clipboard-content-view"
"clipboard-content-view",
]
}
}
@ -60,7 +60,7 @@ Adw.AlertDialog clipboard_confirmation_window {
margin-top: 12;
styles [
"opaque"
"opaque",
]
Image {

View File

@ -8,7 +8,7 @@ Adw.AlertDialog clipboard_confirmation_window {
responses [
cancel: _("Deny") suggested,
ok: _("Allow") destructive
ok: _("Allow") destructive,
]
default-response: "cancel";
@ -16,7 +16,7 @@ Adw.AlertDialog clipboard_confirmation_window {
extra-child: Overlay {
styles [
"osd"
"osd",
]
ScrolledWindow text_view_scroll {
@ -33,7 +33,7 @@ Adw.AlertDialog clipboard_confirmation_window {
right-margin: 8;
styles [
"clipboard-content-view"
"clipboard-content-view",
]
}
}
@ -60,7 +60,7 @@ Adw.AlertDialog clipboard_confirmation_window {
margin-top: 12;
styles [
"opaque"
"opaque",
]
Image {

View File

@ -8,7 +8,7 @@ Adw.AlertDialog clipboard_confirmation_window {
responses [
cancel: _("Cancel") suggested,
ok: _("Paste") destructive
ok: _("Paste") destructive,
]
default-response: "cancel";
@ -16,7 +16,7 @@ Adw.AlertDialog clipboard_confirmation_window {
extra-child: Overlay {
styles [
"osd"
"osd",
]
ScrolledWindow text_view_scroll {
@ -33,7 +33,7 @@ Adw.AlertDialog clipboard_confirmation_window {
right-margin: 8;
styles [
"clipboard-content-view"
"clipboard-content-view",
]
}
}
@ -60,7 +60,7 @@ Adw.AlertDialog clipboard_confirmation_window {
margin-top: 12;
styles [
"opaque"
"opaque",
]
Image {

View File

@ -7,7 +7,7 @@ Adw.AlertDialog prompt_title_dialog {
responses [
cancel: _("Cancel") suggested,
ok: _("OK") destructive
ok: _("OK") destructive,
]
focus-widget: title_entry;

View File

@ -1,21 +1,20 @@
# GTK UI files
This directory is for storing GTK resource definitions. With one exception, the
files should be be in the Blueprint markup language.
This directory is for storing GTK resource definitions. All resource definitions
_must_ start as GTK blueprint `.blp` files. GTK resource definitions are then
generated by `blueprint-compiler` at compile time. A CI job ensures that `.blp`
files are valid and properly formatted.
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.
Blueprint files should be stored in directories that represent the minimum
Adwaita version needed to use that resource. Blueprint files should also be
formatted using `blueprint-compiler format` as well to ensure consistency
(formatting will be checked in CI as well).
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.
Blueprints can be formatted and compiled into resource definitions by running
this command from the root of the source tree:
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.
```
nix run .#compile-blueprints
```
Currently `blueprint-compiler` 0.16.0 is required to compile Blueprint files.

View File

@ -668,71 +668,46 @@ fn addGTK(
});
const generate = b.addRunArtifact(generate_gresource_xml);
const gtk_blueprint_compiler = b.addExecutable(.{
const blueprint_compiler = b.addExecutable(.{
.name = "gtk_blueprint_compiler",
.root_source_file = b.path("src/apprt/gtk/blueprint_compiler.zig"),
.target = b.graph.host,
});
gtk_blueprint_compiler.linkSystemLibrary2("gtk4", dynamic_link_opts);
gtk_blueprint_compiler.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
gtk_blueprint_compiler.linkLibC();
blueprint_compiler.linkSystemLibrary2("gtk4", dynamic_link_opts);
blueprint_compiler.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
blueprint_compiler.linkLibC();
for (gresource.blueprint_files) |blueprint_file| {
const blueprint_compiler = b.addRunArtifact(gtk_blueprint_compiler);
blueprint_compiler.addArgs(&.{
if (b.systemIntegrationOption("blueprint-compiler", .{ .default = true })) {
const blueprint_compile = b.addRunArtifact(blueprint_compiler);
blueprint_compile.addArgs(&.{
b.fmt("{d}", .{blueprint_file.major}),
b.fmt("{d}", .{blueprint_file.minor}),
});
const ui_file = blueprint_compiler.addOutputFileArg(b.fmt(
"{d}.{d}/{s}.ui",
.{
blueprint_file.major,
blueprint_file.minor,
blueprint_file.name,
},
const blueprint_output = blueprint_compile.addOutputFileArg(b.fmt(
"{[major]d}.{[minor]d}/{[name]s}.ui",
blueprint_file,
));
blueprint_compiler.addFileArg(b.path(b.fmt(
"src/apprt/gtk/ui/{d}.{d}/{s}.blp",
.{
blueprint_file.major,
blueprint_file.minor,
blueprint_file.name,
},
blueprint_compile.addFileArg(b.path(b.fmt(
"src/apprt/gtk/ui/{[major]d}.{[minor]d}/{[name]s}.blp",
blueprint_file,
)));
generate.addFileArg(ui_file);
generate.addFileArg(blueprint_output);
} else {
generate.addFileInput(b.path(b.fmt(
"src/apprt/gtk/ui/{[major]d}.{[minor]d}/{[name]s}.ui",
blueprint_file,
)));
}
}
for (gresource.dependencies) |pathname| {
if (std.mem.eql(u8, std.fs.path.extension(pathname), ".blp")) continue;
generate.addFileInput(b.path(pathname));
}
break :gresource_xml generate.captureStdOut();
};
{
const gtk_builder_check = b.addExecutable(.{
.name = "gtk_builder_check",
.root_source_file = b.path("src/apprt/gtk/builder_check.zig"),
.target = b.graph.host,
});
gtk_builder_check.root_module.addOptions("build_options", self.options);
if (gobject_) |gobject| {
gtk_builder_check.root_module.addImport(
"gtk",
gobject.module("gtk4"),
);
gtk_builder_check.root_module.addImport(
"adw",
gobject.module("adw1"),
);
}
for (gresource.dependencies) |pathname| {
const extension = std.fs.path.extension(pathname);
if (!std.mem.eql(u8, extension, ".ui")) continue;
const check = b.addRunArtifact(gtk_builder_check);
check.addFileArg(b.path(pathname));
step.step.dependOn(&check.step);
}
}
const generate_resources_c = b.addSystemCommand(&.{
"glib-compile-resources",
"--c-name",

View File

@ -5,7 +5,6 @@ FROM docker.io/library/debian:${DISTRO_VERSION}
RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \
apt-get -qq -y --no-install-recommends install \
# Build Tools
blueprint-compiler \
build-essential \
libbz2-dev \
libonig-dev \
@ -52,5 +51,5 @@ RUN ZIG_GLOBAL_CACHE_DIR=/zig/global-cache ./nix/build-support/fetch-zig-cache.s
COPY ./src /src/src
# Debian 12 doesn't have gtk4-layer-shell, so we have to manually compile it ourselves
RUN zig build -Doptimize=Debug -Dcpu=baseline -Dapp-runtime=gtk -fno-sys=gtk4-layer-shell --system /zig/global-cache/p
RUN zig build -Doptimize=Debug -Dcpu=baseline -Dapp-runtime=gtk -fno-sys=gtk4-layer-shell -fno-sys=blueprint-compiler --system /zig/global-cache/p