diff --git a/src/Surface.zig b/src/Surface.zig index a25b200f7..6005635d9 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -546,7 +546,7 @@ pub fn init( .shell_integration = config.@"shell-integration", .shell_integration_features = config.@"shell-integration-features", .working_directory = config.@"working-directory", - .resources_dir = global_state.resources_dir, + .resources_dir = global_state.resources_dir.host(), .term = config.term, // Get the cgroup if we're on linux and have the decl. I'd love diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 882448ed7..3193065c4 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -2,6 +2,7 @@ pub const App = @import("gtk/App.zig"); pub const Surface = @import("gtk/Surface.zig"); +pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/apprt/gtk/flatpak.zig b/src/apprt/gtk/flatpak.zig new file mode 100644 index 000000000..dc47c671b --- /dev/null +++ b/src/apprt/gtk/flatpak.zig @@ -0,0 +1,29 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const build_config = @import("../../build_config.zig"); +const internal_os = @import("../../os/main.zig"); +const glib = @import("glib"); + +pub fn resourcesDir(alloc: Allocator) !internal_os.ResourcesDir { + if (comptime build_config.flatpak) { + // Only consult Flatpak runtime data for host case. + if (internal_os.isFlatpak()) { + var result: internal_os.ResourcesDir = .{ + .app_path = try alloc.dupe(u8, "/app/share/ghostty"), + }; + errdefer alloc.free(result.app_path.?); + + const keyfile = glib.KeyFile.new(); + defer keyfile.unref(); + + if (keyfile.loadFromFile("/.flatpak-info", .{}, null) == 0) return result; + const app_dir = std.mem.span(keyfile.getString("Instance", "app-path", null)) orelse return result; + defer glib.free(app_dir.ptr); + + result.host_path = try std.fs.path.join(alloc, &[_][]const u8{ app_dir, "share", "ghostty" }); + return result; + } + } + + return try internal_os.resourcesDir(alloc); +} diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 4bb8a74eb..e80a92286 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -115,7 +115,8 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { const stderr = std.io.getStdErr().writer(); const stdout = std.io.getStdOut().writer(); - if (global_state.resources_dir == null) + const resources_dir = global_state.resources_dir.app(); + if (resources_dir == null) try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++ "that Ghostty is installed correctly.\n", .{}); diff --git a/src/config/theme.zig b/src/config/theme.zig index 21d6faf08..8fa7c93dc 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -56,7 +56,7 @@ pub const Location = enum { }, .resources => try std.fs.path.join(arena_alloc, &.{ - global_state.resources_dir orelse return null, + global_state.resources_dir.app() orelse return null, "themes", }), }; diff --git a/src/global.zig b/src/global.zig index d11dd775b..76b57898b 100644 --- a/src/global.zig +++ b/src/global.zig @@ -9,6 +9,7 @@ const harfbuzz = @import("harfbuzz"); const oni = @import("oniguruma"); const crash = @import("crash/main.zig"); const renderer = @import("renderer.zig"); +const apprt = @import("apprt.zig"); /// We export the xev backend we want to use so that the rest of /// Ghostty can import this once and have access to the proper @@ -35,7 +36,7 @@ pub const GlobalState = struct { /// 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, + resources_dir: internal_os.ResourcesDir, /// Where logging should go pub const Logging = union(enum) { @@ -62,7 +63,7 @@ pub const GlobalState = struct { .action = null, .logging = .{ .stderr = {} }, .rlimits = .{}, - .resources_dir = null, + .resources_dir = .{}, }; errdefer self.deinit(); @@ -170,11 +171,14 @@ pub const GlobalState = struct { // Find our resources directory once for the app so every launch // hereafter can use this cached value. - self.resources_dir = try internal_os.resourcesDir(self.alloc); - errdefer if (self.resources_dir) |dir| self.alloc.free(dir); + self.resources_dir = rd: { + if (@hasDecl(apprt.runtime, "resourcesDir")) break :rd try apprt.runtime.resourcesDir(self.alloc); + break :rd try internal_os.resourcesDir(self.alloc); + }; + errdefer self.resources_dir.deinit(self.alloc); // Setup i18n - if (self.resources_dir) |v| internal_os.i18n.init(v) catch |err| { + if (self.resources_dir.app()) |v| internal_os.i18n.init(v) catch |err| { std.log.warn("failed to init i18n, translations will not be available err={}", .{err}); }; } @@ -182,7 +186,7 @@ pub const GlobalState = struct { /// Cleans up the global state. This doesn't _need_ to be called but /// doing so in dev modes will check for memory leaks. pub fn deinit(self: *GlobalState) void { - if (self.resources_dir) |dir| self.alloc.free(dir); + self.resources_dir.deinit(self.alloc); // Flush our crash logs crash.deinit(); diff --git a/src/os/main.zig b/src/os/main.zig index 96297211c..906e3d150 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -56,6 +56,7 @@ pub const open = openpkg.open; pub const OpenType = openpkg.Type; pub const pipe = pipepkg.pipe; pub const resourcesDir = resourcesdir.resourcesDir; +pub const ResourcesDir = resourcesdir.ResourcesDir; pub const ShellEscapeWriter = shell.ShellEscapeWriter; test { diff --git a/src/os/resourcesdir.zig b/src/os/resourcesdir.zig index 0ef92d3b3..d4287c1bd 100644 --- a/src/os/resourcesdir.zig +++ b/src/os/resourcesdir.zig @@ -2,13 +2,41 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; +pub const ResourcesDir = struct { + app_path: ?[]const u8 = null, + host_path: ?[]const u8 = null, + + /// Free resources held. Requires the same allocator as when resourcesDir() + /// is called. + pub fn deinit(self: *ResourcesDir, alloc: std.mem.Allocator) void { + if (self.app_path) |p| alloc.free(p); + if (self.host_path) |p| alloc.free(p); + } + + /// Get the directory to the bundled resources directory accessible + /// by the application. + pub fn app(self: *ResourcesDir) ?[]const u8 { + return self.app_path; + } + + /// Get the directory to the bundled resources directory accessible + /// by the host environment (i.e. for sandboxed applications). The + /// returned directory might not be accessible from the application + /// itself. + /// + /// In non-sandboxed environment, this should be the same as app(). + pub fn host(self: *ResourcesDir) ?[]const u8 { + return self.host_path orelse self.app_path; + } +}; + /// Gets the directory to the bundled resources directory, if it /// exists (not all platforms or packages have it). The output is /// owned by the caller. /// /// This is highly Ghostty-specific and can likely be generalized at /// some point but we can cross that bridge if we ever need to. -pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { +pub fn resourcesDir(alloc: std.mem.Allocator) !ResourcesDir { // Use the GHOSTTY_RESOURCES_DIR environment variable in release builds. // // In debug builds we try using terminfo detection first instead, since @@ -20,7 +48,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { // freed, do not try to use internal_os.getenv or posix getenv. if (comptime builtin.mode != .Debug) { if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| { - if (dir.len > 0) return dir; + if (dir.len > 0) return .{ .app_path = dir }; } else |err| switch (err) { error.EnvironmentVariableNotFound => {}, else => return err, @@ -38,7 +66,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { // 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; + var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return .{}; // We have an exe path! Climb the tree looking for the terminfo // bundle as we expect it. @@ -50,7 +78,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { if (comptime builtin.target.os.tag.isDarwin()) { inline for (sentinels) |sentinel| { if (try maybeDir(&dir_buf, dir, "Contents/Resources", sentinel)) |v| { - return try std.fs.path.join(alloc, &.{ v, "ghostty" }); + return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) }; } } } @@ -65,7 +93,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { if (builtin.target.os.tag == .freebsd) "local/share" else "share", sentinel, )) |v| { - return try std.fs.path.join(alloc, &.{ v, "ghostty" }); + return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) }; } } } @@ -74,14 +102,14 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { // fallback and use the provided resources dir. if (comptime builtin.mode == .Debug) { if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| { - if (dir.len > 0) return dir; + if (dir.len > 0) return .{ .app_path = dir }; } else |err| switch (err) { error.EnvironmentVariableNotFound => {}, else => return err, } } - return null; + return .{}; } /// Little helper to check if the "base/sub/suffix" directory exists and