feat(nested config files)

Adds a config_files option to config.json that allows the user
to specify one or more config files to load in addition to the
main config file.

For me this allows me to keep my flow configuration in a shared dotfiles
repository managed by git.
This commit is contained in:
Jonathan Marler 2025-01-06 02:02:05 -07:00 committed by CJ van den Berg
parent a6b29e4fe9
commit 194fe70d6e
3 changed files with 67 additions and 3 deletions

View file

@ -22,3 +22,5 @@ top_bar: []const u8 = "",
bottom_bar: []const u8 = "mode file log selection diagnostics keybind linenumber clock spacer",
lsp_request_timeout: usize = 10,
config_files: []const u8 = "",

View file

@ -393,6 +393,9 @@ const config = @import("config");
pub fn read_config(allocator: std.mem.Allocator, buf: *?[]const u8) config {
const file_name = get_app_config_file_name(application_name) catch return .{};
return read_config_at(allocator, buf, file_name);
}
pub fn read_config_at(allocator: std.mem.Allocator, buf: *?[]const u8, file_name: []const u8) config {
return read_json_config_file(allocator, file_name, buf) catch |e| {
log.logger("config").print_err("read_config", "error reading config file: {any}", .{e});
return .{};

View file

@ -82,9 +82,21 @@ fn start(args: StartArgs) tp.result {
fn init(allocator: Allocator) !*Self {
var self = try allocator.create(Self);
var conf_buf: ?[]const u8 = null;
var conf = root.read_config(allocator, &conf_buf);
defer if (conf_buf) |buf| allocator.free(buf);
var conf_bufs: std.ArrayListUnmanaged([]const u8) = .{};
defer {
for (conf_bufs.items) |buf| {
allocator.free(buf);
}
conf_bufs.deinit(allocator);
}
var conf = blk: {
var maybe_buf: ?[]const u8 = null;
const conf = root.read_config(allocator, &maybe_buf);
if (maybe_buf) |buf| try conf_bufs.append(allocator, buf);
break :blk conf;
};
try loadConfigFiles(allocator, &conf_bufs, &conf, conf.config_files);
const theme = get_theme_by_name(conf.theme) orelse get_theme_by_name("dark_modern") orelse return tp.exit("unknown theme");
conf.theme = theme.name;
@ -211,6 +223,53 @@ fn deinit(self: *Self) void {
self.allocator.destroy(self);
}
fn loadConfigFiles(
allocator: std.mem.Allocator,
conf_bufs: *std.ArrayListUnmanaged([]const u8),
final_conf: *config,
config_files: []const u8,
) error{OutOfMemory}!void {
if (config_files.len == 0) return;
var it = std.mem.splitScalar(u8, config_files, std.fs.path.delimiter);
while (it.next()) |path| {
var maybe_buf: ?[]const u8 = null;
const conf = root.read_config_at(allocator, &maybe_buf, path);
if (maybe_buf) |buf| try conf_bufs.append(allocator, buf);
if (conf.config_files.len > 0) {
// just disallow nested config for now as we'd have to establish some
// sort of priority
std.log.err("{s}: ignoring nested 'config_files'", .{path});
}
inline for (std.meta.fields(config)) |field| {
if (comptime std.mem.eql(u8, field.name, "config_files")) continue;
const new_value_ref = &@field(conf, field.name);
const default_value_ref: *const field.type = @alignCast(@ptrCast(field.default_value));
const is_default = switch (field.type) {
bool, usize => new_value_ref.* == default_value_ref.*,
[]const u8 => std.mem.eql(u8, new_value_ref.*, default_value_ref.*),
else => @compileError("unsupported type: " ++ @typeName(field.type)),
};
const value_spec: []const u8 = switch (field.type) {
bool, usize => "",
[]const u8 => "s",
else => @compileError("unsupported type: " ++ @typeName(field.type)),
};
if (!is_default) {
std.log.info("{s} override {s} with {" ++ value_spec ++ "}", .{
path,
field.name,
new_value_ref.*,
});
@field(final_conf, field.name) = new_value_ref.*;
}
}
}
}
fn listen_sigwinch(self: *Self) tp.result {
if (self.sigwinch_signal) |old| old.deinit();
self.sigwinch_signal = tp.signal.init(std.posix.SIG.WINCH, tp.message.fmt(.{"sigwinch"})) catch |e| return tp.exit_error(e, @errorReturnTrace());