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`.
This commit is contained in:
Mitchell Hashimoto
2022-11-02 16:12:50 -07:00
committed by GitHub
parent 116a157e17
commit d75e869b4e
4 changed files with 197 additions and 35 deletions

View File

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

91
src/homedir.zig Normal file
View File

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

View File

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

66
src/xdg.zig Normal file
View File

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