Skip to content

Commit 696d3b2

Browse files
committed
core, gtk: implement host resources dir for Flatpak
Introduces host resources directory as a new concept: A directory containing application resources that can only be accessed from the host operating system. This is significant for sandboxed application runtimes like Flatpak where shells spawned on the host should have access to application resources to enable integrations. Alongside this, apprt is now allowed to override the resources lookup logic.
1 parent 1980f9a commit 696d3b2

File tree

8 files changed

+80
-16
lines changed

8 files changed

+80
-16
lines changed

src/Surface.zig

+1-1
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ pub fn init(
543543
.shell_integration = config.@"shell-integration",
544544
.shell_integration_features = config.@"shell-integration-features",
545545
.working_directory = config.@"working-directory",
546-
.resources_dir = global_state.resources_dir,
546+
.resources_dir = global_state.resources_dir.host(),
547547
.term = config.term,
548548

549549
// Get the cgroup if we're on linux and have the decl. I'd love

src/apprt/gtk.zig

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
pub const App = @import("gtk/App.zig");
44
pub const Surface = @import("gtk/Surface.zig");
5+
pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir;
56

67
test {
78
@import("std").testing.refAllDecls(@This());

src/apprt/gtk/flatpak.zig

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const std = @import("std");
2+
const Allocator = std.mem.Allocator;
3+
const build_config = @import("../../build_config.zig");
4+
const internal_os = @import("../../os/main.zig");
5+
const glib = @import("glib");
6+
7+
pub fn resourcesDir(alloc: Allocator) !internal_os.ResourcesDir {
8+
if (comptime build_config.flatpak) {
9+
// Only consult Flatpak runtime data for host case.
10+
if (internal_os.isFlatpak()) {
11+
var result: internal_os.ResourcesDir = .{
12+
.app_path = try alloc.dupe(u8, "/app/share/ghostty"),
13+
};
14+
errdefer alloc.free(result.app_path.?);
15+
16+
const keyfile = glib.KeyFile.new();
17+
defer keyfile.unref();
18+
19+
if (keyfile.loadFromFile("/.flatpak-info", .{}, null) == 0) return result;
20+
const app_dir = std.mem.span(keyfile.getString("Instance", "app-path", null)) orelse return result;
21+
defer glib.free(app_dir.ptr);
22+
23+
result.host_path = try std.fs.path.join(alloc, &[_][]const u8{ app_dir, "share", "ghostty" });
24+
return result;
25+
}
26+
}
27+
28+
return try internal_os.resourcesDir(alloc);
29+
}

src/cli/list_themes.zig

+2-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
109109
const stderr = std.io.getStdErr().writer();
110110
const stdout = std.io.getStdOut().writer();
111111

112-
if (global_state.resources_dir == null)
112+
const resources_dir = global_state.resources_dir.app();
113+
if (resources_dir == null)
113114
try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++
114115
"that Ghostty is installed correctly.\n", .{});
115116

src/config/theme.zig

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub const Location = enum {
5656
},
5757

5858
.resources => try std.fs.path.join(arena_alloc, &.{
59-
global_state.resources_dir orelse return null,
59+
global_state.resources_dir.app() orelse return null,
6060
"themes",
6161
}),
6262
};

src/global.zig

+10-6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const harfbuzz = @import("harfbuzz");
99
const oni = @import("oniguruma");
1010
const crash = @import("crash/main.zig");
1111
const renderer = @import("renderer.zig");
12+
const apprt = @import("apprt.zig");
1213

