Implement loading custom css in the GTK app (#4200)

Closes https://github.com/ghostty-org/ghostty/issues/4089
Gave it a shot and implemented the custom css loading.
My general idea is to use a provider for each stylesheet the user wants
to load and then when the config changes unload them and create new
providers.
A separate provider has to be used for each stylesheet the user wants to
load, since when the provider loads the css it clears all the previously
loaded styles, so in effect we cannot use one provider to load multiple
stylesheets, but maybe there is a better way to overcome this limitation
which I'm not seeing.
This commit is contained in:
Mitchell Hashimoto
2025-01-02 14:34:28 -08:00
committed by GitHub
2 changed files with 83 additions and 6 deletions

View File

@ -81,6 +81,9 @@ transient_cgroup_base: ?[]const u8 = null,
/// CSS Provider for any styles based on ghostty configuration values /// CSS Provider for any styles based on ghostty configuration values
css_provider: *c.GtkCssProvider, css_provider: *c.GtkCssProvider,
/// Providers for loading custom stylesheets defined by user
custom_css_providers: std.ArrayListUnmanaged(*c.GtkCssProvider) = .{},
/// The timer used to quit the application after the last window is closed. /// The timer used to quit the application after the last window is closed.
quit_timer: union(enum) { quit_timer: union(enum) {
off: void, off: void,
@ -441,6 +444,11 @@ pub fn terminate(self: *App) void {
if (self.context_menu) |context_menu| c.g_object_unref(context_menu); if (self.context_menu) |context_menu| c.g_object_unref(context_menu);
if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path); if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path);
for (self.custom_css_providers.items) |provider| {
c.g_object_unref(provider);
}
self.custom_css_providers.deinit(self.core_app.alloc);
self.config.deinit(); self.config.deinit();
} }
@ -893,7 +901,7 @@ fn syncConfigChanges(self: *App) !void {
try self.updateConfigErrors(); try self.updateConfigErrors();
try self.syncActionAccelerators(); try self.syncActionAccelerators();
// Load our runtime CSS. If this fails then our window is just stuck // Load our runtime and custom CSS. If this fails then our window is just stuck
// with the old CSS but we don't want to fail the entire sync operation. // with the old CSS but we don't want to fail the entire sync operation.
self.loadRuntimeCss() catch |err| switch (err) { self.loadRuntimeCss() catch |err| switch (err) {
error.OutOfMemory => log.warn( error.OutOfMemory => log.warn(
@ -901,6 +909,9 @@ fn syncConfigChanges(self: *App) !void {
.{}, .{},
), ),
}; };
self.loadCustomCss() catch |err| {
log.warn("Failed to load custom CSS, no custom CSS applied, err={}", .{err});
};
} }
/// This should be called whenever the configuration changes to update /// This should be called whenever the configuration changes to update
@ -1034,11 +1045,68 @@ fn loadRuntimeCss(
} }
// Clears any previously loaded CSS from this provider // Clears any previously loaded CSS from this provider
c.gtk_css_provider_load_from_data( loadCssProviderFromData(self.css_provider, buf.items);
self.css_provider, }
buf.items.ptr,
@intCast(buf.items.len), fn loadCustomCss(self: *App) !void {
); const display = c.gdk_display_get_default();
// unload the previously loaded style providers
for (self.custom_css_providers.items) |provider| {
c.gtk_style_context_remove_provider_for_display(
display,
@ptrCast(provider),
);
c.g_object_unref(provider);
}
self.custom_css_providers.clearRetainingCapacity();
for (self.config.@"gtk-custom-css".value.items) |p| {
const path, const optional = switch (p) {
.optional => |path| .{ path, true },
.required => |path| .{ path, false },
};
const file = std.fs.openFileAbsolute(path, .{}) catch |err| {
if (err != error.FileNotFound or !optional) {
log.err("error opening gtk-custom-css file {s}: {}", .{ path, err });
}
continue;
};
defer file.close();
log.info("loading gtk-custom-css path={s}", .{path});
const contents = try file.reader().readAllAlloc(
self.core_app.alloc,
5 * 1024 * 1024 // 5MB
);
defer self.core_app.alloc.free(contents);
const provider = c.gtk_css_provider_new();
c.gtk_style_context_add_provider_for_display(
display,
@ptrCast(provider),
c.GTK_STYLE_PROVIDER_PRIORITY_USER,
);
loadCssProviderFromData(provider, contents);
try self.custom_css_providers.append(self.core_app.alloc, provider);
}
}
fn loadCssProviderFromData(provider: *c.GtkCssProvider, data: []const u8) void {
if (version.atLeast(4, 12, 0)) {
const g_bytes = c.g_bytes_new(data.ptr, data.len);
defer c.g_bytes_unref(g_bytes);
c.gtk_css_provider_load_from_bytes(provider, g_bytes);
} else {
c.gtk_css_provider_load_from_data(
provider,
data.ptr,
@intCast(data.len),
);
}
} }
/// Called by CoreApp to wake up the event loop. /// Called by CoreApp to wake up the event loop.

View File

@ -1987,6 +1987,15 @@ keybind: Keybinds = .{},
/// Adwaita support. /// Adwaita support.
@"gtk-adwaita": bool = true, @"gtk-adwaita": bool = true,
/// Custom CSS files to be loaded.
///
/// This configuration can be repeated multiple times to load multiple files.
/// Prepend a ? character to the file path to suppress errors if the file does
/// not exist. If you want to include a file that begins with a literal ?
/// character, surround the file path in double quotes (").
/// The file size limit for a single stylesheet is 5MiB.
@"gtk-custom-css": RepeatablePath = .{},
/// If `true` (default), applications running in the terminal can show desktop /// If `true` (default), applications running in the terminal can show desktop
/// notifications using certain escape sequences such as OSC 9 or OSC 777. /// notifications using certain escape sequences such as OSC 9 or OSC 777.
@"desktop-notifications": bool = true, @"desktop-notifications": bool = true,