From d75e869b4eca91fce08bb60d0d1e3f3a16547f7e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 2 Nov 2022 16:12:50 -0700 Subject: [PATCH] Load `$XDG_CONFIG_HOME/ghostty/config` if it exists (#25) Ghostty now loads the config file in `$XDG_CONFIG_HOME/ghostty/config` if it exists on startup. This follows the XDG base dir specification so if $XDG_CONFIG_HOME is not set, we default to `$HOME/.config/ghostty/config`. --- src/cli_args.zig | 24 ------------- src/homedir.zig | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 51 +++++++++++++++++++++------ src/xdg.zig | 66 +++++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+), 35 deletions(-) create mode 100644 src/homedir.zig create mode 100644 src/xdg.zig diff --git a/src/cli_args.zig b/src/cli_args.zig index b6263f048..502cfcf3f 100644 --- a/src/cli_args.zig +++ b/src/cli_args.zig @@ -60,8 +60,6 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void { try parseIntoField(T, arena_alloc, dst, key, value); } } - - if (@hasDecl(T, "finalize")) try dst.finalize(); } /// Parse a single key/value pair into the destination type T. @@ -195,28 +193,6 @@ test "parse: simple" { try testing.expect(!data.@"b-f"); } -test "parse: finalize" { - const testing = std.testing; - - var data: struct { - a: []const u8 = "", - _arena: ?ArenaAllocator = null, - - pub fn finalize(self: *@This()) !void { - self.a = "YO"; - } - } = .{}; - defer if (data._arena) |arena| arena.deinit(); - - var iter = try std.process.ArgIteratorGeneral(.{}).init( - testing.allocator, - "--a=42", - ); - defer iter.deinit(); - try parse(@TypeOf(data), testing.allocator, &data, &iter); - try testing.expectEqualStrings("YO", data.a); -} - test "parseIntoField: string" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); diff --git a/src/homedir.zig b/src/homedir.zig new file mode 100644 index 000000000..c16ff9489 --- /dev/null +++ b/src/homedir.zig @@ -0,0 +1,91 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const passwd = @import("passwd.zig"); + +const Error = error{ + /// The buffer used for output is not large enough to store the value. + BufferTooSmall, +}; + +/// Determine the home directory for the currently executing user. This +/// is generally an expensive process so the value should be cached. +pub inline fn home(buf: []u8) !?[]u8 { + return switch (builtin.os.tag) { + inline .linux, .macos => try homeUnix(buf), + else => @compileError("unimplemented"), + }; +} + +fn homeUnix(buf: []u8) !?[]u8 { + // First: if we have a HOME env var, then we use that. + if (std.os.getenv("HOME")) |result| { + if (buf.len < result.len) return Error.BufferTooSmall; + std.mem.copy(u8, buf, result); + return buf[0..result.len]; + } + + // Everything below here will require some allocation + var tempBuf: [1024]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&tempBuf); + + // If we're on darwin, we try the directory service. I'm not sure if there + // is a Mac API to do this but if so we can link to that... + if (builtin.os.tag == .macos) { + const exec = try std.ChildProcess.exec(.{ + .allocator = fba.allocator(), + .argv = &[_][]const u8{ + "/bin/sh", + "-c", + "dscl -q . -read /Users/\"$(whoami)\" NFSHomeDirectory | sed 's/^[^ ]*: //'", + }, + .max_output_bytes = fba.buffer.len / 2, + }); + + if (exec.term == .Exited and exec.term.Exited == 0) { + const result = trimSpace(exec.stdout); + if (buf.len < result.len) return Error.BufferTooSmall; + std.mem.copy(u8, buf, result); + return buf[0..result.len]; + } + } + + // We try passwd. This doesn't work on multi-user mac but we try it anyways. + fba.reset(); + const pw = try passwd.get(fba.allocator()); + if (pw.home) |result| { + if (buf.len < result.len) return Error.BufferTooSmall; + std.mem.copy(u8, buf, result); + return buf[0..result.len]; + } + + // If all else fails, have the shell tell us... + fba.reset(); + const exec = try std.ChildProcess.exec(.{ + .allocator = fba.allocator(), + .argv = &[_][]const u8{ "/bin/sh", "-c", "cd && pwd" }, + .max_output_bytes = fba.buffer.len / 2, + }); + + if (exec.term == .Exited and exec.term.Exited == 0) { + const result = trimSpace(exec.stdout); + if (buf.len < result.len) return Error.BufferTooSmall; + std.mem.copy(u8, buf, result); + return buf[0..result.len]; + } + + return null; +} + +fn trimSpace(input: []const u8) []const u8 { + return std.mem.trim(u8, input, " \n\t"); +} + +test { + const testing = std.testing; + + var buf: [1024]u8 = undefined; + const result = try home(&buf); + try testing.expect(result != null); + try testing.expect(result.?.len > 0); +} diff --git a/src/main.zig b/src/main.zig index 9dd388440..cd92fd761 100644 --- a/src/main.zig +++ b/src/main.zig @@ -8,6 +8,7 @@ const harfbuzz = @import("harfbuzz"); const macos = @import("macos"); const tracy = @import("tracy"); const renderer = @import("renderer.zig"); +const xdg = @import("xdg.zig"); const App = @import("App.zig"); const cli_args = @import("cli_args.zig"); @@ -58,23 +59,48 @@ pub fn main() !void { break :alloc tracy_alloc.allocator(); }; - // Parse the config from the CLI args - var config = config: { - var result = try Config.default(alloc); - errdefer result.deinit(); - var iter = try std.process.argsWithAllocator(alloc); - defer iter.deinit(); - try cli_args.parse(Config, alloc, &result, &iter); - break :config result; - }; + // Try reading our config + var config = try Config.default(alloc); defer config.deinit(); - // Parse the config files + // If we have a configuration file in our home directory, parse that first. + const cwd = std.fs.cwd(); + { + const home_config_path = try xdg.config(alloc, .{ .subdir = "ghostty/config" }); + defer alloc.free(home_config_path); + + if (cwd.openFile(home_config_path, .{})) |file| { + defer file.close(); + + var buf_reader = std.io.bufferedReader(file.reader()); + var iter = cli_args.lineIterator(buf_reader.reader()); + try cli_args.parse(Config, alloc, &config, &iter); + } else |err| switch (err) { + error.FileNotFound => std.log.info( + "homedir config not found, not loading path={s}", + .{home_config_path}, + ), + + else => std.log.warn( + "error reading homedir config file, not loading err={} path={s}", + .{ err, home_config_path }, + ), + } + } + + // Parse the config from the CLI args + { + var iter = try std.process.argsWithAllocator(alloc); + defer iter.deinit(); + try cli_args.parse(Config, alloc, &config, &iter); + } + + // Parse the config files that were added from our file and CLI args. + // TODO(mitchellh): we should parse the files form the homedir first // TODO(mitchellh): support nesting (config-file in a config file) // TODO(mitchellh): detect cycles when nesting if (config.@"config-file".list.items.len > 0) { const len = config.@"config-file".list.items.len; - const cwd = std.fs.cwd(); for (config.@"config-file".list.items) |path| { var file = try cwd.openFile(path, .{}); defer file.close(); @@ -91,6 +117,7 @@ pub fn main() !void { return error.ConfigFileInConfigFile; } } + try config.finalize(); std.log.info("config={}", .{config}); // We want to log all our errors @@ -177,7 +204,9 @@ test { // TODO _ = @import("config.zig"); + _ = @import("homedir.zig"); _ = @import("passwd.zig"); + _ = @import("xdg.zig"); _ = @import("cli_args.zig"); _ = @import("lru.zig"); } diff --git a/src/xdg.zig b/src/xdg.zig new file mode 100644 index 000000000..cab8aab3d --- /dev/null +++ b/src/xdg.zig @@ -0,0 +1,66 @@ +//! Implementation of the XDG Base Directory specification +//! (https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const homedir = @import("homedir.zig"); + +pub const Options = struct { + /// Subdirectories to join to the base. This avoids extra allocations + /// when building up the directory. This is commonly the application. + subdir: ?[]const u8 = null, + + /// The home directory for the user. If this is not set, we will attempt + /// to look it up which is an expensive process. By setting this, you can + /// avoid lookups. + home: ?[]const u8 = null, +}; + +/// Get the XDG user config directory. The returned value is allocated. +pub fn config(alloc: Allocator, opts: Options) ![]u8 { + if (std.os.getenv("XDG_CONFIG_HOME")) |env| { + // If we have a subdir, then we use the env as-is to avoid a copy. + if (opts.subdir) |subdir| { + return try std.fs.path.join(alloc, &[_][]const u8{ + env, + subdir, + }); + } + + return try alloc.dupe(u8, env); + } + + // If we have a cached home dir, use that. + if (opts.home) |home| { + return try std.fs.path.join(alloc, &[_][]const u8{ + home, + ".config", + opts.subdir orelse "", + }); + } + + // Get our home dir + var buf: [1024]u8 = undefined; + if (try homedir.home(&buf)) |home| { + return try std.fs.path.join(alloc, &[_][]const u8{ + home, + ".config", + opts.subdir orelse "", + }); + } + + return error.NoHomeDir; +} + +test { + const testing = std.testing; + const alloc = testing.allocator; + + { + const value = try config(alloc, .{}); + defer alloc.free(value); + try testing.expect(value.len > 0); + } +}