1314
/// We export the xev backend we want to use so that the rest of
1415
/// Ghostty can import this once and have access to the proper
@@ -35,7 +36,7 @@ pub const GlobalState = struct {
3536

3637
/// The app resources directory, equivalent to zig-out/share when we build
3738
/// from source. This is null if we can't detect it.
38-
resources_dir: ?[]const u8,
39+
resources_dir: internal_os.ResourcesDir,
3940

4041
/// Where logging should go
4142
pub const Logging = union(enum) {
@@ -62,7 +63,7 @@ pub const GlobalState = struct {
6263
.action = null,
6364
.logging = .{ .stderr = {} },
6465
.rlimits = .{},
65-
.resources_dir = null,
66+
.resources_dir = .{},
6667
};
6768
errdefer self.deinit();
6869

@@ -170,19 +171,22 @@ pub const GlobalState = struct {
170171

171172
// Find our resources directory once for the app so every launch
172173
// hereafter can use this cached value.
173-
self.resources_dir = try internal_os.resourcesDir(self.alloc);
174-
errdefer if (self.resources_dir) |dir| self.alloc.free(dir);
174+
self.resources_dir = rd: {
175+
if (@hasDecl(apprt.runtime, "resourcesDir")) break :rd try apprt.runtime.resourcesDir(self.alloc);
176+
break :rd try internal_os.resourcesDir(self.alloc);
177+
};
178+
errdefer self.resources_dir.deinit(self.alloc);
175179

176180
// Setup i18n
177-
if (self.resources_dir) |v| internal_os.i18n.init(v) catch |err| {
181+
if (self.resources_dir.app()) |v| internal_os.i18n.init(v) catch |err| {
178182
std.log.warn("failed to init i18n, translations will not be available err={}", .{err});
179183
};
180184
}
181185

182186
/// Cleans up the global state. This doesn't _need_ to be called but
183187
/// doing so in dev modes will check for memory leaks.
184188
pub fn deinit(self: *GlobalState) void {
185-
if (self.resources_dir) |dir| self.alloc.free(dir);
189+
self.resources_dir.deinit(self.alloc);
186190

187191
// Flush our crash logs
188192
crash.deinit();

src/os/main.zig

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub const open = openpkg.open;
5151
pub const OpenType = openpkg.Type;
5252
pub const pipe = pipepkg.pipe;
5353
pub const resourcesDir = resourcesdir.resourcesDir;
54+
pub const ResourcesDir = resourcesdir.ResourcesDir;
5455
pub const ShellEscapeWriter = shell.ShellEscapeWriter;
5556

5657
test {

src/os/resourcesdir.zig

+35-7
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,41 @@ const std = @import("std");
22
const builtin = @import("builtin");
33
const Allocator = std.mem.Allocator;
44

5+
pub const ResourcesDir = struct {
6+
app_path: ?[]const u8 = null,
7+
host_path: ?[]const u8 = null,
8+
9+
/// Free resources held. Requires the same allocator as when resourcesDir()
10+
/// is called.
11+
pub fn deinit(self: *ResourcesDir, alloc: std.mem.Allocator) void {
12+
if (self.app_path) |p| alloc.free(p);
13+
if (self.host_path) |p| alloc.free(p);
14+
}
15+
16+
/// Get the directory to the bundled resources directory accessible
17+
/// by the application.
18+
pub fn app(self: *ResourcesDir) ?[]const u8 {
19+
return self.app_path;
20+
}
21+
22+
/// Get the directory to the bundled resources directory accessible
23+
/// by the host environment (i.e. for sandboxed applications). The
24+
/// returned directory might not be accessible from the application
25+
/// itself.
26+
///
27+
/// In non-sandboxed environment, this should be the same as app().
28+
pub fn host(self: *ResourcesDir) ?[]const u8 {
29+
return self.host_path orelse self.app_path;
30+
}
31+
};
32+
533
/// Gets the directory to the bundled resources directory, if it
634
/// exists (not all platforms or packages have it). The output is
735
/// owned by the caller.
836
///
937
/// This is highly Ghostty-specific and can likely be generalized at
1038
/// some point but we can cross that bridge if we ever need to.
11-
pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
39+
pub fn resourcesDir(alloc: std.mem.Allocator) !ResourcesDir {
1240
// Use the GHOSTTY_RESOURCES_DIR environment variable in release builds.
1341
//
1442
// In debug builds we try using terminfo detection first instead, since
@@ -20,7 +48,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
2048
// freed, do not try to use internal_os.getenv or posix getenv.
2149
if (comptime builtin.mode != .Debug) {
2250
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| {
23-
if (dir.len > 0) return dir;
51+
if (dir.len > 0) return .{ .app_path = dir };
2452
} else |err| switch (err) {
2553
error.EnvironmentVariableNotFound => {},
2654
else => return err,
@@ -37,7 +65,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
3765

3866
// Get the path to our running binary
3967
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
40-
var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return null;
68+
var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return .{};
4169

4270
// We have an exe path! Climb the tree looking for the terminfo
4371
// bundle as we expect it.
@@ -49,7 +77,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
4977
if (comptime builtin.target.os.tag.isDarwin()) {
5078
inline for (sentinels) |sentinel| {
5179
if (try maybeDir(&dir_buf, dir, "Contents/Resources", sentinel)) |v| {
52-
return try std.fs.path.join(alloc, &.{ v, "ghostty" });
80+
return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) };
5381
}
5482
}
5583
}
@@ -59,7 +87,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
5987
// Ghostty to be in an app bundle.
6088
inline for (sentinels) |sentinel| {
6189
if (try maybeDir(&dir_buf, dir, "share", sentinel)) |v| {
62-
return try std.fs.path.join(alloc, &.{ v, "ghostty" });
90+
return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) };
6391
}
6492
}
6593
}
@@ -68,14 +96,14 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
6896
// fallback and use the provided resources dir.
6997
if (comptime builtin.mode == .Debug) {
7098
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| {
71-
if (dir.len > 0) return dir;
99+
if (dir.len > 0) return .{ .app_path = dir };
72100
} else |err| switch (err) {
73101
error.EnvironmentVariableNotFound => {},
74102
else => return err,
75103
}
76104
}
77105

78-
return null;
106+
return .{};
79107
}
80108

81109
/// Little helper to check if the "base/sub/suffix" directory exists and

0 commit comments

Comments
 (0)