From 194fe70d6e8bf3d19de10ee99d47c60b9fc44bfb Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Mon, 6 Jan 2025 02:02:05 -0700 Subject: [PATCH] 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. --- src/config.zig | 2 ++ src/main.zig | 3 +++ src/tui/tui.zig | 65 ++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/config.zig b/src/config.zig index 4863397..4ea9038 100644 --- a/src/config.zig +++ b/src/config.zig @@ -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 = "", diff --git a/src/main.zig b/src/main.zig index 7e3aeb4..72b5164 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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 .{}; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 025c12c..d860797 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -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());