feat: add FreeBSD support (#7606)

- [x] Waiting for mitchellh/libxev#167
- [x] Translations
- [x] x11
- [x] Wayland
- [ ] CI
This commit is contained in:
Mitchell Hashimoto
2025-06-21 14:16:52 -07:00
committed by GitHub
18 changed files with 122 additions and 57 deletions

View File

@ -8,8 +8,8 @@
.libxev = .{
// mitchellh/libxev
.url = "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz",
.hash = "libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz",
.url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
.hash = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3",
.lazy = true,
},
.vaxis = .{

6
build.zig.zon.json generated
View File

@ -64,10 +64,10 @@
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
"hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="
},
"libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz": {
"libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3": {
"name": "libxev",
"url": "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz",
"hash": "sha256-oKZqA9d79jHnp/HsqJWQE33Ffn5Ee5G4VnlQepQuY4o="
"url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
"hash": "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU="
},
"N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": {
"name": "libxml2",

6
build.zig.zon.nix generated
View File

@ -186,11 +186,11 @@ in
};
}
{
name = "libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz";
name = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3";
path = fetchZigArtifact {
name = "libxev";
url = "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz";
hash = "sha256-oKZqA9d79jHnp/HsqJWQE33Ffn5Ee5G4VnlQepQuY4o=";
url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz";
hash = "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU=";
};
}
{

2
build.zig.zon.txt generated
View File

@ -28,7 +28,7 @@ https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90
https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz
https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz
https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz
https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz

View File

@ -79,9 +79,9 @@
},
{
"type": "archive",
"url": "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz",
"dest": "vendor/p/libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz",
"sha256": "a0a66a03d77bf631e7a7f1eca89590137dc57e7e447b91b85679507a942e638a"
"url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
"dest": "vendor/p/libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3",
"sha256": "570141c83a29b6a88de5492894543b4db461c0c11145e0fd12bda98f14c26085"
},
{
"type": "archive",

View File

@ -164,11 +164,23 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu
"-DHAVE_SYS_STATVFS_H",
"-DFC_CACHEDIR=\"/var/cache/fontconfig\"",
"-DFC_TEMPLATEDIR=\"/usr/share/fontconfig/conf.avail\"",
"-DFONTCONFIG_PATH=\"/etc/fonts\"",
"-DCONFIGDIR=\"/usr/local/fontconfig/conf.d\"",
"-DFC_DEFAULT_FONTS=\"<dir>/usr/share/fonts</dir><dir>/usr/local/share/fonts</dir>\"",
});
if (target.result.os.tag == .freebsd) {
try flags.appendSlice(&.{
"-DFC_TEMPLATEDIR=\"/usr/local/etc/fonts/conf.avail\"",
"-DFONTCONFIG_PATH=\"/usr/local/etc/fonts\"",
"-DCONFIGDIR=\"/usr/local/etc/fonts/conf.d\"",
});
} else {
try flags.appendSlice(&.{
"-DFC_TEMPLATEDIR=\"/usr/share/fontconfig/conf.avail\"",
"-DFONTCONFIG_PATH=\"/etc/fonts\"",
"-DCONFIGDIR=\"/usr/local/fontconfig/conf.d\"",
});
}
if (target.result.os.tag == .linux) {
try flags.appendSlice(&.{
"-DHAVE_SYS_STATFS_H",

View File

@ -323,7 +323,7 @@ fn setupFd(src: File.Handle, target: i32) !void {
}
}
},
.ios, .macos => {
.freebsd, .ios, .macos => {
// Mac doesn't support dup3 so we use dup2. We purposely clear
// CLO_ON_EXEC for this fd.
const flags = try posix.fcntl(src, posix.F.GETFD, 0);

View File

@ -143,8 +143,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
if (config.@"async-backend" != .auto) {
const result: bool = switch (config.@"async-backend") {
.auto => unreachable,
.epoll => xev.prefer(.epoll),
.io_uring => xev.prefer(.io_uring),
.epoll => if (comptime xev.dynamic) xev.prefer(.epoll) else false,
.io_uring => if (comptime xev.dynamic) xev.prefer(.io_uring) else false,
};
if (result) {

View File

@ -1,6 +1,7 @@
const GhosttyI18n = @This();
const std = @import("std");
const builtin = @import("builtin");
const Config = @import("Config.zig");
const gresource = @import("../apprt/gtk/gresource.zig");
const internal_os = @import("../os/main.zig");
@ -21,6 +22,14 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n {
defer steps.deinit();
inline for (internal_os.i18n.locales) |locale| {
// There is no encoding suffix in the LC_MESSAGES path on FreeBSD,
// so we need to remove it from `locale` to have a correct destination string.
// (/usr/local/share/locale/en_AU/LC_MESSAGES)
const target_locale = comptime if (builtin.target.os.tag == .freebsd)
std.mem.trimRight(u8, locale, ".UTF-8")
else
locale;
const msgfmt = b.addSystemCommand(&.{ "msgfmt", "-o", "-" });
msgfmt.addFileArg(b.path("po/" ++ locale ++ ".po"));
@ -28,7 +37,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n {
msgfmt.captureStdOut(),
std.fmt.comptimePrint(
"share/locale/{s}/LC_MESSAGES/{s}.mo",
.{ locale, domain },
.{ target_locale, domain },
),
).step);
}

View File

@ -1,6 +1,7 @@
const GhosttyResources = @This();
const std = @import("std");
const builtin = @import("builtin");
const buildpkg = @import("main.zig");
const Config = @import("Config.zig");
const config_vim = @import("../config/vim.zig");
@ -16,6 +17,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Terminfo
terminfo: {
const os_tag = cfg.target.result.os.tag;
const terminfo_share_dir = if (os_tag == .freebsd)
"site-terminfo"
else
"terminfo";
// Encode our terminfo
var str = std.ArrayList(u8).init(b.allocator);
defer str.deinit();
@ -26,12 +33,19 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
const source = wf.add("ghostty.terminfo", str.items);
if (cfg.emit_terminfo) {
const source_install = b.addInstallFile(source, "share/terminfo/ghostty.terminfo");
const source_install = b.addInstallFile(
source,
if (os_tag == .freebsd)
"share/site-terminfo/ghostty.terminfo"
else
"share/terminfo/ghostty.terminfo",
);
try steps.append(&source_install.step);
}
// Windows doesn't have the binaries below.
if (cfg.target.result.os.tag == .windows) break :terminfo;
if (os_tag == .windows) break :terminfo;
// Convert to termcap source format if thats helpful to people and
// install it. The resulting value here is the termcap source in case
@ -43,7 +57,14 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
const out_source = run_step.captureStdOut();
_ = run_step.captureStdErr(); // so we don't see stderr
const cap_install = b.addInstallFile(out_source, "share/terminfo/ghostty.termcap");
const cap_install = b.addInstallFile(
out_source,
if (os_tag == .freebsd)
"share/site-terminfo/ghostty.termcap"
else
"share/terminfo/ghostty.termcap",
);
try steps.append(&cap_install.step);
}
@ -51,7 +72,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
{
const run_step = RunStep.create(b, "tic");
run_step.addArgs(&.{ "tic", "-x", "-o" });
const path = run_step.addOutputFileArg("terminfo");
const path = run_step.addOutputFileArg(terminfo_share_dir);
run_step.addFileArg(source);
_ = run_step.captureStdErr(); // so we don't see stderr
@ -63,7 +85,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
.windows => mkdir_step.addArgs(&.{"mkdir"}),
else => mkdir_step.addArgs(&.{ "mkdir", "-p" }),
}
mkdir_step.addArg(b.fmt("{s}/share/terminfo", .{b.install_path}));
mkdir_step.addArg(b.fmt(
"{s}/share/{s}",
.{ b.install_path, terminfo_share_dir },
));
try steps.append(&mkdir_step.step);
// Use cp -R instead of Step.InstallDir because we need to preserve

View File

@ -1823,7 +1823,7 @@ keybind: Keybinds = .{},
/// Automatically hide the quick terminal when focus shifts to another window.
/// Set it to false for the quick terminal to remain open even when it loses focus.
///
/// Defaults to true on macOS and on false on Linux. This is because global
/// Defaults to true on macOS and on false on Linux/BSD. This is because global
/// shortcuts on Linux require system configuration and are considerably less
/// accessible than on macOS, meaning that it is more preferable to keep the
/// quick terminal open until the user has completed their task.
@ -2404,7 +2404,10 @@ keybind: Keybinds = .{},
/// * `single-instance` - Enable cgroups only for Ghostty instances launched
/// as single-instance applications (see gtk-single-instance).
///
@"linux-cgroup": LinuxCgroup = .@"single-instance",
@"linux-cgroup": LinuxCgroup = if (builtin.os.tag == .linux)
.@"single-instance"
else
.never,
/// Memory limit for any individual terminal process (tab, split, window,
/// etc.) in bytes. If this is unset then no memory limit will be set.
@ -2817,7 +2820,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
.windows => {},
// Fast-path if we are Linux and have no args.
.linux => if (std.os.argv.len <= 1) return,
.linux, .freebsd => if (std.os.argv.len <= 1) return,
// Everything else we have to at least try because it may
// not use std.os.argv.
@ -2835,7 +2838,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
// styling, etc. based on the command.
//
// See: https://github.com/Vladimir-csp/xdg-terminal-exec
if (comptime builtin.os.tag == .linux) {
if ((comptime builtin.os.tag == .linux) or (comptime builtin.os.tag == .freebsd)) {
if (internal_os.xdg.parseTerminalExec(std.os.argv)) |args| {
const arena_alloc = self._arena.?.allocator();

View File

@ -11,7 +11,7 @@ pub const entries: []const Entry = entries: {
const native_idx = switch (builtin.os.tag) {
.ios, .macos => 4, // mac
.windows => 3, // win
.linux => 2, // xkb
.freebsd, .linux => 2, // xkb
else => @compileError("unsupported platform"),
};

View File

@ -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;

View File

@ -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"),
};

View File

@ -68,23 +68,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;
switch (builtin.os.tag) {
// i18n is unsupported on Windows
.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;
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;
// 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;
// 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

View File

@ -19,7 +19,7 @@ pub fn open(
url: []const u8,
) !void {
const cmd: OpenCommand = switch (builtin.os.tag) {
.linux => .{ .child = std.process.Child.init(
.linux, .freebsd => .{ .child = std.process.Child.init(
&.{ "xdg-open", url },
alloc,
) },

View File

@ -32,6 +32,7 @@ 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" },
};
@ -54,11 +55,16 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
}
}
// 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| {
if (try maybeDir(
&dir_buf,
dir,
if (builtin.target.os.tag == .freebsd) "local/share" else "share",
sentinel,
)) |v| {
return try std.fs.path.join(alloc, &.{ v, "ghostty" });
}
}

View File

@ -99,6 +99,10 @@ const PosixPty = struct {
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("util.h"); // openpty()
}),
.freebsd => @cImport({
@cInclude("termios.h"); // ioctl and constants
@cInclude("libutil.h"); // openpty()
}),
else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h");