Merge pull request #251 from mitchellh/resources-dir

gtk: app icon
This commit is contained in:
Mitchell Hashimoto
2023-08-08 09:59:07 -07:00
committed by GitHub
9 changed files with 140 additions and 88 deletions

View File

@ -16,6 +16,7 @@ const Config = @import("config.zig").Config;
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue;
const renderer = @import("renderer.zig");
const font = @import("font/main.zig");
const internal_os = @import("os/main.zig");
const macos = @import("macos");
const objc = @import("objc");
@ -40,6 +41,10 @@ mailbox: Mailbox.Queue,
/// Set to true once we're quitting. This never goes false again.
quit: bool,
/// The app resources directory, equivalent to zig-out/share when we build
/// from source. This is null if we can't detect it.
resources_dir: ?[]const u8 = null,
/// Initialize the main app instance. This creates the main window, sets
/// up the renderer state, compiles the shaders, etc. This is the primary
/// "startup" logic.
@ -48,11 +53,21 @@ pub fn create(
) !*App {
var app = try alloc.create(App);
errdefer alloc.destroy(app);
// Find our resources directory once for the app so every launch
// hereafter can use this cached value.
var resources_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const resources_dir = if (try internal_os.resourcesDir(&resources_buf)) |dir|
try alloc.dupe(u8, dir)
else
null;
app.* = .{
.alloc = alloc,
.surfaces = .{},
.mailbox = .{},
.quit = false,
.resources_dir = resources_dir,
};
errdefer app.surfaces.deinit(alloc);
@ -64,6 +79,7 @@ pub fn destroy(self: *App) void {
for (self.surfaces.items) |surface| surface.deinit();
self.surfaces.deinit(self.alloc);
if (self.resources_dir) |dir| self.alloc.free(dir);
self.alloc.destroy(self);
}

View File

@ -182,6 +182,7 @@ pub fn init(
alloc: Allocator,
config: *const configpkg.Config,
app_mailbox: App.Mailbox,
app_resources_dir: ?[]const u8,
rt_surface: *apprt.runtime.Surface,
) !void {
// Initialize our renderer with our initialized surface.
@ -405,6 +406,7 @@ pub fn init(
.screen_size = screen_size,
.full_config = config,
.config = try termio.Impl.DerivedConfig.init(alloc, config),
.resources_dir = app_resources_dir,
.renderer_state = &self.renderer_state,
.renderer_wakeup = render_thread.wakeup,
.renderer_mailbox = render_thread.mailbox,

View File

@ -177,6 +177,7 @@ pub const Surface = struct {
app.core_app.alloc,
&config,
.{ .rt_app = app, .mailbox = &app.core_app.mailbox },
app.core_app.resources_dir,
self,
);
errdefer self.core_surface.deinit();

View File

@ -363,6 +363,7 @@ pub const Surface = struct {
app.app.alloc,
&config,
.{ .rt_app = app, .mailbox = &app.app.mailbox },
app.app.resources_dir,
self,
);
errdefer self.core_surface.deinit();

View File

@ -298,6 +298,9 @@ const Window = struct {
/// The background CSS for the window (if any).
css_window_background: ?[]u8 = null,
/// The resources directory for the icon (if any).
icon_search_dir: ?[:0]const u8 = null,
pub fn init(self: *Window, app: *App) !void {
// Set up our own state
self.* = .{
@ -314,6 +317,31 @@ const Window = struct {
c.gtk_window_set_title(gtk_window, "Ghostty");
c.gtk_window_set_default_size(gtk_window, 1000, 600);
// 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_name = "com.mitchellh.ghostty";
const icon_theme = c.gtk_icon_theme_get_for_display(c.gtk_widget_get_display(window));
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) icon: {
const base = self.app.core_app.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;
};
// 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});
self.icon_search_dir = 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", .{});
}
}
c.gtk_window_set_icon_name(gtk_window, icon_name);
// Apply background opacity if we have it
if (app.config.@"background-opacity" < 1) {
var css = try std.fmt.allocPrint(
@ -361,6 +389,7 @@ const Window = struct {
pub fn deinit(self: *Window) void {
if (self.css_window_background) |ptr| self.app.core_app.alloc.free(ptr);
if (self.icon_search_dir) |ptr| self.app.core_app.alloc.free(ptr);
}
/// Add a new tab to this window.
@ -765,6 +794,7 @@ pub const Surface = struct {
self.app.core_app.alloc,
&config,
.{ .rt_app = self.app, .mailbox = &self.app.core_app.mailbox },
self.app.core_app.resources_dir,
self,
);
errdefer self.core_surface.deinit();

View File

@ -7,6 +7,7 @@ pub usingnamespace @import("homedir.zig");
pub usingnamespace @import("locale.zig");
pub usingnamespace @import("macos_version.zig");
pub usingnamespace @import("mouse.zig");
pub usingnamespace @import("resourcesdir.zig");
pub const TempDir = @import("TempDir.zig");
pub const passwd = @import("passwd.zig");
pub const xdg = @import("xdg.zig");

77
src/os/resourcesdir.zig Normal file
View File

@ -0,0 +1,77 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
/// Gets the directory to the bundled resources directory, if it
/// exists (not all platforms or packages have it). The output is
/// written to the given buffer if we need to allocate. Note that
/// the output is not ALWAYS written to the buffer and may refer to
/// static memory.
///
/// This is highly Ghostty-specific and can likely be generalized at
/// some point but we can cross that bridge if we ever need to.
///
/// This returns error.OutOfMemory is buffer is not big enough.
pub fn resourcesDir(buf: []u8) !?[]const u8 {
// If we have an environment variable set, we always use that.
if (std.os.getenv("GHOSTTY_RESOURCES_DIR")) |dir| {
if (dir.len > 0) {
return dir;
}
}
// This is the sentinel value we look for in the path to know
// we've found the resources directory.
const sentinel = "terminfo/ghostty.termcap";
// Get the path to our running binary
var exe_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return null;
// We have an exe path! Climb the tree looking for the terminfo
// bundle as we expect it.
while (std.fs.path.dirname(exe)) |dir| {
exe = dir;
// On MacOS, we look for the app bundle path.
if (comptime builtin.target.isDarwin()) {
if (try maybeDir(buf, dir, "Contents/Resources", sentinel)) |v| {
return v;
}
}
// On all platforms, we look for a /usr/share style path. This
// is valid even on Mac since there is nothing that requires
// Ghostty to be in an app bundle.
if (try maybeDir(buf, dir, "share", sentinel)) |v| {
return v;
}
}
return null;
}
/// Little helper to check if the "base/sub/suffix" directory exists and
/// if so return true. The "suffix" is just used as a way to verify a directory
/// seems roughly right.
///
/// "buf" must be large enough to fit base + sub + suffix. This is generally
/// MAX_PATH_BYTES so its not a big deal.
pub fn maybeDir(
buf: []u8,
base: []const u8,
sub: []const u8,
suffix: []const u8,
) !?[]const u8 {
const path = try std.fmt.bufPrint(buf, "{s}/{s}/{s}", .{ base, sub, suffix });
if (std.fs.accessAbsolute(path, .{})) {
const len = path.len - suffix.len - 1;
return buf[0..len];
} else |_| {
// Folder doesn't exist. If a different error happens its okay
// we just ignore it and move on.
}
return null;
}

View File

@ -544,34 +544,22 @@ const Subprocess = struct {
};
errdefer env.deinit();
// Get our bundled resources directory, if it exists. We use this
// for terminfo, shell-integration, etc.
// If we have a resources dir then set our env var
const resources_key = "GHOSTTY_RESOURCES_DIR";
const resources_dir = dir: {
if (env.get(resources_key)) |dir| {
if (dir.len > 0) {
log.info("using Ghostty resources dir from env var: {s}", .{dir});
break :dir dir;
}
}
if (try resourcesDir(alloc)) |dir| {
if (opts.resources_dir) |dir| {
log.info("found Ghostty resources dir: {s}", .{dir});
try env.put(resources_key, dir);
break :dir dir;
}
log.warn("Ghostty resources dir not found, some features disabled", .{});
break :dir null;
};
// Set our TERM var. This is a bit complicated because we want to use
// the ghostty TERM value but we want to only do that if we have
// ghostty in the TERMINFO database.
//
// For now, we just look up a bundled dir but in the future we should
// also load the terminfo database and look for it.
if (try terminfoDir(alloc, resources_dir)) |dir| {
if (opts.resources_dir) |base| {
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const dir = try std.fmt.bufPrint(&buf, "{s}/terminfo", .{base});
try env.put("TERM", "xterm-ghostty");
try env.put("COLORTERM", "truecolor");
try env.put("TERMINFO", dir);
@ -641,7 +629,7 @@ const Subprocess = struct {
.zsh => .zsh,
};
const dir = resources_dir orelse break :shell null;
const dir = opts.resources_dir orelse break :shell null;
break :shell try shell_integration.setup(
dir,
final_path,
@ -860,73 +848,6 @@ const Subprocess = struct {
fn killCommandFlatpak(command: *FlatpakHostCommand) !void {
try command.signal(c.SIGHUP, true);
}
/// Gets the directory to the terminfo database, if it can be detected.
/// The memory returned can't be easily freed so the alloc should be
/// an arena or something similar.
fn terminfoDir(alloc: Allocator, base: ?[]const u8) !?[]const u8 {
const dir = base orelse return null;
return try tryDir(alloc, dir, "terminfo", "");
}
/// Gets the directory to the bundled resources directory, if it
/// exists (not all platforms or packages have it).
///
/// The memory returned can't be easily freed so the alloc should be
/// an arena or something similar.
fn resourcesDir(alloc: Allocator) !?[]const u8 {
// This is the sentinel value we look for in the path to know
// we've found the resources directory.
const sentinel = "terminfo/ghostty.termcap";
// Get the path to our running binary
var exe_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return null;
// We have an exe path! Climb the tree looking for the terminfo
// bundle as we expect it.
while (std.fs.path.dirname(exe)) |dir| {
exe = dir;
// On MacOS, we look for the app bundle path.
if (comptime builtin.target.isDarwin()) {
if (try tryDir(alloc, dir, "Contents/Resources", sentinel)) |v| {
return v;
}
}
// On all platforms, we look for a /usr/share style path. This
// is valid even on Mac since there is nothing that requires
// Ghostty to be in an app bundle.
if (try tryDir(alloc, dir, "share", sentinel)) |v| {
return v;
}
}
return null;
}
/// Little helper to check if the "base/sub/suffix" directory exists and
/// if so, duplicate the "base/sub" path and return it.
fn tryDir(
alloc: Allocator,
base: []const u8,
sub: []const u8,
suffix: []const u8,
) !?[]const u8 {
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const path = try std.fmt.bufPrint(&buf, "{s}/{s}/{s}", .{ base, sub, suffix });
if (std.fs.accessAbsolute(path, .{})) {
const len = path.len - suffix.len - 1;
return try alloc.dupe(u8, path[0..len]);
} else |_| {
// Folder doesn't exist. If a different error happens its okay
// we just ignore it and move on.
}
return null;
}
};
/// The read thread sits in a loop doing the following pseudo code:

View File

@ -20,6 +20,9 @@ full_config: *const Config,
/// The derived configuration for this termio implementation.
config: termio.Impl.DerivedConfig,
/// The application resources directory.
resources_dir: ?[]const u8,
/// The render state. The IO implementation can modify anything here. The
/// surface thread will setup the initial "terminal" pointer but the IO impl
/// is free to change that if that is useful (i.e. doing some sort of dual