summaryrefslogtreecommitdiff
path: root/src/os
diff options
context:
space:
mode:
authorAlan Moyano <alanmoyano203@gmail.com>2025-06-29 15:59:29 -0300
committerGitHub <noreply@github.com>2025-06-29 15:59:29 -0300
commit43a3338491b3f6c409606ebfaab3a38c96455144 (patch)
treeef2dd3fa426b45307e376a5809d2d41d23afb26b /src/os
parentcf7e76d8f23f7e048ff55fd4db5da7a08e5176c6 (diff)
parent0d55a1deefa3f67abb2981cc83b46af5058789b5 (diff)
Merge branch 'ghostty-org:main' into main
Diffstat (limited to 'src/os')
-rw-r--r--src/os/args.zig2
-rw-r--r--src/os/cf_release_thread.zig8
-rw-r--r--src/os/cgroup.zig19
-rw-r--r--src/os/dbus.zig21
-rw-r--r--src/os/desktop.zig18
-rw-r--r--src/os/flatpak.zig25
-rw-r--r--src/os/homedir.zig4
-rw-r--r--src/os/hostname.zig194
-rw-r--r--src/os/i18n.zig38
-rw-r--r--src/os/locale.zig7
-rw-r--r--src/os/macos.zig4
-rw-r--r--src/os/main.zig6
-rw-r--r--src/os/open.zig91
-rw-r--r--src/os/resourcesdir.zig53
-rw-r--r--src/os/systemd.zig65
15 files changed, 460 insertions, 95 deletions
diff --git a/src/os/args.zig b/src/os/args.zig
index 9f7401c94..a531a418b 100644
--- a/src/os/args.zig
+++ b/src/os/args.zig
@@ -12,7 +12,7 @@ const macos = @import("macos");
/// but handles macOS using NSProcessInfo instead of libc argc/argv.
pub fn iterator(allocator: Allocator) ArgIterator.InitError!ArgIterator {
//if (true) return try std.process.argsWithAllocator(allocator);
- return ArgIterator.initWithAllocator(allocator);
+ return .initWithAllocator(allocator);
}
/// Duck-typed to std.process.ArgIterator
diff --git a/src/os/cf_release_thread.zig b/src/os/cf_release_thread.zig
index dbf8e6592..445dc4864 100644
--- a/src/os/cf_release_thread.zig
+++ b/src/os/cf_release_thread.zig
@@ -8,6 +8,7 @@ const std = @import("std");
const builtin = @import("builtin");
const macos = @import("macos");
+const internal_os = @import("../os/main.zig");
const xev = @import("../global.zig").xev;
const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;
@@ -119,6 +120,13 @@ pub fn threadMain(self: *Thread) void {
fn threadMain_(self: *Thread) !void {
defer log.debug("cf release thread exited", .{});
+ // Right now, on Darwin, `std.Thread.setName` can only name the current
+ // thread, and we have no way to get the current thread from within it,
+ // so instead we use this code to name the thread instead.
+ if (builtin.os.tag.isDarwin()) {
+ internal_os.macos.pthread_setname_np(&"cf_release".*);
+ }
+
// Start the async handlers. We start these first so that they're
// registered even if anything below fails so we can drain the mailbox.
self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback);
diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig
index 5645e337a..4f13921c5 100644
--- a/src/os/cgroup.zig
+++ b/src/os/cgroup.zig
@@ -56,6 +56,25 @@ pub fn create(
}
}
+/// Remove a cgroup. This will only succeed if the cgroup is empty
+/// (has no processes). The cgroup path should be relative to the
+/// cgroup root (e.g. "/user.slice/surfaces/abc123.scope").
+pub fn remove(cgroup: []const u8) !void {
+ assert(cgroup.len > 0);
+ assert(cgroup[0] == '/');
+
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = try std.fmt.bufPrint(&buf, "/sys/fs/cgroup{s}", .{cgroup});
+ std.fs.cwd().deleteDir(path) catch |err| switch (err) {
+ // If it doesn't exist, that's fine - maybe it was already cleaned up
+ error.FileNotFound => {},
+
+ // Any other error we failed to delete it so we want to notify
+ // the user.
+ else => return err,
+ };
+}
+
/// Move the given PID into the given cgroup.
pub fn moveInto(
cgroup: []const u8,
diff --git a/src/os/dbus.zig b/src/os/dbus.zig
new file mode 100644
index 000000000..99824db71
--- /dev/null
+++ b/src/os/dbus.zig
@@ -0,0 +1,21 @@
+const std = @import("std");
+const builtin = @import("builtin");
+
+/// Returns true if the program was launched by D-Bus activation.
+///
+/// On Linux GTK, this returns true if the program was launched using D-Bus
+/// activation. It will return false if Ghostty was launched any other way.
+///
+/// For other platforms and app runtimes, this returns false.
+pub fn launchedByDbusActivation() bool {
+ return switch (builtin.os.tag) {
+ // On Linux, D-Bus activation sets `DBUS_STARTER_ADDRESS` and
+ // `DBUS_STARTER_BUS_TYPE`. If these environment variables are present
+ // (no matter the value) we were launched by D-Bus activation.
+ .linux => std.posix.getenv("DBUS_STARTER_ADDRESS") != null and
+ std.posix.getenv("DBUS_STARTER_BUS_TYPE") != null,
+
+ // No other system supports D-Bus so always return false.
+ else => false,
+ };
+}
diff --git a/src/os/desktop.zig b/src/os/desktop.zig
index c73f150e0..3bc843e5c 100644
--- a/src/os/desktop.zig
+++ b/src/os/desktop.zig
@@ -30,24 +30,24 @@ pub fn launchedFromDesktop() bool {
break :macos c.getppid() == 1;
},
- // On Linux, GTK sets GIO_LAUNCHED_DESKTOP_FILE and
+ // On Linux and BSD, GTK sets GIO_LAUNCHED_DESKTOP_FILE and
// GIO_LAUNCHED_DESKTOP_FILE_PID. We only check the latter to see if
// we match the PID and assume that if we do, we were launched from
// the desktop file. Pid comparing catches the scenario where
// another terminal was launched from a desktop file and then launches
// Ghostty and Ghostty inherits the env.
- .linux => linux: {
+ .linux, .freebsd => ul: {
const gio_pid_str = posix.getenv("GIO_LAUNCHED_DESKTOP_FILE_PID") orelse
- break :linux false;
+ break :ul false;
const pid = c.getpid();
const gio_pid = std.fmt.parseInt(
@TypeOf(pid),
gio_pid_str,
10,
- ) catch break :linux false;
+ ) catch break :ul false;
- break :linux gio_pid == pid;
+ break :ul gio_pid == pid;
},
// TODO: This should have some logic to detect this. Perhaps std.builtin.subsystem
@@ -71,14 +71,14 @@ pub const DesktopEnvironment = enum {
};
/// Detect what desktop environment we are running under. This is mainly used
-/// on Linux to enable or disable certain features but there may be more uses in
+/// on Linux and BSD to enable or disable certain features but there may be more uses in
/// the future.
pub fn desktopEnvironment() DesktopEnvironment {
return switch (comptime builtin.os.tag) {
.macos => .macos,
.windows => .windows,
- .linux => de: {
- if (@inComptime()) @compileError("Checking for the desktop environment on Linux must be done at runtime.");
+ .linux, .freebsd => de: {
+ if (@inComptime()) @compileError("Checking for the desktop environment on Linux/BSD must be done at runtime.");
// Use $XDG_SESSION_DESKTOP to determine what DE we are using on Linux
// https://www.freedesktop.org/software/systemd/man/latest/pam_systemd.html#desktop=
@@ -110,7 +110,7 @@ test "desktop environment" {
switch (builtin.os.tag) {
.macos => try testing.expectEqual(.macos, desktopEnvironment()),
.windows => try testing.expectEqual(.windows, desktopEnvironment()),
- .linux => {
+ .linux, .freebsd => {
const getenv = std.posix.getenv;
const setenv = @import("env.zig").setenv;
const unsetenv = @import("env.zig").unsetenv;
diff --git a/src/os/flatpak.zig b/src/os/flatpak.zig
index 7b92a8ba9..7bd84bc27 100644
--- a/src/os/flatpak.zig
+++ b/src/os/flatpak.zig
@@ -112,6 +112,8 @@ pub const FlatpakHostCommand = struct {
pub fn spawn(self: *FlatpakHostCommand, alloc: Allocator) !u32 {
const thread = try std.Thread.spawn(.{}, threadMain, .{ self, alloc });
thread.setName("flatpak-host-command") catch {};
+ // We don't track this thread, it will terminate on its own on command exit
+ thread.detach();
// Wait for the process to start or error.
self.state_mutex.lock();
@@ -232,9 +234,10 @@ pub const FlatpakHostCommand = struct {
};
// Get our bus connection.
- var g_err: [*c]c.GError = null;
+ var g_err: ?*c.GError = null;
+ defer if (g_err) |ptr| c.g_error_free(ptr);
const bus = c.g_bus_get_sync(c.G_BUS_TYPE_SESSION, null, &g_err) orelse {
- log.warn("signal error getting bus: {s}", .{g_err.*.message});
+ log.warn("signal error getting bus: {s}", .{g_err.?.*.message});
return Error.FlatpakSetupFail;
};
defer c.g_object_unref(bus);
@@ -258,7 +261,7 @@ pub const FlatpakHostCommand = struct {
&g_err,
);
if (g_err != null) {
- log.warn("signal send error: {s}", .{g_err.*.message});
+ log.warn("signal send error: {s}", .{g_err.?.*.message});
return;
}
defer c.g_variant_unref(reply);
@@ -278,9 +281,10 @@ pub const FlatpakHostCommand = struct {
// Get our bus connection. This has to remain active until we exit
// the thread otherwise our signals won't be called.
- var g_err: [*c]c.GError = null;
+ var g_err: ?*c.GError = null;
+ defer if (g_err) |ptr| c.g_error_free(ptr);
const bus = c.g_bus_get_sync(c.G_BUS_TYPE_SESSION, null, &g_err) orelse {
- log.warn("spawn error getting bus: {s}", .{g_err.*.message});
+ log.warn("spawn error getting bus: {s}", .{g_err.?.*.message});
self.updateState(.{ .err = {} });
return;
};
@@ -308,7 +312,8 @@ pub const FlatpakHostCommand = struct {
bus: *c.GDBusConnection,
loop: *c.GMainLoop,
) !void {
- var err: [*c]c.GError = null;
+ var err: ?*c.GError = null;
+ defer if (err) |ptr| c.g_error_free(ptr);
var arena_allocator = std.heap.ArenaAllocator.init(alloc);
defer arena_allocator.deinit();
const arena = arena_allocator.allocator();
@@ -317,15 +322,15 @@ pub const FlatpakHostCommand = struct {
const fd_list = c.g_unix_fd_list_new();
defer c.g_object_unref(fd_list);
if (c.g_unix_fd_list_append(fd_list, self.stdin, &err) < 0) {
- log.warn("error adding fd: {s}", .{err.*.message});
+ log.warn("error adding fd: {s}", .{err.?.*.message});
return Error.FlatpakSetupFail;
}
if (c.g_unix_fd_list_append(fd_list, self.stdout, &err) < 0) {
- log.warn("error adding fd: {s}", .{err.*.message});
+ log.warn("error adding fd: {s}", .{err.?.*.message});
return Error.FlatpakSetupFail;
}
if (c.g_unix_fd_list_append(fd_list, self.stderr, &err) < 0) {
- log.warn("error adding fd: {s}", .{err.*.message});
+ log.warn("error adding fd: {s}", .{err.?.*.message});
return Error.FlatpakSetupFail;
}
@@ -405,7 +410,7 @@ pub const FlatpakHostCommand = struct {
null,
&err,
) orelse {
- log.warn("Flatpak.HostCommand failed: {s}", .{err.*.message});
+ log.warn("Flatpak.HostCommand failed: {s}", .{err.?.*.message});
return Error.FlatpakRPCFail;
};
defer c.g_variant_unref(reply);
diff --git a/src/os/homedir.zig b/src/os/homedir.zig
index b5629fd65..f3d6e4498 100644
--- a/src/os/homedir.zig
+++ b/src/os/homedir.zig
@@ -14,7 +14,7 @@ const Error = error{
/// is generally an expensive process so the value should be cached.
pub inline fn home(buf: []u8) !?[]const u8 {
return switch (builtin.os.tag) {
- inline .linux, .macos => try homeUnix(buf),
+ inline .linux, .freebsd, .macos => try homeUnix(buf),
.windows => try homeWindows(buf),
// iOS doesn't have a user-writable home directory
@@ -122,7 +122,7 @@ pub const ExpandError = error{
/// than `buf.len`.
pub fn expandHome(path: []const u8, buf: []u8) ExpandError![]const u8 {
return switch (builtin.os.tag) {
- .linux, .macos => try expandHomeUnix(path, buf),
+ .linux, .freebsd, .macos => try expandHomeUnix(path, buf),
.ios => return path,
else => @compileError("unimplemented"),
};
diff --git a/src/os/hostname.zig b/src/os/hostname.zig
index 22f29ceff..a75ca1cbb 100644
--- a/src/os/hostname.zig
+++ b/src/os/hostname.zig
@@ -1,4 +1,5 @@
const std = @import("std");
+const builtin = @import("builtin");
const posix = std.posix;
pub const HostnameParsingError = error{
@@ -6,6 +7,96 @@ pub const HostnameParsingError = error{
NoSpaceLeft,
};
+pub const UrlParsingError = std.Uri.ParseError || error{
+ HostnameIsNotMacAddress,
+ NoSchemeProvided,
+};
+
+const mac_address_length = 17;
+
+fn isUriPathSeparator(c: u8) bool {
+ return switch (c) {
+ '?', '#' => true,
+ else => false,
+ };
+}
+
+fn isValidMacAddress(mac_address: []const u8) bool {
+ // A valid mac address has 6 two-character components with 5 colons, e.g. 12:34:56:ab:cd:ef.
+ if (mac_address.len != 17) {
+ return false;
+ }
+
+ for (mac_address, 0..) |c, i| {
+ if ((i + 1) % 3 == 0) {
+ if (c != ':') {
+ return false;
+ }
+ } else if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/// Parses the provided url to a `std.Uri` struct. This is very specific to getting hostname and
+/// path information for Ghostty's PWD reporting functionality. Takes into account that on macOS
+/// the url passed to this function might have a mac address as its hostname and parses it
+/// correctly.
+pub fn parseUrl(url: []const u8) UrlParsingError!std.Uri {
+ return std.Uri.parse(url) catch |e| {
+ // The mac-address-as-hostname issue is specific to macOS so we just return an error if we
+ // hit it on other platforms.
+ if (comptime builtin.os.tag != .macos) return e;
+
+ // It's possible this is a mac address on macOS where the last 2 characters in the
+ // address are non-digits, e.g. 'ff', and thus an invalid port.
+ //
+ // Example: file://12:34:56:78:90:12/path/to/file
+ if (e != error.InvalidPort) return e;
+
+ const url_without_scheme_start = std.mem.indexOf(u8, url, "://") orelse {
+ return error.NoSchemeProvided;
+ };
+ const scheme = url[0..url_without_scheme_start];
+ const url_without_scheme = url[url_without_scheme_start + 3 ..];
+
+ // The first '/' after the scheme marks the end of the hostname. If the first '/'
+ // following the end of the scheme is not at the right position this is not a
+ // valid mac address.
+ if (url_without_scheme.len != mac_address_length and
+ std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != mac_address_length)
+ {
+ return error.HostnameIsNotMacAddress;
+ }
+
+ // At this point we may have a mac address as the hostname.
+ const mac_address = url_without_scheme[0..mac_address_length];
+
+ if (!isValidMacAddress(mac_address)) {
+ return error.HostnameIsNotMacAddress;
+ }
+
+ var uri_path_end_idx: usize = mac_address_length;
+ while (uri_path_end_idx < url_without_scheme.len and
+ !isUriPathSeparator(url_without_scheme[uri_path_end_idx]))
+ {
+ uri_path_end_idx += 1;
+ }
+
+ // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI
+ // spec.
+ return .{
+ .scheme = scheme,
+ .host = .{ .percent_encoded = mac_address },
+ .path = .{
+ .percent_encoded = url_without_scheme[mac_address_length..uri_path_end_idx],
+ },
+ };
+ };
+}
+
/// Print the hostname from a file URI into a buffer.
pub fn bufPrintHostnameFromFileUri(
buf: []u8,
@@ -70,6 +161,101 @@ pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool {
return std.mem.eql(u8, hostname, ourHostname);
}
+test parseUrl {
+ // 1. Typical hostnames.
+
+ var uri = try parseUrl("file://personal.computer/home/test/");
+
+ try std.testing.expectEqualStrings("file", uri.scheme);
+ try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded);
+ try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
+ try std.testing.expect(uri.port == null);
+
+ uri = try parseUrl("kitty-shell-cwd://personal.computer/home/test/");
+
+ try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme);
+ try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded);
+ try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
+ try std.testing.expect(uri.port == null);
+
+ // 2. Hostnames that are mac addresses.
+
+ // Numerical mac addresses.
+
+ uri = try parseUrl("file://12:34:56:78:90:12/home/test/");
+
+ try std.testing.expectEqualStrings("file", uri.scheme);
+ try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded);
+ try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
+ try std.testing.expect(uri.port == 12);
+
+ uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12/home/test/");
+
+ try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme);
+ try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded);
+ try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
+ try std.testing.expect(uri.port == 12);
+
+ // Alphabetical mac addresses.
+
+ uri = try parseUrl("file://ab:cd:ef:ab:cd:ef/home/test/");
+
+ try std.testing.expectEqualStrings("file", uri.scheme);
+ try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded);
+ try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
+ try std.testing.expect(uri.port == null);
+
+ uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef/home/test/");
+
+ try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme);
+ try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded);
+ try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
+ try std.testing.expect(uri.port == null);
+
+ // 3. Hostnames that are mac addresses with no path.
+
+ // Numerical mac addresses.
+
+ uri = try parseUrl("file://12:34:56:78:90:12");
+
+ try std.testing.expectEqualStrings("file", uri.scheme);
+ try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded);
+ try std.testing.expectEqualStrings("", uri.path.percent_encoded);
+ try std.testing.expect(uri.port == 12);
+
+ uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12");
+
+ try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme);
+ try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded);
+ try std.testing.expectEqualStrings("", uri.path.percent_encoded);
+ try std.testing.expect(uri.port == 12);
+
+ // Alphabetical mac addresses.
+
+ uri = try parseUrl("file://ab:cd:ef:ab:cd:ef");
+
+ try std.testing.expectEqualStrings("file", uri.scheme);
+ try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded);
+ try std.testing.expectEqualStrings("", uri.path.percent_encoded);
+ try std.testing.expect(uri.port == null);
+
+ uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef");
+
+ try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme);
+ try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded);
+ try std.testing.expectEqualStrings("", uri.path.percent_encoded);
+ try std.testing.expect(uri.port == null);
+}
+
+test "parseUrl succeeds even if path component is missing" {
+ const uri = try parseUrl("file://12:34:56:78:90:ab");
+
+ try std.testing.expectEqualStrings("file", uri.scheme);
+ try std.testing.expectEqualStrings("12:34:56:78:90:ab", uri.host.?.percent_encoded);
+ try std.testing.expect(uri.path.isEmpty());
+ try std.testing.expect(uri.port == null);
+}
+
test "bufPrintHostnameFromFileUri succeeds with ascii hostname" {
const uri = try std.Uri.parse("file://localhost/");
@@ -86,6 +272,14 @@ test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" {
try std.testing.expectEqualStrings("12:34:56:78:90:12", actual);
}
+test "bufPrintHostnameFromFileUri succeeds with hostname as mac address with the last component as ascii" {
+ const uri = try parseUrl("file://12:34:56:78:90:ab");
+
+ var buf: [posix.HOST_NAME_MAX]u8 = undefined;
+ const actual = try bufPrintHostnameFromFileUri(&buf, uri);
+ try std.testing.expectEqualStrings("12:34:56:78:90:ab", actual);
+}
+
test "bufPrintHostnameFromFileUri succeeds with hostname as a mac address and the last section is < 10" {
const uri = try std.Uri.parse("file://12:34:56:78:90:05");
diff --git a/src/os/i18n.zig b/src/os/i18n.zig
index fd1d44ab0..1ba8a676c 100644
--- a/src/os/i18n.zig
+++ b/src/os/i18n.zig
@@ -69,23 +69,27 @@ pub const InitError = error{
/// want to set the domain for the entire application since this is also
/// used by libghostty.
pub fn init(resources_dir: []const u8) InitError!void {
- // i18n is unsupported on Windows
- if (builtin.os.tag == .windows) return;
-
- // Our resources dir is always nested below the share dir that
- // is standard for translations.
- const share_dir = std.fs.path.dirname(resources_dir) orelse
- return error.InvalidResourcesDir;
-
- // Build our locale path
- var buf: [std.fs.max_path_bytes]u8 = undefined;
- const path = std.fmt.bufPrintZ(&buf, "{s}/locale", .{share_dir}) catch
- return error.OutOfMemory;
-
- // Bind our bundle ID to the given locale path
- log.debug("binding domain={s} path={s}", .{ build_config.bundle_id, path });
- _ = bindtextdomain(build_config.bundle_id, path.ptr) orelse
- return error.OutOfMemory;
+ switch (builtin.os.tag) {
+ // i18n is unsupported on Windows
+ .windows => return,
+
+ else => {
+ // Our resources dir is always nested below the share dir that
+ // is standard for translations.
+ const share_dir = std.fs.path.dirname(resources_dir) orelse
+ return error.InvalidResourcesDir;
+
+ // Build our locale path
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = std.fmt.bufPrintZ(&buf, "{s}/locale", .{share_dir}) catch
+ return error.OutOfMemory;
+
+ // Bind our bundle ID to the given locale path
+ log.debug("binding domain={s} path={s}", .{ build_config.bundle_id, path });
+ _ = bindtextdomain(build_config.bundle_id, path.ptr) orelse
+ return error.OutOfMemory;
+ },
+ }
}
/// Set the global gettext domain to our bundle ID, allowing unqualified
diff --git a/src/os/locale.zig b/src/os/locale.zig
index 17e4d163c..b391d690f 100644
--- a/src/os/locale.zig
+++ b/src/os/locale.zig
@@ -108,11 +108,8 @@ fn setLangFromCocoa() void {
}
// Get our preferred languages and set that to the LANGUAGE
- // env var in case our language differs from our locale. We only
- // do this when the app is launched from the desktop because then
- // we're in an app bundle and we are expected to read from our
- // Bundle's preferred languages.
- if (internal_os.launchedFromDesktop()) language: {
+ // env var in case our language differs from our locale.
+ language: {
var buf: [1024]u8 = undefined;
const pref_ = preferredLanguageFromCocoa(
&buf,
diff --git a/src/os/macos.zig b/src/os/macos.zig
index ca7c81a47..100d0fe44 100644
--- a/src/os/macos.zig
+++ b/src/os/macos.zig
@@ -88,6 +88,10 @@ extern "c" fn pthread_set_qos_class_self_np(
relative_priority: c_int,
) c_int;
+pub extern "c" fn pthread_setname_np(
+ name: [*:0]const u8,
+) void;
+
pub const NSOperatingSystemVersion = extern struct {
major: i64,
minor: i64,
diff --git a/src/os/main.zig b/src/os/main.zig
index 36833f427..906e3d150 100644
--- a/src/os/main.zig
+++ b/src/os/main.zig
@@ -2,6 +2,7 @@
//! system. These aren't restricted to syscalls or low-level operations, but
//! also OS-specific features and conventions.
+const dbus = @import("dbus.zig");
const desktop = @import("desktop.zig");
const env = @import("env.zig");
const file = @import("file.zig");
@@ -12,6 +13,7 @@ const mouse = @import("mouse.zig");
const openpkg = @import("open.zig");
const pipepkg = @import("pipe.zig");
const resourcesdir = @import("resourcesdir.zig");
+const systemd = @import("systemd.zig");
// Namespaces
pub const args = @import("args.zig");
@@ -27,6 +29,7 @@ pub const shell = @import("shell.zig");
// Functions and types
pub const CFReleaseThread = @import("cf_release_thread.zig");
pub const TempDir = @import("TempDir.zig");
+pub const GetEnvResult = env.GetEnvResult;
pub const getEnvMap = env.getEnvMap;
pub const appendEnv = env.appendEnv;
pub const appendEnvAlways = env.appendEnvAlways;
@@ -35,6 +38,8 @@ pub const getenv = env.getenv;
pub const setenv = env.setenv;
pub const unsetenv = env.unsetenv;
pub const launchedFromDesktop = desktop.launchedFromDesktop;
+pub const launchedByDbusActivation = dbus.launchedByDbusActivation;
+pub const launchedBySystemd = systemd.launchedBySystemd;
pub const desktopEnvironment = desktop.desktopEnvironment;
pub const rlimit = file.rlimit;
pub const fixMaxFiles = file.fixMaxFiles;
@@ -51,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/open.zig b/src/os/open.zig
index f7eadd06e..ce62a7e0b 100644
--- a/src/os/open.zig
+++ b/src/os/open.zig
@@ -2,6 +2,8 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
+const log = std.log.scoped(.@"os-open");
+
/// The type of the data at the URL to open. This is used as a hint
/// to potentially open the URL in a different way.
pub const Type = enum {
@@ -12,68 +14,73 @@ pub const Type = enum {
/// Open a URL in the default handling application.
///
/// Any output on stderr is logged as a warning in the application logs.
-/// Output on stdout is ignored.
+/// Output on stdout is ignored. The allocator is used to buffer the
+/// log output and may allocate from another thread.
pub fn open(
alloc: Allocator,
typ: Type,
url: []const u8,
) !void {
- const cmd: OpenCommand = switch (builtin.os.tag) {
- .linux => .{ .child = std.process.Child.init(
+ var exe: std.process.Child = switch (builtin.os.tag) {
+ .linux, .freebsd => .init(
&.{ "xdg-open", url },
alloc,
- ) },
+ ),
- .windows => .{ .child = std.process.Child.init(
+ .windows => .init(
&.{ "rundll32", "url.dll,FileProtocolHandler", url },
alloc,
- ) },
+ ),
- .macos => .{
- .child = std.process.Child.init(
- switch (typ) {
- .text => &.{ "open", "-t", url },
- .unknown => &.{ "open", url },
- },
- alloc,
- ),
- .wait = true,
- },
+ .macos => .init(
+ switch (typ) {
+ .text => &.{ "open", "-t", url },
+ .unknown => &.{ "open", url },
+ },
+ alloc,
+ ),
.ios => return error.Unimplemented,
else => @compileError("unsupported OS"),
};
- var exe = cmd.child;
- if (cmd.wait) {
- // Pipe stdout/stderr so we can collect output from the command
- exe.stdout_behavior = .Pipe;
- exe.stderr_behavior = .Pipe;
- }
+ // Pipe stdout/stderr so we can collect output from the command.
+ // This must be set before spawning the process.
+ exe.stdout_behavior = .Pipe;
+ exe.stderr_behavior = .Pipe;
+ // Spawn the process on our same thread so we can detect failure
+ // quickly.
try exe.spawn();
- if (cmd.wait) {
- // 50 KiB is the default value used by std.process.Child.run
- const output_max_size = 50 * 1024;
-
- var stdout: std.ArrayListUnmanaged(u8) = .{};
- var stderr: std.ArrayListUnmanaged(u8) = .{};
- defer {
- stdout.deinit(alloc);
- stderr.deinit(alloc);
- }
+ // Create a thread that handles collecting output and reaping
+ // the process. This is done in a separate thread because SOME
+ // open implementations block and some do not. It's easier to just
+ // spawn a thread to handle this so that we never block.
+ const thread = try std.Thread.spawn(.{}, openThread, .{ alloc, exe });
+ thread.detach();
+}
- try exe.collectOutput(alloc, &stdout, &stderr, output_max_size);
- _ = try exe.wait();
+fn openThread(alloc: Allocator, exe_: std.process.Child) !void {
+ // 50 KiB is the default value used by std.process.Child.run and should
+ // be enough to get the output we care about.
+ const output_max_size = 50 * 1024;
- // If we have any stderr output we log it. This makes it easier for
- // users to debug why some open commands may not work as expected.
- if (stderr.items.len > 0) std.log.err("open stderr={s}", .{stderr.items});
+ var stdout: std.ArrayListUnmanaged(u8) = .{};
+ var stderr: std.ArrayListUnmanaged(u8) = .{};
+ defer {
+ stdout.deinit(alloc);
+ stderr.deinit(alloc);
}
-}
-const OpenCommand = struct {
- child: std.process.Child,
- wait: bool = false,
-};
+ // Copy the exe so it is non-const. This is necessary because wait()
+ // requires a mutable reference and we can't have one as a thread
+ // param.
+ var exe = exe_;
+ try exe.collectOutput(alloc, &stdout, &stderr, output_max_size);
+ _ = try exe.wait();
+
+ // If we have any stderr output we log it. This makes it easier for
+ // users to debug why some open commands may not work as expected.
+ if (stderr.items.len > 0) log.warn("wait stderr={s}", .{stderr.items});
+}
diff --git a/src/os/resourcesdir.zig b/src/os/resourcesdir.zig
index 6f69b91d3..278de44fc 100644
--- a/src/os/resourcesdir.zig
+++ b/src/os/resourcesdir.zig
@@ -2,13 +2,42 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
+pub const ResourcesDir = struct {
+ /// Avoid accessing these directly, use the app() and host() methods instead.
+ 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: 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: 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 +49,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,
@@ -32,12 +61,13 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
const sentinels = switch (comptime builtin.target.os.tag) {
.windows => .{"terminfo/ghostty.terminfo"},
.macos => .{"terminfo/78/xterm-ghostty"},
+ .freebsd => .{ "site-terminfo/g/ghostty", "site-terminfo/x/xterm-ghostty" },
else => .{ "terminfo/g/ghostty", "terminfo/x/xterm-ghostty" },
};
// 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.
@@ -49,17 +79,22 @@ 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" }) };
}
}
}
- // On all platforms, we look for a /usr/share style path. This
+ // On all platforms (except BSD), 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.
inline for (sentinels) |sentinel| {
- if (try maybeDir(&dir_buf, dir, "share", sentinel)) |v| {
- return try std.fs.path.join(alloc, &.{ v, "ghostty" });
+ if (try maybeDir(
+ &dir_buf,
+ dir,
+ if (builtin.target.os.tag == .freebsd) "local/share" else "share",
+ sentinel,
+ )) |v| {
+ return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) };
}
}
}
@@ -68,14 +103,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
diff --git a/src/os/systemd.zig b/src/os/systemd.zig
new file mode 100644
index 000000000..9b67296d6
--- /dev/null
+++ b/src/os/systemd.zig
@@ -0,0 +1,65 @@
+const std = @import("std");
+const builtin = @import("builtin");
+
+const log = std.log.scoped(.systemd);
+
+/// Returns true if the program was launched as a systemd service.
+///
+/// On Linux, this returns true if the program was launched as a systemd
+/// service. It will return false if Ghostty was launched any other way.
+///
+/// For other platforms and app runtimes, this returns false.
+pub fn launchedBySystemd() bool {
+ return switch (builtin.os.tag) {
+ .linux => linux: {
+ // On Linux, systemd sets the `INVOCATION_ID` (v232+) and the
+ // `JOURNAL_STREAM` (v231+) environment variables. If these
+ // environment variables are not present we were not launched by
+ // systemd.
+ if (std.posix.getenv("INVOCATION_ID") == null) break :linux false;
+ if (std.posix.getenv("JOURNAL_STREAM") == null) break :linux false;
+
+ // If `INVOCATION_ID` and `JOURNAL_STREAM` are present, check to make sure
+ // that our parent process is actually `systemd`, not some other terminal
+ // emulator that doesn't clean up those environment variables.
+ const ppid = std.os.linux.getppid();
+ if (ppid == 1) break :linux true;
+
+ // If the parent PID is not 1 we need to check to see if we were launched by
+ // a user systemd daemon. Do that by checking the `/proc/<ppid>/comm`
+ // to see if it ends with `systemd`.
+ var comm_path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const comm_path = std.fmt.bufPrint(&comm_path_buf, "/proc/{d}/comm", .{ppid}) catch {
+ log.err("unable to format comm path for pid {d}", .{ppid});
+ break :linux false;
+ };
+ const comm_file = std.fs.openFileAbsolute(comm_path, .{ .mode = .read_only }) catch {
+ log.err("unable to open '{s}' for reading", .{comm_path});
+ break :linux false;
+ };
+ defer comm_file.close();
+
+ // The maximum length of the command name is defined by
+ // `TASK_COMM_LEN` in the Linux kernel. This is usually 16
+ // bytes at the time of writing (Jun 2025) so its set to that.
+ // Also, since we only care to compare to "systemd", anything
+ // longer can be assumed to not be systemd.
+ const TASK_COMM_LEN = 16;
+ var comm_data_buf: [TASK_COMM_LEN]u8 = undefined;
+ const comm_size = comm_file.readAll(&comm_data_buf) catch {
+ log.err("problems reading from '{s}'", .{comm_path});
+ break :linux false;
+ };
+ const comm_data = comm_data_buf[0..comm_size];
+
+ break :linux std.mem.eql(
+ u8,
+ std.mem.trimRight(u8, comm_data, "\n"),
+ "systemd",
+ );
+ },
+
+ // No other system supports systemd so always return false.
+ else => false,
+ };
+}