mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 11:16:08 +03:00
Merge pull request #1506 from jcollie/gtk-resources
GTK: Add compiled-in GTK resources and use them for icons.
This commit is contained in:
37
build.zig
37
build.zig
@ -1165,6 +1165,43 @@ fn addDeps(
|
||||
.gtk => {
|
||||
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
|
||||
if (config.libadwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
|
||||
|
||||
{
|
||||
const gresource = @import("src/apprt/gtk/gresource.zig");
|
||||
|
||||
const wf = b.addWriteFiles();
|
||||
const gresource_xml = wf.add(
|
||||
"gresource.xml",
|
||||
if (config.libadwaita)
|
||||
gresource.gresource_xml_libadwaita
|
||||
else
|
||||
gresource.gresource_xml_gtk,
|
||||
);
|
||||
|
||||
const generate_resources_c = b.addSystemCommand(&.{
|
||||
"glib-compile-resources",
|
||||
"--c-name",
|
||||
"ghostty",
|
||||
"--generate-source",
|
||||
"--target",
|
||||
});
|
||||
const ghostty_resources_c = generate_resources_c.addOutputFileArg("ghostty_resources.c");
|
||||
generate_resources_c.addFileArg(gresource_xml);
|
||||
generate_resources_c.extra_file_dependencies = if (config.libadwaita) &gresource.dependencies_libadwaita else &gresource.dependencies_gtk;
|
||||
step.addCSourceFile(.{ .file = ghostty_resources_c, .flags = &.{} });
|
||||
|
||||
const generate_resources_h = b.addSystemCommand(&.{
|
||||
"glib-compile-resources",
|
||||
"--c-name",
|
||||
"ghostty",
|
||||
"--generate-header",
|
||||
"--target",
|
||||
});
|
||||
const ghostty_resources_h = generate_resources_h.addOutputFileArg("ghostty_resources.h");
|
||||
generate_resources_h.addFileArg(gresource_xml);
|
||||
generate_resources_h.extra_file_dependencies = if (config.libadwaita) &gresource.dependencies_libadwaita else &gresource.dependencies_gtk;
|
||||
step.addIncludePath(ghostty_resources_h.dirname());
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -151,8 +151,16 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
|
||||
break :app @ptrCast(adw_app);
|
||||
};
|
||||
|
||||
errdefer c.g_object_unref(app);
|
||||
const gapp = @as(*c.GApplication, @ptrCast(app));
|
||||
|
||||
// force the resource path to a known value so that it doesn't depend on
|
||||
// the app id and load in compiled resources
|
||||
c.g_application_set_resource_base_path(gapp, "/com/mitchellh/ghostty");
|
||||
c.g_resources_register(c.ghostty_get_resource());
|
||||
|
||||
// The `activate` signal is used when Ghostty is first launched and when a
|
||||
// secondary Ghostty is launched and requests a new window.
|
||||
_ = c.g_signal_connect_data(
|
||||
app,
|
||||
"activate",
|
||||
@ -169,7 +177,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
if (c.g_main_context_acquire(ctx) == 0) return error.GtkContextAcquireFailed;
|
||||
errdefer c.g_main_context_release(ctx);
|
||||
|
||||
const gapp = @as(*c.GApplication, @ptrCast(app));
|
||||
var err_: ?*c.GError = null;
|
||||
if (c.g_application_register(
|
||||
gapp,
|
||||
|
@ -53,6 +53,7 @@ fn init(self: *ConfigErrors, app: *App) !void {
|
||||
c.gtk_window_set_title(gtk_window, "Configuration Errors");
|
||||
c.gtk_window_set_default_size(gtk_window, 600, 275);
|
||||
c.gtk_window_set_resizable(gtk_window, 0);
|
||||
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
|
||||
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Set some state
|
||||
|
@ -943,32 +943,14 @@ pub fn showDesktopNotification(
|
||||
0 => "Ghostty",
|
||||
else => title,
|
||||
};
|
||||
|
||||
const notif = c.g_notification_new(t.ptr);
|
||||
defer c.g_object_unref(notif);
|
||||
c.g_notification_set_body(notif, body.ptr);
|
||||
|
||||
// Find our icon in the current icon theme. Not pretty, but the builtin GIO
|
||||
// method "g_themed_icon_new" doesn't search XDG_DATA_DIRS, so any install
|
||||
// not in /usr/share will be unable to find an icon
|
||||
const display = c.gdk_display_get_default();
|
||||
const theme = c.gtk_icon_theme_get_for_display(display);
|
||||
const icon = c.gtk_icon_theme_lookup_icon(
|
||||
theme,
|
||||
"com.mitchellh.ghostty",
|
||||
null,
|
||||
48,
|
||||
1, // Window scale
|
||||
c.GTK_TEXT_DIR_LTR,
|
||||
0,
|
||||
);
|
||||
const icon = c.g_themed_icon_new("com.mitchellh.ghostty");
|
||||
defer c.g_object_unref(icon);
|
||||
// Get the filepath of the icon we found
|
||||
const file = c.gtk_icon_paintable_get_file(icon);
|
||||
defer c.g_object_unref(file);
|
||||
// Create a GIO icon
|
||||
const gicon = c.g_file_icon_new(file);
|
||||
defer c.g_object_unref(gicon);
|
||||
c.g_notification_set_icon(notif, gicon);
|
||||
c.g_notification_set_icon(notif, icon);
|
||||
|
||||
const g_app: *c.GApplication = @ptrCast(self.app.app);
|
||||
|
||||
|
@ -18,7 +18,6 @@ const CoreSurface = @import("../../Surface.zig");
|
||||
const App = @import("App.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const icon = @import("icon.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@ -31,10 +30,6 @@ window: *c.GtkWindow,
|
||||
/// The notebook (tab grouping) for this window.
|
||||
notebook: *c.GtkNotebook,
|
||||
|
||||
/// The resources directory for the icon (if any). We need to retain a
|
||||
/// pointer to this because GTK can use it at any time.
|
||||
icon: icon.Icon,
|
||||
|
||||
pub fn create(alloc: Allocator, app: *App) !*Window {
|
||||
// Allocate a fixed pointer for our window. We try to minimize
|
||||
// allocations but windows and other GUI requirements are so minimal
|
||||
@ -53,7 +48,6 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// Set up our own state
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.icon = undefined,
|
||||
.window = undefined,
|
||||
.notebook = undefined,
|
||||
};
|
||||
@ -70,10 +64,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// to disable this so that terminal programs can capture F10 (such as htop)
|
||||
c.gtk_window_set_handle_menubar_accel(gtk_window, 0);
|
||||
|
||||
// If we don't have the icon then we'll try to add our resources dir
|
||||
// to the search path and see if we can find it there.
|
||||
self.icon = try icon.appIcon(self.app, window);
|
||||
c.gtk_window_set_icon_name(gtk_window, self.icon.name);
|
||||
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
|
||||
|
||||
// Apply background opacity if we have it
|
||||
if (app.config.@"background-opacity" < 1) {
|
||||
@ -189,9 +180,7 @@ fn initActions(self: *Window) void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Window) void {
|
||||
self.icon.deinit(self.app);
|
||||
}
|
||||
pub fn deinit(_: *Window) void {}
|
||||
|
||||
/// Add a new tab to this window.
|
||||
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
|
||||
|
@ -7,6 +7,9 @@ const c = @cImport({
|
||||
@cInclude("gdk/x11/gdkx.h");
|
||||
// Xkb for X11 state handling
|
||||
@cInclude("X11/XKBlib.h");
|
||||
|
||||
// generated header files
|
||||
@cInclude("ghostty_resources.h");
|
||||
});
|
||||
|
||||
pub usingnamespace c;
|
||||
|
124
src/apprt/gtk/gresource.zig
Normal file
124
src/apprt/gtk/gresource.zig
Normal file
@ -0,0 +1,124 @@
|
||||
const std = @import("std");
|
||||
|
||||
const css_files = [_][]const u8{
|
||||
"style.css",
|
||||
"style-dark.css",
|
||||
"style-hc.css",
|
||||
"style-hc-dark.css",
|
||||
};
|
||||
|
||||
const icons = [_]struct {
|
||||
alias: []const u8,
|
||||
source: []const u8,
|
||||
}{
|
||||
.{
|
||||
.alias = "16x16",
|
||||
.source = "16x16",
|
||||
},
|
||||
.{
|
||||
.alias = "16x16@2",
|
||||
.source = "16x16@2x@2x",
|
||||
},
|
||||
.{
|
||||
.alias = "32x32",
|
||||
.source = "32x32",
|
||||
},
|
||||
.{
|
||||
.alias = "32x32@2",
|
||||
.source = "32x32@2x@2x",
|
||||
},
|
||||
.{
|
||||
.alias = "128x128",
|
||||
.source = "128x128",
|
||||
},
|
||||
.{
|
||||
.alias = "128x128@2",
|
||||
.source = "128x128@2x@2x",
|
||||
},
|
||||
.{
|
||||
.alias = "256x256",
|
||||
.source = "256x256",
|
||||
},
|
||||
.{
|
||||
.alias = "256x256@2",
|
||||
.source = "256x256@2x@2x",
|
||||
},
|
||||
.{
|
||||
.alias = "512x512",
|
||||
.source = "512x512",
|
||||
},
|
||||
};
|
||||
|
||||
pub const gresource_xml_gtk = comptimeGenerateGResourceXML(false);
|
||||
pub const gresource_xml_libadwaita = comptimeGenerateGResourceXML(true);
|
||||
|
||||
fn comptimeGenerateGResourceXML(comptime libadwaita: bool) []const u8 {
|
||||
comptime {
|
||||
@setEvalBranchQuota(13000);
|
||||
var counter = std.io.countingWriter(std.io.null_writer);
|
||||
try writeGResourceXML(libadwaita, &counter.writer());
|
||||
|
||||
var buf: [counter.bytes_written]u8 = undefined;
|
||||
var stream = std.io.fixedBufferStream(&buf);
|
||||
try writeGResourceXML(libadwaita, stream.writer());
|
||||
return stream.getWritten();
|
||||
}
|
||||
}
|
||||
|
||||
fn writeGResourceXML(libadwaita: bool, writer: anytype) !void {
|
||||
try writer.writeAll(
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<gresources>
|
||||
\\
|
||||
);
|
||||
if (libadwaita) {
|
||||
try writer.writeAll(
|
||||
\\ <gresource prefix="/com/mitchellh/ghostty">
|
||||
\\
|
||||
);
|
||||
for (css_files) |css_file| {
|
||||
try writer.print(
|
||||
" <file compressed=\"true\" alias=\"{s}\">src/apprt/gtk/{s}</file>\n",
|
||||
.{ css_file, css_file },
|
||||
);
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\ </gresource>
|
||||
\\
|
||||
);
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\ <gresource prefix="/com/mitchellh/ghostty/icons">
|
||||
\\
|
||||
);
|
||||
for (icons) |icon| {
|
||||
try writer.print(
|
||||
" <file preprocess=\"to-pixdata\" alias=\"{s}/apps/com.mitchellh.ghostty.png\">images/icons/icon_{s}.png</file>\n",
|
||||
.{ icon.alias, icon.source },
|
||||
);
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\ </gresource>
|
||||
\\</gresources>
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
pub const dependencies_gtk = deps: {
|
||||
var deps: [icons.len][]const u8 = undefined;
|
||||
for (icons, 0..) |icon, i| {
|
||||
deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
|
||||
}
|
||||
break :deps deps;
|
||||
};
|
||||
|
||||
pub const dependencies_libadwaita = deps: {
|
||||
var deps: [css_files.len + icons.len][]const u8 = undefined;
|
||||
for (css_files, 0..) |css_file, i| {
|
||||
deps[i] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
|
||||
}
|
||||
for (icons, css_files.len..) |icon, i| {
|
||||
deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
|
||||
}
|
||||
break :deps deps;
|
||||
};
|
@ -1,63 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const c = @import("c.zig");
|
||||
const global_state = &@import("../../main.zig").state;
|
||||
|
||||
const log = std.log.scoped(.gtk_icon);
|
||||
|
||||
/// An icon. The icon may be associated with some allocated state so when
|
||||
/// the icon is no longer in use it should be deinitialized.
|
||||
pub const Icon = struct {
|
||||
name: [:0]const u8,
|
||||
state: ?[:0]const u8 = null,
|
||||
|
||||
pub fn deinit(self: *const Icon, app: *App) void {
|
||||
if (self.state) |v| app.core_app.alloc.free(v);
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns the application icon that can be used anywhere. This attempts to
|
||||
/// find the icon in the theme and if it can't be found, it is loaded from
|
||||
/// the resources dir. If the resources dir can't be found, we'll log a warning
|
||||
/// and let GTK choose a fallback.
|
||||
pub fn appIcon(app: *App, widget: *c.GtkWidget) !Icon {
|
||||
const icon_name = "com.mitchellh.ghostty";
|
||||
var result: Icon = .{ .name = icon_name };
|
||||
|
||||
// If we don't have the icon then we'll try to add our resources dir
|
||||
// to the search path and see if we can find it there.
|
||||
const icon_theme = c.gtk_icon_theme_get_for_display(c.gtk_widget_get_display(widget));
|
||||
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) icon: {
|
||||
const resources_dir = global_state.resources_dir orelse {
|
||||
log.info("gtk app missing Ghostty icon and no resources dir detected", .{});
|
||||
log.info("gtk app will not have Ghostty icon", .{});
|
||||
break :icon;
|
||||
};
|
||||
|
||||
// The resources dir usually is `/usr/share/ghostty` but GTK icons
|
||||
// go into `/usr/share/icons`.
|
||||
const base = std.fs.path.dirname(resources_dir) orelse {
|
||||
log.warn(
|
||||
"unexpected error getting dirname of resources dir dir={s}",
|
||||
.{resources_dir},
|
||||
);
|
||||
break :icon;
|
||||
};
|
||||
|
||||
// Note that this method for adding the icon search path is
|
||||
// a fallback mechanism. The recommended mechanism is the
|
||||
// Freedesktop Icon Theme Specification. We distribute a ".desktop"
|
||||
// file in zig-out/share that should be installed to the proper
|
||||
// place.
|
||||
const dir = try std.fmt.allocPrintZ(app.core_app.alloc, "{s}/icons", .{base});
|
||||
errdefer app.core_app.alloc.free(dir);
|
||||
result.state = dir;
|
||||
c.gtk_icon_theme_add_search_path(icon_theme, dir.ptr);
|
||||
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) {
|
||||
log.warn("Ghostty icon for gtk app not found", .{});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@ -7,7 +7,6 @@ const Surface = @import("Surface.zig");
|
||||
const TerminalWindow = @import("Window.zig");
|
||||
const ImguiWidget = @import("ImguiWidget.zig");
|
||||
const c = @import("c.zig");
|
||||
const icon = @import("icon.zig");
|
||||
const CoreInspector = @import("../../inspector/main.zig").Inspector;
|
||||
|
||||
const log = std.log.scoped(.inspector);
|
||||
@ -125,14 +124,12 @@ pub const Inspector = struct {
|
||||
const Window = struct {
|
||||
inspector: *Inspector,
|
||||
window: *c.GtkWindow,
|
||||
icon: icon.Icon,
|
||||
imgui_widget: ImguiWidget,
|
||||
|
||||
pub fn init(self: *Window, inspector: *Inspector) !void {
|
||||
// Initialize to undefined
|
||||
self.* = .{
|
||||
.inspector = inspector,
|
||||
.icon = undefined,
|
||||
.window = undefined,
|
||||
.imgui_widget = undefined,
|
||||
};
|
||||
@ -144,8 +141,7 @@ const Window = struct {
|
||||
self.window = gtk_window;
|
||||
c.gtk_window_set_title(gtk_window, "Ghostty: Terminal Inspector");
|
||||
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||
self.icon = try icon.appIcon(self.inspector.surface.app, window);
|
||||
c.gtk_window_set_icon_name(gtk_window, self.icon.name);
|
||||
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
|
||||
|
||||
// Initialize our imgui widget
|
||||
try self.imgui_widget.init();
|
||||
@ -163,7 +159,6 @@ const Window = struct {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Window) void {
|
||||
self.icon.deinit(self.inspector.surface.app);
|
||||
self.inspector.locationDidClose();
|
||||
}
|
||||
|
||||
|
0
src/apprt/gtk/style-dark.css
Normal file
0
src/apprt/gtk/style-dark.css
Normal file
0
src/apprt/gtk/style-hc-dark.css
Normal file
0
src/apprt/gtk/style-hc-dark.css
Normal file
0
src/apprt/gtk/style-hc.css
Normal file
0
src/apprt/gtk/style-hc.css
Normal file
0
src/apprt/gtk/style.css
Normal file
0
src/apprt/gtk/style.css
Normal file
Reference in New Issue
Block a user