apprt/gtk-ng: gresource creation, resource registration in Application

This commit is contained in:
Mitchell Hashimoto
2025-07-15 13:53:48 -07:00
parent 9c6cf61cd4
commit ecb77fb8bc
5 changed files with 491 additions and 2 deletions

View File

@ -0,0 +1,172 @@
//! Compiles a blueprint file using `blueprint-compiler`. This performs
//! additional checks to ensure that various minimum versions are met.
//!
//! Usage: blueprint.zig <major> <minor> <output> <input>
//!
//! Example: blueprint.zig 1 5 output.ui input.blp
const std = @import("std");
pub const c = @cImport({
@cInclude("adwaita.h");
});
const adwaita_version = std.SemanticVersion{
.major = c.ADW_MAJOR_VERSION,
.minor = c.ADW_MINOR_VERSION,
.patch = c.ADW_MICRO_VERSION,
};
const required_blueprint_version = std.SemanticVersion{
.major = 0,
.minor = 16,
.patch = 0,
};
pub fn main() !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
const alloc = debug_allocator.allocator();
// Get our args
var it = try std.process.argsWithAllocator(alloc);
defer it.deinit();
_ = it.next(); // Skip argv0
const arg_major = it.next() orelse return error.NoMajorVersion;
const arg_minor = it.next() orelse return error.NoMinorVersion;
const output = it.next() orelse return error.NoOutput;
const input = it.next() orelse return error.NoInput;
const required_adwaita_version = std.SemanticVersion{
.major = try std.fmt.parseUnsigned(u8, arg_major, 10),
.minor = try std.fmt.parseUnsigned(u8, arg_minor, 10),
.patch = 0,
};
if (adwaita_version.order(required_adwaita_version) == .lt) {
std.debug.print(
\\`libadwaita` is too old.
\\
\\Ghostty requires a version {} or newer of `libadwaita` to
\\compile this blueprint. Please install it, ensure that it is
\\available on your PATH, and then retry building Ghostty.
, .{required_adwaita_version});
std.posix.exit(1);
}
// Version checks
{
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.debug.print(
\\`blueprint-compiler` not found.
\\
\\Ghostty requires version {} or newer of
\\`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.
\\
, .{required_blueprint_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 = try std.SemanticVersion.parse(std.mem.trim(
u8,
stdout.items,
&std.ascii.whitespace,
));
if (version.order(required_blueprint_version) == .lt) {
std.debug.print(
\\`blueprint-compiler` is the wrong version.
\\
\\Ghostty requires version {} or newer of
\\`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.
\\
, .{required_blueprint_version});
std.posix.exit(1);
}
}
// Compilation
{
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",
"--output",
output,
input,
},
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.debug.print(
\\`blueprint-compiler` not found.
\\
\\Ghostty requires version {} or newer of
\\`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.
\\
, .{required_blueprint_version});
std.posix.exit(1);
},
else => return err,
};
switch (term) {
.Exited => |rc| {
if (rc != 0) {
std.debug.print("{s}", .{stderr.items});
std.process.exit(1);
}
},
else => {
std.debug.print("{s}", .{stderr.items});
std.process.exit(1);
},
}
}
}

View File

@ -0,0 +1,182 @@
//! This file contains a binary helper that builds our gresource XML
//! file that we can then use with `glib-compile-resources`.
//!
//! This binary is expected to be run from the Ghostty source root.
//! Litmus test: `src/apprt/gtk` should exist relative to the pwd.
const std = @import("std");
const Allocator = std.mem.Allocator;
/// Prefix/appid for the gresource file.
pub const prefix = "/com/mitchellh/ghostty";
pub const app_id = "com.mitchellh.ghostty";
/// The path to the Blueprint files. The folder structure is expected to be
/// `{version}/{name}.blp` where `version` is the major and minor
/// minimum adwaita version.
pub const ui_path = "src/apprt/gtk-ng/ui";
/// The possible icon sizes we'll embed into the gresource file.
/// If any size doesn't exist then it will be an error. We could
/// infer this completely from available files but we wouldn't be
/// able to error when they don't exist that way.
pub const icon_sizes: []const comptime_int = &.{ 16, 32, 128, 256, 512, 1024 };
/// The blueprint files that we will embed into the gresource file.
/// We can't look these up at runtime [easily] because we require the
/// compiled UI files as input. We can refactor this lator to maybe do
/// all of this automatically and ensure we have the right dependencies
/// setup in the build system.
///
/// These will be asserted to exist at runtime.
pub const blueprints: []const struct {
major: u16,
minor: u16,
name: []const u8,
} = &.{
.{ .major = 1, .minor = 5, .name = "window" },
};
/// The list of filepaths that we depend on. Used for the build
/// system to have proper caching.
pub const file_inputs = deps: {
const total = (icon_sizes.len * 2) + blueprints.len;
var deps: [total][]const u8 = undefined;
var index: usize = 0;
for (icon_sizes) |size| {
deps[index] = std.fmt.comptimePrint("images/icons/icon_{d}.png", .{size});
deps[index + 1] = std.fmt.comptimePrint("images/icons/icon_{d}@2x.png", .{size});
index += 2;
}
for (blueprints) |bp| {
deps[index] = std.fmt.comptimePrint("{s}/{d}.{d}/{s}.blp", .{
ui_path,
bp.major,
bp.minor,
bp.name,
});
index += 1;
}
break :deps deps;
};
pub fn main() !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
const alloc = debug_allocator.allocator();
// Collect the UI files that are passed in as arguments.
var ui_files: std.ArrayListUnmanaged([]const u8) = .empty;
defer {
for (ui_files.items) |item| alloc.free(item);
ui_files.deinit(alloc);
}
var it = try std.process.argsWithAllocator(alloc);
defer it.deinit();
while (it.next()) |arg| {
if (!std.mem.endsWith(u8, arg, ".ui")) continue;
try ui_files.append(
alloc,
try alloc.dupe(u8, arg),
);
}
const writer = std.io.getStdOut().writer();
try writer.writeAll(
\\<?xml version="1.0" encoding="UTF-8"?>
\\<gresources>
\\
);
try genIcons(writer);
try genUi(alloc, writer, &ui_files);
try writer.writeAll(
\\</gresources>
\\
);
}
/// Generate the icon resources. This works by looking up all the icons
/// specified by `icon_sizes` in `images/icons/`. They are asserted to exist
/// by trying to access the file.
fn genIcons(writer: anytype) !void {
try writer.print(
\\ <gresource prefix="{s}/icons">
\\
, .{prefix});
const cwd = std.fs.cwd();
inline for (icon_sizes) |size| {
// 1x
{
const alias = std.fmt.comptimePrint("{d}x{d}", .{ size, size });
const source = std.fmt.comptimePrint("images/icons/icon_{d}.png", .{size});
try cwd.access(source, .{});
try writer.print(
\\ <file alias="{s}/apps/{s}.png">{s}</file>
\\
,
.{ alias, app_id, source },
);
}
// 2x
{
const alias = std.fmt.comptimePrint("{d}x{d}@2", .{ size, size });
const source = std.fmt.comptimePrint("images/icons/icon_{d}@2x.png", .{size});
try cwd.access(source, .{});
try writer.print(
\\ <file alias="{s}/apps/{s}.png">{s}</file>
\\
,
.{ alias, app_id, source },
);
}
}
try writer.writeAll(
\\ </gresource>
\\
);
}
/// Generate all the UI resources. This works by looking up all the
/// blueprint files in `${ui_path}/{major}.{minor}/{name}.blp` and
/// assuming these will be
fn genUi(
alloc: Allocator,
writer: anytype,
files: *const std.ArrayListUnmanaged([]const u8),
) !void {
try writer.print(
\\ <gresource prefix="{s}/ui">
\\
, .{prefix});
for (files.items) |ui_file| {
for (blueprints) |bp| {
const expected = try std.fmt.allocPrint(
alloc,
"/{d}.{d}/{s}.ui",
.{ bp.major, bp.minor, bp.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",
.{ bp.major, bp.minor, bp.name, ui_file },
);
break;
} else {
// The for loop never broke which means it didn't find
// a matching blueprint for this input.
return error.BlueprintNotFound;
}
}
try writer.writeAll(
\\ </gresource>
\\
);
}

View File

@ -100,11 +100,20 @@ pub const GhosttyApplication = extern struct {
single_instance, single_instance,
}); });
// Initialize the app.
const self = gobject.ext.newInstance(Self, .{ const self = gobject.ext.newInstance(Self, .{
.application_id = app_id.ptr, .application_id = app_id.ptr,
.flags = app_flags, .flags = app_flags,
// Force the resource path to a known value so it doesn't depend
// on the app id (which changes between debug/release and can be
// user-configured) and force it to load in compiled resources.
.resource_base_path = "/com/mitchellh/ghostty",
}); });
// Setup our private state. More setup is done in the init
// callback that GObject calls, but we can't pass this data through
// to there (and we don't need it there directly) so this is here.
const priv = self.private(); const priv = self.private();
priv.core_app = core_app; priv.core_app = core_app;
priv.config = config; priv.config = config;
@ -275,6 +284,23 @@ pub const GhosttyApplication = extern struct {
pub const Instance = Self; pub const Instance = Self;
fn init(class: *Class) callconv(.C) void { fn init(class: *Class) callconv(.C) void {
// Register our compiled resources exactly once.
{
const c = @cImport({
// generated header files
@cInclude("ghostty_resources.h");
});
if (c.ghostty_get_resource()) |ptr| {
gio.resourcesRegister(@ptrCast(@alignCast(ptr)));
} else {
// If we fail to load resources then things will
// probably look really bad but it shouldn't stop our
// app from loading.
log.warn("unable to load resources", .{});
}
}
// Virtual methods
gio.Application.virtual_methods.activate.implement(class, &activate); gio.Application.virtual_methods.activate.implement(class, &activate);
gio.Application.virtual_methods.startup.implement(class, &startup); gio.Application.virtual_methods.startup.implement(class, &startup);
gobject.Object.virtual_methods.finalize.implement(class, &finalize); gobject.Object.virtual_methods.finalize.implement(class, &finalize);

View File

@ -0,0 +1,8 @@
using Gtk 4.0;
using Adw 1;
Adw.Window {
Label {
label: "Hello";
}
}

View File

@ -8,8 +8,6 @@ const UnicodeTables = @import("UnicodeTables.zig");
const GhosttyFrameData = @import("GhosttyFrameData.zig"); const GhosttyFrameData = @import("GhosttyFrameData.zig");
const DistResource = @import("GhosttyDist.zig").Resource; const DistResource = @import("GhosttyDist.zig").Resource;
const gresource = @import("../apprt/gtk/gresource.zig");
config: *const Config, config: *const Config,
options: *std.Build.Step.Options, options: *std.Build.Step.Options,
@ -688,6 +686,107 @@ fn addGtkNg(
step.linkSystemLibrary2("wayland-client", dynamic_link_opts); step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
} }
{
// Get our gresource c/h files and add them to our build.
const dist = gtkNgDistResources(b);
step.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} });
step.addIncludePath(dist.resources_h.path(b).dirname());
}
}
/// Creates the resources that can be prebuilt for our dist build.
pub fn gtkNgDistResources(
b: *std.Build,
) struct {
resources_c: DistResource,
resources_h: DistResource,
} {
const gresource = @import("../apprt/gtk-ng/build/gresource.zig");
const gresource_xml = gresource_xml: {
const xml_exe = b.addExecutable(.{
.name = "generate_gresource_xml",
.root_source_file = b.path("src/apprt/gtk-ng/build/gresource.zig"),
.target = b.graph.host,
});
const xml_run = b.addRunArtifact(xml_exe);
// Run our blueprint compiler across all of our blueprint files.
const blueprint_exe = b.addExecutable(.{
.name = "gtk_blueprint_compiler",
.root_source_file = b.path("src/apprt/gtk-ng/build/blueprint.zig"),
.target = b.graph.host,
});
blueprint_exe.linkLibC();
blueprint_exe.linkSystemLibrary2("gtk4", dynamic_link_opts);
blueprint_exe.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
for (gresource.blueprints) |bp| {
const blueprint_run = b.addRunArtifact(blueprint_exe);
blueprint_run.addArgs(&.{
b.fmt("{d}", .{bp.major}),
b.fmt("{d}", .{bp.minor}),
});
const ui_file = blueprint_run.addOutputFileArg(b.fmt(
"{d}.{d}/{s}.ui",
.{
bp.major,
bp.minor,
bp.name,
},
));
blueprint_run.addFileArg(b.path(b.fmt(
"{s}/{d}.{d}/{s}.blp",
.{
gresource.ui_path,
bp.major,
bp.minor,
bp.name,
},
)));
xml_run.addFileArg(ui_file);
}
break :gresource_xml xml_run.captureStdOut();
};
const generate_c = b.addSystemCommand(&.{
"glib-compile-resources",
"--c-name",
"ghostty",
"--generate-source",
"--target",
});
const resources_c = generate_c.addOutputFileArg("ghostty_resources.c");
generate_c.addFileArg(gresource_xml);
for (gresource.file_inputs) |path| {
generate_c.addFileInput(b.path(path));
}
const generate_h = b.addSystemCommand(&.{
"glib-compile-resources",
"--c-name",
"ghostty",
"--generate-header",
"--target",
});
const resources_h = generate_h.addOutputFileArg("ghostty_resources.h");
generate_h.addFileArg(gresource_xml);
for (gresource.file_inputs) |path| {
generate_h.addFileInput(b.path(path));
}
return .{
.resources_c = .{
.dist = "src/apprt/gtk-ng/ghostty_resources.c",
.generated = resources_c,
},
.resources_h = .{
.dist = "src/apprt/gtk-ng/ghostty_resources.h",
.generated = resources_h,
},
};
} }
/// Setup the dependencies for the GTK apprt build. The GTK apprt /// Setup the dependencies for the GTK apprt build. The GTK apprt
@ -832,6 +931,8 @@ pub fn gtkDistResources(
resources_c: DistResource, resources_c: DistResource,
resources_h: DistResource, resources_h: DistResource,
} { } {
const gresource = @import("../apprt/gtk/gresource.zig");
const gresource_xml = gresource_xml: { const gresource_xml = gresource_xml: {
const xml_exe = b.addExecutable(.{ const xml_exe = b.addExecutable(.{
.name = "generate_gresource_xml", .name = "generate_gresource_xml",