mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
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:

committed by
GitHub

parent
116a157e17
commit
d75e869b4e
@ -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
91
src/homedir.zig
Normal 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);
|
||||
}
|
51
src/main.zig
51
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");
|
||||
}
|
||||
|
66
src/xdg.zig
Normal file
66
src/xdg.zig
Normal 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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user