From 3113f9d8af010590c2490203a106d4e0dfe44b05 Mon Sep 17 00:00:00 2001 From: Paul Jimenez Date: Wed, 8 Nov 2023 02:19:12 -0500 Subject: [PATCH 1/4] config: make config-file names resolve relative to the config dir --- src/config/Config.zig | 58 ++++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 26425fb12..22137bc1a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1069,31 +1069,54 @@ fn ctrlOrSuper(mods: inputpkg.Mods) inputpkg.Mods { return copy; } -/// Load the configuration from the default file locations. Currently, -/// this loads from $XDG_CONFIG_HOME/ghostty/config. -pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { - const home_config_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" }); +/// Get the configuration file default location as a std.fs.Dir +/// Currently, this loads from $XDG_CONFIG_HOME/ghostty. +fn getConfigDir(alloc: Allocator) std.fs.Dir { + const home_config_path = internal_os.xdg.config(alloc, .{ .subdir = "ghostty" }) catch ""; defer alloc.free(home_config_path); const cwd = std.fs.cwd(); - if (cwd.openFile(home_config_path, .{})) |file| { - defer file.close(); - std.log.info("reading configuration file path={s}", .{home_config_path}); - - var buf_reader = std.io.bufferedReader(file.reader()); - var iter = cli.args.lineIterator(buf_reader.reader()); - try cli.args.parse(Config, alloc, self, &iter); + if (cwd.openDir(home_config_path, .{})) |dir| { + std.log.info("using configuration file path={s}", .{home_config_path}); + return dir; } else |err| switch (err) { error.FileNotFound => std.log.info( - "homedir config not found, not loading path={s}", + "homedir not found, not loading from path={s}", .{home_config_path}, ), else => std.log.warn( - "error reading homedir config file, not loading err={} path={s}", + "error reading homedir, not loading err={!} path={s}", .{ err, home_config_path }, ), } + std.log.warn("configuration file path not found. defaulting to current working dir", .{}); + return cwd; +} + +/// Load the configuration from the default configuration file. +pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { + const cfgdir = getConfigDir(alloc); + if (cfgdir.openFile("config", .{})) |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, self, &iter); + } else |err| { + const cfgpath = cfgdir.realpathAlloc(alloc, "config") catch "config"; + switch (err) { + error.FileNotFound => std.log.info( + "homedir config not found, not loading path={s}", + .{cfgpath}, + ), + + else => std.log.warn( + "error reading config file, not loading err={} path={s}", + .{ err, cfgpath }, + ), + } + } } /// Load and parse the CLI args. @@ -1117,17 +1140,18 @@ pub fn loadRecursiveFiles(self: *Config, alloc: Allocator) !void { // TODO(mitchellh): detect cycles when nesting if (self.@"config-file".list.items.len == 0) return; - const arena_alloc = self._arena.?.allocator(); - const cwd = std.fs.cwd(); + + const cfgdir = getConfigDir(alloc); const len = self.@"config-file".list.items.len; for (self.@"config-file".list.items) |path| { - var file = cwd.openFile(path, .{}) catch |err| { + var file = cfgdir.openFile(path, .{}) catch |err| { + const cfgpath = cfgdir.realpathAlloc(arena_alloc, path) catch path; try self._errors.add(arena_alloc, .{ .message = try std.fmt.allocPrintZ( arena_alloc, "error opening config-file {s}: {}", - .{ path, err }, + .{ cfgpath, err }, ), }); continue; From b8bfb66ad84c25789e1dd43f1ef765d82a887924 Mon Sep 17 00:00:00 2001 From: Paul Jimenez Date: Wed, 8 Nov 2023 09:57:34 -0500 Subject: [PATCH 2/4] config: support nested/recursive config-file keys --- src/config/Config.zig | 61 ++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 22137bc1a..bb1f32703 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1136,44 +1136,45 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { /// Load and parse the config files that were added in the "config-file" key. pub fn loadRecursiveFiles(self: *Config, alloc: Allocator) !void { - // TODO(mitchellh): support nesting (config-file in a config file) - // TODO(mitchellh): detect cycles when nesting - if (self.@"config-file".list.items.len == 0) return; const arena_alloc = self._arena.?.allocator(); + var loaded = std.StringHashMap(void).init(alloc); + defer loaded.deinit(); + const cfgdir = getConfigDir(alloc); - const len = self.@"config-file".list.items.len; - for (self.@"config-file".list.items) |path| { - var file = cfgdir.openFile(path, .{}) catch |err| { + more_found: while (true) { + const len = self.@"config-file".list.items.len; + for (self.@"config-file".list.items) |path| { const cfgpath = cfgdir.realpathAlloc(arena_alloc, path) catch path; - try self._errors.add(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( - arena_alloc, - "error opening config-file {s}: {}", - .{ cfgpath, err }, - ), - }); - continue; - }; - 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, self, &iter); + if (loaded.contains(cfgpath)) continue; + try loaded.put(cfgpath, {}); - // We don't currently support adding more config files to load - // from within a loaded config file. This can be supported - // later. - if (self.@"config-file".list.items.len > len) { - try self._errors.add(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( - arena_alloc, - "config-file cannot be used in a config-file. Found in {s}", - .{path}, - ), - }); + var file = cfgdir.openFile(path, .{}) catch |err| { + try self._errors.add(arena_alloc, .{ + .message = try std.fmt.allocPrintZ( + arena_alloc, + "error opening config-file {s}: {}", + .{ cfgpath, err }, + ), + }); + continue; + }; + defer file.close(); + + log.info("loading config-file path={s}", .{cfgpath}); + var buf_reader = std.io.bufferedReader(file.reader()); + var iter = cli.args.lineIterator(buf_reader.reader()); + try cli.args.parse(Config, alloc, self, &iter); + + // Added more config files, so the list's backing array might have + // been resized, so start the iteration over + if (self.@"config-file".list.items.len > len) { + continue :more_found; + } } + break; } } From 85fea9d5ee4ceaec6132a303be53c9ca5688e3ea Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 9 Nov 2023 15:53:15 -0800 Subject: [PATCH 3/4] config: resolve file paths relative to their loaded file --- src/config/Config.zig | 162 ++++++++++++++++++++++++------------------ 1 file changed, 92 insertions(+), 70 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index bb1f32703..45ccbe7a8 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1069,53 +1069,60 @@ fn ctrlOrSuper(mods: inputpkg.Mods) inputpkg.Mods { return copy; } -/// Get the configuration file default location as a std.fs.Dir -/// Currently, this loads from $XDG_CONFIG_HOME/ghostty. -fn getConfigDir(alloc: Allocator) std.fs.Dir { - const home_config_path = internal_os.xdg.config(alloc, .{ .subdir = "ghostty" }) catch ""; - defer alloc.free(home_config_path); - - const cwd = std.fs.cwd(); - if (cwd.openDir(home_config_path, .{})) |dir| { - std.log.info("using configuration file path={s}", .{home_config_path}); - return dir; - } else |err| switch (err) { - error.FileNotFound => std.log.info( - "homedir not found, not loading from path={s}", - .{home_config_path}, - ), - - else => std.log.warn( - "error reading homedir, not loading err={!} path={s}", - .{ err, home_config_path }, - ), - } - std.log.warn("configuration file path not found. defaulting to current working dir", .{}); - return cwd; -} - /// Load the configuration from the default configuration file. pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { - const cfgdir = getConfigDir(alloc); - if (cfgdir.openFile("config", .{})) |file| { + 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 cli.args.parse(Config, alloc, self, &iter); - } else |err| { - const cfgpath = cfgdir.realpathAlloc(alloc, "config") catch "config"; - switch (err) { - error.FileNotFound => std.log.info( - "homedir config not found, not loading path={s}", - .{cfgpath}, - ), + try self.expandConfigFiles(std.fs.path.dirname(config_path).?); + } else |err| switch (err) { + error.FileNotFound => std.log.info( + "homedir config not found, not loading path={s}", + .{config_path}, + ), - else => std.log.warn( - "error reading config file, not loading err={} path={s}", - .{ err, cfgpath }, - ), - } + else => std.log.warn( + "error reading config file, not loading err={} path={s}", + .{ err, config_path }, + ), + } +} + +/// Expand the relative paths in config-files to be absolute paths +/// relative to the base directory. +fn expandConfigFiles(self: *Config, base: []const u8) !void { + assert(std.fs.path.isAbsolute(base)); + var dir = try std.fs.cwd().openDir(base, .{}); + defer dir.close(); + + const arena_alloc = self._arena.?.allocator(); + for (self.@"config-file".list.items, 0..) |path, i| { + // If it is already absolute we can ignore it. + if (path.len == 0 or std.fs.path.isAbsolute(path)) continue; + + // If it isn't absolute, we need to make it absolute relative to the base. + const abs = dir.realpathAlloc(arena_alloc, path) catch |err| { + try self._errors.add(arena_alloc, .{ + .message = try std.fmt.allocPrintZ( + arena_alloc, + "error resolving config-file {s}: {}", + .{ path, err }, + ), + }); + self.@"config-file".list.items[i] = ""; + continue; + }; + + log.debug("expanding config-file path relative={s} abs={s}", .{ path, abs }); + self.@"config-file".list.items[i] = abs; } } @@ -1132,49 +1139,64 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { var iter = try std.process.argsWithAllocator(alloc_gpa); defer iter.deinit(); try cli.args.parse(Config, alloc_gpa, self, &iter); + + // Config files loaded from the CLI args are relative to pwd + if (self.@"config-file".list.items.len > 0) { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + try self.expandConfigFiles(try std.fs.cwd().realpath(".", &buf)); + } } /// Load and parse the config files that were added in the "config-file" key. -pub fn loadRecursiveFiles(self: *Config, alloc: Allocator) !void { +pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { if (self.@"config-file".list.items.len == 0) return; const arena_alloc = self._arena.?.allocator(); - var loaded = std.StringHashMap(void).init(alloc); + // Keeps track of loaded files to prevent cycles. + var loaded = std.StringHashMap(void).init(alloc_gpa); defer loaded.deinit(); - const cfgdir = getConfigDir(alloc); - more_found: while (true) { - const len = self.@"config-file".list.items.len; - for (self.@"config-file".list.items) |path| { - const cfgpath = cfgdir.realpathAlloc(arena_alloc, path) catch path; + const cwd = std.fs.cwd(); + var i: usize = 0; + while (i < self.@"config-file".list.items.len) : (i += 1) { + const path = self.@"config-file".list.items[i]; - if (loaded.contains(cfgpath)) continue; - try loaded.put(cfgpath, {}); + // Error paths + if (path.len == 0) continue; - var file = cfgdir.openFile(path, .{}) catch |err| { - try self._errors.add(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( - arena_alloc, - "error opening config-file {s}: {}", - .{ cfgpath, err }, - ), - }); - continue; - }; - defer file.close(); + // All paths should already be absolute at this point because + // they're fixed up after each load. + assert(std.fs.path.isAbsolute(path)); - log.info("loading config-file path={s}", .{cfgpath}); - var buf_reader = std.io.bufferedReader(file.reader()); - var iter = cli.args.lineIterator(buf_reader.reader()); - try cli.args.parse(Config, alloc, self, &iter); - - // Added more config files, so the list's backing array might have - // been resized, so start the iteration over - if (self.@"config-file".list.items.len > len) { - continue :more_found; - } + // We must only load a unique file once + if (try loaded.fetchPut(path, {}) != null) { + try self._errors.add(arena_alloc, .{ + .message = try std.fmt.allocPrintZ( + arena_alloc, + "config-file {s}: cycle detected", + .{path}, + ), + }); + continue; } - break; + + var file = cwd.openFile(path, .{}) catch |err| { + try self._errors.add(arena_alloc, .{ + .message = try std.fmt.allocPrintZ( + arena_alloc, + "error opening config-file {s}: {}", + .{ path, err }, + ), + }); + continue; + }; + defer file.close(); + + log.info("loading config-file path={s}", .{path}); + var buf_reader = std.io.bufferedReader(file.reader()); + var iter = cli.args.lineIterator(buf_reader.reader()); + try cli.args.parse(Config, alloc_gpa, self, &iter); + try self.expandConfigFiles(std.fs.path.dirname(path).?); } } From a9761728e9cbd37fe56b5ac6b92cb0fdb4e7c6ac Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 9 Nov 2023 16:52:57 -0800 Subject: [PATCH 4/4] config: comments --- src/config/Config.zig | 72 ++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 45ccbe7a8..3e3d247fd 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -503,7 +503,14 @@ keybind: Keybinds = .{}, /// is determined by the OS settings. On every other platform it is 500ms. @"click-repeat-interval": u32 = 0, -/// Additional configuration files to read. +/// Additional configuration files to read. This configuration can be repeated +/// to read multiple configuration files. Configuration files themselves can +/// load more configuration files. Paths are relative to the file containing +/// the `config-file` directive. For command-line arguments, paths are +/// relative to the current working directory. +/// +/// Cycles are not allowed. If a cycle is detected, an error will be logged +/// and the configuration file will be ignored. @"config-file": RepeatableString = .{}, /// Confirms that a surface should be closed before closing it. This defaults @@ -1069,7 +1076,8 @@ fn ctrlOrSuper(mods: inputpkg.Mods) inputpkg.Mods { return copy; } -/// Load the configuration from the default configuration file. +/// 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); @@ -1096,36 +1104,6 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { } } -/// Expand the relative paths in config-files to be absolute paths -/// relative to the base directory. -fn expandConfigFiles(self: *Config, base: []const u8) !void { - assert(std.fs.path.isAbsolute(base)); - var dir = try std.fs.cwd().openDir(base, .{}); - defer dir.close(); - - const arena_alloc = self._arena.?.allocator(); - for (self.@"config-file".list.items, 0..) |path, i| { - // If it is already absolute we can ignore it. - if (path.len == 0 or std.fs.path.isAbsolute(path)) continue; - - // If it isn't absolute, we need to make it absolute relative to the base. - const abs = dir.realpathAlloc(arena_alloc, path) catch |err| { - try self._errors.add(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( - arena_alloc, - "error resolving config-file {s}: {}", - .{ path, err }, - ), - }); - self.@"config-file".list.items[i] = ""; - continue; - }; - - log.debug("expanding config-file path relative={s} abs={s}", .{ path, abs }); - self.@"config-file".list.items[i] = abs; - } -} - /// Load and parse the CLI args. pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { switch (builtin.os.tag) { @@ -1200,6 +1178,36 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { } } +/// Expand the relative paths in config-files to be absolute paths +/// relative to the base directory. +fn expandConfigFiles(self: *Config, base: []const u8) !void { + assert(std.fs.path.isAbsolute(base)); + var dir = try std.fs.cwd().openDir(base, .{}); + defer dir.close(); + + const arena_alloc = self._arena.?.allocator(); + for (self.@"config-file".list.items, 0..) |path, i| { + // If it is already absolute we can ignore it. + if (path.len == 0 or std.fs.path.isAbsolute(path)) continue; + + // If it isn't absolute, we need to make it absolute relative to the base. + const abs = dir.realpathAlloc(arena_alloc, path) catch |err| { + try self._errors.add(arena_alloc, .{ + .message = try std.fmt.allocPrintZ( + arena_alloc, + "error resolving config-file {s}: {}", + .{ path, err }, + ), + }); + self.@"config-file".list.items[i] = ""; + continue; + }; + + log.debug("expanding config-file path relative={s} abs={s}", .{ path, abs }); + self.@"config-file".list.items[i] = abs; + } +} + pub fn finalize(self: *Config) !void { // If we have a font-family set and don't set the others, default // the others to the font family. This way, if someone does