mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 09:16:11 +03:00
apprt/gtk-ng: gresource creation, resource registration in Application
This commit is contained in:
172
src/apprt/gtk-ng/build/blueprint.zig
Normal file
172
src/apprt/gtk-ng/build/blueprint.zig
Normal 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);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
182
src/apprt/gtk-ng/build/gresource.zig
Normal file
182
src/apprt/gtk-ng/build/gresource.zig
Normal 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>
|
||||
\\
|
||||
);
|
||||
}
|
@ -100,11 +100,20 @@ pub const GhosttyApplication = extern struct {
|
||||
single_instance,
|
||||
});
|
||||
|
||||
// Initialize the app.
|
||||
const self = gobject.ext.newInstance(Self, .{
|
||||
.application_id = app_id.ptr,
|
||||
.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();
|
||||
priv.core_app = core_app;
|
||||
priv.config = config;
|
||||
@ -275,6 +284,23 @@ pub const GhosttyApplication = extern struct {
|
||||
pub const Instance = Self;
|
||||
|
||||
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.startup.implement(class, &startup);
|
||||
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
|
||||
|
8
src/apprt/gtk-ng/ui/1.5/window.blp
Normal file
8
src/apprt/gtk-ng/ui/1.5/window.blp
Normal file
@ -0,0 +1,8 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
Adw.Window {
|
||||
Label {
|
||||
label: "Hello";
|
||||
}
|
||||
}
|
@ -8,8 +8,6 @@ const UnicodeTables = @import("UnicodeTables.zig");
|
||||
const GhosttyFrameData = @import("GhosttyFrameData.zig");
|
||||
const DistResource = @import("GhosttyDist.zig").Resource;
|
||||
|
||||
const gresource = @import("../apprt/gtk/gresource.zig");
|
||||
|
||||
config: *const Config,
|
||||
|
||||
options: *std.Build.Step.Options,
|
||||
@ -688,6 +686,107 @@ fn addGtkNg(
|
||||
|
||||
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
|
||||
@ -832,6 +931,8 @@ pub fn gtkDistResources(
|
||||
resources_c: DistResource,
|
||||
resources_h: DistResource,
|
||||
} {
|
||||
const gresource = @import("../apprt/gtk/gresource.zig");
|
||||
|
||||
const gresource_xml = gresource_xml: {
|
||||
const xml_exe = b.addExecutable(.{
|
||||
.name = "generate_gresource_xml",
|
||||
|
Reference in New Issue
Block a user