gtk: use versioned directories to store blueprint files

This commit is contained in:
Jeffrey C. Ollie
2025-02-24 18:19:22 -06:00
parent c6b049b12b
commit 0638eca633
20 changed files with 161 additions and 57 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

@ -65,14 +65,14 @@ fn init(
) !void {
var builder = switch (DialogType) {
adw.AlertDialog => switch (request) {
.osc_52_read => Builder.init("ccw-osc-52-write-15", .blp),
.osc_52_write => Builder.init("ccw-osc-52-write-15", .blp),
.paste => Builder.init("ccw-paste-15", .blp),
.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-write-12", .ui),
.osc_52_write => Builder.init("ccw-osc-52-write-12", .ui),
.paste => Builder.init("ccw-paste-12", .ui),
.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,
};

View File

@ -1050,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,26 +53,31 @@ const icons = [_]struct {
},
};
pub const ui_files = [_][]const u8{
"ccw-osc-52-read-12",
"ccw-osc-52-write-12",
"ccw-paste-12",
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, .micro = 0, .name = "ccw-osc-52-read-15" },
.{ .major = 1, .minor = 5, .micro = 0, .name = "ccw-osc-52-write-15" },
.{ .major = 1, .minor = 5, .micro = 0, .name = "ccw-paste-15" },
.{ .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 {
@ -126,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>
@ -156,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,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);
}