diff --git a/src/cli/action.zig b/src/cli/action.zig index a7cbabf25..68bcf6448 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -9,6 +9,7 @@ const list_keybinds = @import("list_keybinds.zig"); const list_themes = @import("list_themes.zig"); const list_colors = @import("list_colors.zig"); const show_config = @import("show_config.zig"); +const validate_config = @import("validate_config.zig"); /// Special commands that can be invoked via CLI flags. These are all /// invoked by using `+` as a CLI flag. The only exception is @@ -35,6 +36,9 @@ pub const Action = enum { /// Dump the config to stdout @"show-config", + // Validate passed config file + @"validate-config", + pub const Error = error{ /// Multiple actions were detected. You can specify at most one /// action on the CLI otherwise the behavior desired is ambiguous. @@ -124,6 +128,7 @@ pub const Action = enum { .@"list-themes" => try list_themes.run(alloc), .@"list-colors" => try list_colors.run(alloc), .@"show-config" => try show_config.run(alloc), + .@"validate-config" => try validate_config.run(alloc), }; } diff --git a/src/cli/validate_config.zig b/src/cli/validate_config.zig new file mode 100644 index 000000000..d6fedc544 --- /dev/null +++ b/src/cli/validate_config.zig @@ -0,0 +1,67 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const args = @import("args.zig"); +const Action = @import("action.zig").Action; +const Config = @import("../config.zig").Config; +const cli = @import("../cli.zig"); + +pub const Options = struct { + /// The path of the config file to validate. If this isn't specified, + /// then the default config file paths will be validated. + @"config-file": ?[:0]const u8 = null, + + pub fn deinit(self: Options) void { + _ = self; + } + + /// Enables "-h" and "--help" to work. + pub fn help(self: Options) !void { + _ = self; + return Action.help_error; + } +}; + +/// The `validate-config` command is used to validate a Ghostty config file. +/// +/// When executed without any arguments, this will load the config from the default location. +/// +/// The `--config-file` argument can be passed to validate a specific target config +/// file in a non-default location. +pub fn run(alloc: std.mem.Allocator) !u8 { + var opts: Options = .{}; + defer opts.deinit(); + + { + var iter = try std.process.argsWithAllocator(alloc); + defer iter.deinit(); + try args.parse(Options, alloc, &opts, &iter); + } + + const stdout = std.io.getStdOut().writer(); + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + + // If a config path is passed, validate it, otherwise validate default configs + if (opts.@"config-file") |config_path| { + var buf: [std.fs.max_path_bytes]u8 = undefined; + const abs_path = try std.fs.cwd().realpath(config_path, &buf); + + try cfg.loadFile(alloc, abs_path); + try cfg.loadRecursiveFiles(alloc); + } else { + cfg = try Config.load(alloc); + } + + try cfg.finalize(); + + if (!cfg._errors.empty()) { + for (cfg._errors.list.items) |err| { + try stdout.print("{s}\n", .{err.message}); + } + + return 1; + } + + return 0; +} diff --git a/src/config/Config.zig b/src/config/Config.zig index e33c2d92f..a9bbc226b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1785,22 +1785,30 @@ pub fn loadIter( try cli.args.parse(Config, alloc, self, iter); } +/// Load configuration from the target config file at `path`. +/// +/// `path` must be resolved and absolute. +pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void { + assert(std.fs.path.isAbsolute(path)); + + var file = try std.fs.cwd().openFile(path, .{}); + defer file.close(); + + std.log.info("reading configuration file path={s}", .{path}); + + var buf_reader = std.io.bufferedReader(file.reader()); + var iter = cli.args.lineIterator(buf_reader.reader()); + try self.loadIter(alloc, &iter); + try self.expandPaths(std.fs.path.dirname(path).?); +} + /// Load the configuration from the default configuration file. The default /// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`. pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { const config_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" }); defer alloc.free(config_path); - const cwd = std.fs.cwd(); - if (cwd.openFile(config_path, .{})) |file| { - defer file.close(); - std.log.info("reading configuration file path={s}", .{config_path}); - - var buf_reader = std.io.bufferedReader(file.reader()); - var iter = cli.args.lineIterator(buf_reader.reader()); - try self.loadIter(alloc, &iter); - try self.expandPaths(std.fs.path.dirname(config_path).?); - } else |err| switch (err) { + self.loadFile(alloc, config_path) catch |err| switch (err) { error.FileNotFound => std.log.info( "homedir config not found, not loading path={s}", .{config_path}, @@ -1810,7 +1818,7 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { "error reading config file, not loading err={} path={s}", .{ err, config_path }, ), - } + }; } /// Load and parse the CLI args.