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, resource_name: [:0]const u8,
builder: ?*gtk.Builder, builder: ?*gtk.Builder,
pub fn init(comptime name: []const u8, comptime kind: enum { blp, ui }) Builder { pub fn init(
comptime { /// 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) { switch (kind) {
.blp => { .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 // Check to make sure that our file is listed as a
// `blueprint_file` in `gresource.zig`. If it isn't Ghostty // `blueprint_file` in `gresource.zig`. If it isn't Ghostty
// could crash at runtime when we try and load a nonexistent // could crash at runtime when we try and load a nonexistent
// GResource. // GResource.
const gresource = @import("gresource.zig"); for (gresource.blueprint_files) |file| {
for (gresource.blueprint_files) |blueprint_file| { if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
if (std.mem.eql(u8, blueprint_file.name, name)) break; // 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"); } else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
}, },
.ui => { .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 // 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 // `gresource.zig`. If it isn't Ghostty could crash at runtime
// when we try and load a nonexistent GResource. // when we try and load a nonexistent GResource.
const gresource = @import("gresource.zig"); for (gresource.ui_files) |file| {
for (gresource.ui_files) |ui_file| { if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
if (std.mem.eql(u8, ui_file, name)) break; // 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"); } else @compileError("missing ui file '" ++ name ++ "' in gresource.zig");
}, },
} }
} };
return .{ return .{
.resource_name = "/com/mitchellh/ghostty/ui/" ++ name ++ ".ui", .resource_name = resource_path,
.builder = null, .builder = null,
}; };
} }

View File

@ -65,14 +65,14 @@ fn init(
) !void { ) !void {
var builder = switch (DialogType) { var builder = switch (DialogType) {
adw.AlertDialog => switch (request) { adw.AlertDialog => switch (request) {
.osc_52_read => Builder.init("ccw-osc-52-write-15", .blp), .osc_52_read => Builder.init("ccw-osc-52-read", 1, 5, .blp),
.osc_52_write => Builder.init("ccw-osc-52-write-15", .blp), .osc_52_write => Builder.init("ccw-osc-52-write", 1, 5, .blp),
.paste => Builder.init("ccw-paste-15", .blp), .paste => Builder.init("ccw-paste", 1, 5, .blp),
}, },
adw.MessageDialog => switch (request) { adw.MessageDialog => switch (request) {
.osc_52_read => Builder.init("ccw-osc-52-write-12", .ui), .osc_52_read => Builder.init("ccw-osc-52-read", 1, 2, .ui),
.osc_52_write => Builder.init("ccw-osc-52-write-12", .ui), .osc_52_write => Builder.init("ccw-osc-52-write", 1, 2, .ui),
.paste => Builder.init("ccw-paste-12", .ui), .paste => Builder.init("ccw-paste", 1, 2, .ui),
}, },
else => unreachable, else => unreachable,
}; };

View File

@ -1050,7 +1050,7 @@ pub fn promptTitle(self: *Surface) !void {
if (!adwaita.versionAtLeast(1, 5, 0)) return; if (!adwaita.versionAtLeast(1, 5, 0)) return;
const window = self.container.window() orelse 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(); defer builder.deinit();
const entry = builder.getObject(gtk.Entry, "title_entry").?; 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 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 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 output = it.next() orelse return error.NoOutput;
const input = it.next() orelse return error.NoInput; const input = it.next() orelse return error.NoInput;
if (c.ADW_MAJOR_VERSION < major or if (c.ADW_MAJOR_VERSION < major or (c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION < minor)) {
(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 the Adwaita version is too old, generate an "empty" file. // If the Adwaita version is too old, generate an "empty" file.
const file = try std.fs.createFileAbsolute(output, .{ const file = try std.fs.createFileAbsolute(output, .{
.truncate = true, .truncate = true,

View File

@ -53,26 +53,31 @@ const icons = [_]struct {
}, },
}; };
pub const ui_files = [_][]const u8{ pub const VersionedBuilderXML = struct {
"ccw-osc-52-read-12", major: u16,
"ccw-osc-52-write-12", minor: u16,
"ccw-paste-12", 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 { pub const VersionedBlueprint = struct {
major: u16, major: u16,
minor: u16, minor: u16,
micro: u16,
name: []const u8, name: []const u8,
}; };
pub const blueprint_files = [_]VersionedBlueprint{ pub const blueprint_files = [_]VersionedBlueprint{
.{ .major = 1, .minor = 5, .micro = 0, .name = "prompt-title-dialog" }, .{ .major = 1, .minor = 5, .name = "prompt-title-dialog" },
.{ .major = 1, .minor = 0, .micro = 0, .name = "menu-surface-context_menu" }, .{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" },
.{ .major = 1, .minor = 0, .micro = 0, .name = "menu-window-titlebar_menu" }, .{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" },
.{ .major = 1, .minor = 5, .micro = 0, .name = "ccw-osc-52-read-15" }, .{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" },
.{ .major = 1, .minor = 5, .micro = 0, .name = "ccw-osc-52-write-15" }, .{ .major = 1, .minor = 5, .name = "ccw-osc-52-write" },
.{ .major = 1, .minor = 5, .micro = 0, .name = "ccw-paste-15" }, .{ .major = 1, .minor = 5, .name = "ccw-paste" },
}; };
pub fn main() !void { pub fn main() !void {
@ -126,15 +131,20 @@ pub fn main() !void {
); );
for (ui_files) |ui_file| { for (ui_files) |ui_file| {
try writer.print( try writer.print(
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{0s}.ui\">src/apprt/gtk/ui/{0s}.ui</file>\n", " <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{0d}.{1d}/{2s}.ui\">src/apprt/gtk/ui/{0d}.{1d}/{2s}.ui</file>\n",
.{ui_file}, .{ ui_file.major, ui_file.minor, ui_file.name },
); );
} }
for (extra_ui_files.items) |ui_file| { for (extra_ui_files.items) |ui_file| {
try writer.print( const stem = std.fs.path.stem(ui_file);
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{s}\">{s}</file>\n", for (blueprint_files) |file| {
.{ std.fs.path.basename(ui_file), ui_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( try writer.writeAll(
\\ </gresource> \\ </gresource>
@ -156,11 +166,19 @@ pub const dependencies = deps: {
index += 1; index += 1;
} }
for (ui_files) |ui_file| { 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; index += 1;
} }
for (blueprint_files) |blueprint_file| { 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; index += 1;
} }
break :deps deps; break :deps deps;

View File

@ -41,7 +41,7 @@ pub fn Menu(
else => unreachable, 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(); defer builder.deinit();
const menu_model = builder.getObject(gio.MenuModel, "menu").?; 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(&.{ blueprint_compiler.addArgs(&.{
b.fmt("{d}", .{blueprint_file.major}), b.fmt("{d}", .{blueprint_file.major}),
b.fmt("{d}", .{blueprint_file.minor}), 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})); const ui_file = blueprint_compiler.addOutputFileArg(b.fmt(
blueprint_compiler.addFileArg(b.path(b.fmt("src/apprt/gtk/ui/{s}.blp", .{blueprint_file.name}))); "{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); generate.addFileArg(ui_file);
} }