From f748b17e27f5ee089494044179dea1c493ce63cc Mon Sep 17 00:00:00 2001 From: phl Date: Sat, 15 Aug 2026 15:54:19 +0200 Subject: [PATCH 1/4] feat: expand tildes in config theme path to HOME When loading a theme from a path that includes a tilde: ``` theme="~/.cache/wal/colors-ghostty" ``` currently fails with the following error: ``` cannot include path separators unless it is an absolute path ``` This PR tries to expand the ~ of the path. If there is no ~ or expansion fails, it falls back to the unexpanded value. --- src/config/theme.zig | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/config/theme.zig b/src/config/theme.zig index a8c39cad9..0a3a8ecee 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -5,6 +5,8 @@ const internal_os = @import("../os/main.zig"); const cli = @import("../cli.zig"); const global = @import("../global.zig"); +const log = std.log.scoped(.config); + /// Location of possible themes. The order of this enum matters because it /// defines the priority of theme search (from top to bottom). pub const Location = enum { @@ -113,12 +115,40 @@ pub const LocationIterator = struct { /// will be added to the list and null will be returned. pub fn open( arena_alloc: Allocator, - theme: []const u8, + theme_: []const u8, diags: *cli.DiagnosticList, ) error{ OutOfMemory, Unexpected }!?struct { path: []const u8, file: std.Io.File, } { + var buf: [std.fs.max_path_bytes]u8 = undefined; + const theme = expanded: { + if (!std.mem.startsWith(u8, theme_, "~/")) + break :expanded theme_; + + var environ_map = global.environMap() catch |err| { + log.warn( + "error getting environment map when expanding theme path={s}: {}", + .{ theme_, err }, + ); + break :expanded theme_; + }; + defer environ_map.deinit(); + + break :expanded internal_os.expandHome( + global.io(), + &environ_map, + theme_, + &buf, + ) catch |err| { + log.warn( + "error expanding home directory for theme path={s}: {}", + .{ theme_, err }, + ); + break :expanded theme_; + }; + }; + // Absolute themes are loaded a different path. if (std.fs.path.isAbsolute(theme)) { const file: std.Io.File = try openAbsolute( From c1217342958b90ed3a25413c1616dfd2dd8cd1bf Mon Sep 17 00:00:00 2001 From: phl Date: Sat, 15 Aug 2026 20:04:12 +0200 Subject: [PATCH 2/4] move shell expand of theme from theme.zig to config.zig --- src/config/Config.zig | 52 ++++++++++++++++++++++++++++++++++++++++++- src/config/theme.zig | 32 +------------------------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 814532b5d..bb4e7a38d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -9973,9 +9973,32 @@ pub const Theme = struct { // Trim our value const trimmed = std.mem.trim(u8, input, cli.args.whitespace); + // Expand our value + var buf: [std.fs.max_path_bytes]u8 = undefined; + const expanded = expanded: { + if (!std.mem.startsWith(u8, trimmed, "~/")) + break :expanded trimmed; + + var environ_map = global.environMap() catch |err| { + log.warn("error getting environment map for theme path {s}: {}", .{ trimmed, err }); + break :expanded trimmed; + }; + defer environ_map.deinit(); + + break :expanded internal_os.expandHome( + global.io(), + &environ_map, + trimmed, + &buf, + ) catch |err| { + log.warn("error expanding home directory in theme path {s}: {}", .{ trimmed, err }); + break :expanded trimmed; + }; + }; + // Set the value to the specified value directly. self.* = .{ - .light = try alloc.dupeZ(u8, trimmed), + .light = try alloc.dupeZ(u8, expanded), .dark = self.light, }; } @@ -10028,6 +10051,33 @@ pub const Theme = struct { try testing.expectEqualStrings("foo", v.dark); } + // Expand home + { + var environ_map = try testing.environ.createMap(alloc); + defer environ_map.deinit(); + + var home_buf: [std.fs.max_path_bytes]u8 = undefined; + const home = try internal_os.expandHome( + testing.io, + &environ_map, + "~/theme", + &home_buf, + ); + + var v: Theme = undefined; + try v.parseCLI(alloc, "~/theme/foo"); + + var expected_buf: [std.fs.max_path_bytes]u8 = undefined; + const expected = try std.fmt.bufPrint( + &expected_buf, + "{s}/theme/foo", + .{home}, + ); + + try testing.expectEqualStrings(expected, v.light); + try testing.expectEqualStrings(expected, v.dark); + } + // Light/dark { var v: Theme = undefined; diff --git a/src/config/theme.zig b/src/config/theme.zig index 0a3a8ecee..a8c39cad9 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -5,8 +5,6 @@ const internal_os = @import("../os/main.zig"); const cli = @import("../cli.zig"); const global = @import("../global.zig"); -const log = std.log.scoped(.config); - /// Location of possible themes. The order of this enum matters because it /// defines the priority of theme search (from top to bottom). pub const Location = enum { @@ -115,40 +113,12 @@ pub const LocationIterator = struct { /// will be added to the list and null will be returned. pub fn open( arena_alloc: Allocator, - theme_: []const u8, + theme: []const u8, diags: *cli.DiagnosticList, ) error{ OutOfMemory, Unexpected }!?struct { path: []const u8, file: std.Io.File, } { - var buf: [std.fs.max_path_bytes]u8 = undefined; - const theme = expanded: { - if (!std.mem.startsWith(u8, theme_, "~/")) - break :expanded theme_; - - var environ_map = global.environMap() catch |err| { - log.warn( - "error getting environment map when expanding theme path={s}: {}", - .{ theme_, err }, - ); - break :expanded theme_; - }; - defer environ_map.deinit(); - - break :expanded internal_os.expandHome( - global.io(), - &environ_map, - theme_, - &buf, - ) catch |err| { - log.warn( - "error expanding home directory for theme path={s}: {}", - .{ theme_, err }, - ); - break :expanded theme_; - }; - }; - // Absolute themes are loaded a different path. if (std.fs.path.isAbsolute(theme)) { const file: std.Io.File = try openAbsolute( From 37174c73b0600cc3b2d8579fa7a210086ee3bbed Mon Sep 17 00:00:00 2001 From: phl Date: Sun, 16 Aug 2026 09:50:35 +0200 Subject: [PATCH 3/4] add helper and expand also if light/dark is specified --- src/config/Config.zig | 121 ++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 59 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index bb4e7a38d..0e1f44b30 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4518,6 +4518,29 @@ fn expandPaths(self: *Config, base: []const u8) !void { } } +/// Expand tilde paths to absolute paths to the user's home directory. +/// If expansion fails, an error is logged and the original path is returned. +fn expandHome(path: []const u8, buf: []u8) []const u8 { + if (!std.mem.startsWith(u8, path, "~/")) + return path; + + var environ_map = global.environMap() catch |err| { + log.warn("failed to get environment map for path \"{s}\": {}", .{ path, err }); + return path; + }; + defer environ_map.deinit(); + + return internal_os.expandHome( + global.io(), + &environ_map, + path, + buf, + ) catch |err| { + log.warn("failed to expand home directory in path \"{s}\": {}", .{ path, err }); + return path; + }; +} + fn loadTheme(self: *Config, theme: Theme) !void { // Load the correct theme depending on the conditional state. // Dark/light themes were programmed prior to conditional configuration @@ -4617,14 +4640,17 @@ fn loadTheme(self: *Config, theme: Theme) !void { /// Call this once after you are done setting configuration. This /// is idempotent but will waste memory if called multiple times. pub fn finalize(self: *Config) !void { + const alloc = self._arena.?.allocator(); + // We always load the theme first because it may set other fields // in our config. - if (self.theme) |theme| { + if (self.theme) |*theme| { + try theme.finalize(alloc); const different = !std.mem.eql(u8, theme.light, theme.dark); // Warning: loadTheme will deinit our existing config and replace // it so all memory from self prior to this point will be freed. - try self.loadTheme(theme); + try self.loadTheme(theme.*); // If we have different light vs dark mode themes, disable // window-theme = auto since that breaks it. @@ -4638,8 +4664,6 @@ pub fn finalize(self: *Config) !void { } } - const alloc = self._arena.?.allocator(); - // Used for a variety of defaults. See the function docs as well the // specific variable use sites for more details. const probable_cli = probableCliEnvironment(); @@ -5444,20 +5468,8 @@ pub const WorkingDirectory = union(enum) { else => return, }; - if (!std.mem.startsWith(u8, path, "~/")) return; - var buf: [std.fs.max_path_bytes]u8 = undefined; - const expanded = expanded: { - var environ_map = global.environMap() catch |err| break :expanded err; - defer environ_map.deinit(); - break :expanded internal_os.expandHome(global.io(), &environ_map, path, &buf); - } catch |err| { - log.warn( - "error expanding home directory for working-directory path={s}: {}", - .{ path, err }, - ); - return; - }; + const expanded = expandHome(path, &buf); if (std.mem.eql(u8, expanded, path)) return; self.* = .{ .path = try alloc.dupe(u8, expanded) }; @@ -9973,36 +9985,26 @@ pub const Theme = struct { // Trim our value const trimmed = std.mem.trim(u8, input, cli.args.whitespace); - // Expand our value - var buf: [std.fs.max_path_bytes]u8 = undefined; - const expanded = expanded: { - if (!std.mem.startsWith(u8, trimmed, "~/")) - break :expanded trimmed; - - var environ_map = global.environMap() catch |err| { - log.warn("error getting environment map for theme path {s}: {}", .{ trimmed, err }); - break :expanded trimmed; - }; - defer environ_map.deinit(); - - break :expanded internal_os.expandHome( - global.io(), - &environ_map, - trimmed, - &buf, - ) catch |err| { - log.warn("error expanding home directory in theme path {s}: {}", .{ trimmed, err }); - break :expanded trimmed; - }; - }; - // Set the value to the specified value directly. self.* = .{ - .light = try alloc.dupeZ(u8, expanded), + .light = try alloc.dupeZ(u8, trimmed), .dark = self.light, }; } + /// Expand tilde paths in light/dark theme values. + pub fn finalize(self: *Theme, alloc: Allocator) Allocator.Error!void { + var buf: [std.fs.max_path_bytes]u8 = undefined; + + const light = expandHome(self.light, &buf); + if (!std.mem.eql(u8, light, self.light)) + self.light = try alloc.dupeZ(u8, light); + + const dark = expandHome(self.dark, &buf); + if (!std.mem.eql(u8, dark, self.dark)) + self.dark = try alloc.dupeZ(u8, dark); + } + /// Deep copy of the struct. Required by Config. pub fn clone(self: *const Theme, alloc: Allocator) Allocator.Error!Theme { return .{ @@ -10051,7 +10053,15 @@ pub const Theme = struct { try testing.expectEqualStrings("foo", v.dark); } - // Expand home + // Light/dark + { + var v: Theme = undefined; + try v.parseCLI(alloc, " light:foo, dark : bar "); + try testing.expectEqualStrings("foo", v.light); + try testing.expectEqualStrings("bar", v.dark); + } + + // Expand tilde to home { var environ_map = try testing.environ.createMap(alloc); defer environ_map.deinit(); @@ -10060,30 +10070,23 @@ pub const Theme = struct { const home = try internal_os.expandHome( testing.io, &environ_map, - "~/theme", + "~/", &home_buf, ); var v: Theme = undefined; - try v.parseCLI(alloc, "~/theme/foo"); + try v.parseCLI(alloc, "light:~/foo, dark:~/bar"); + try v.finalize(alloc); var expected_buf: [std.fs.max_path_bytes]u8 = undefined; - const expected = try std.fmt.bufPrint( - &expected_buf, - "{s}/theme/foo", - .{home}, + try testing.expectEqualStrings( + try std.fmt.bufPrint(&expected_buf, "{s}foo", .{home}), + v.light, + ); + try testing.expectEqualStrings( + try std.fmt.bufPrint(&expected_buf, "{s}bar", .{home}), + v.dark, ); - - try testing.expectEqualStrings(expected, v.light); - try testing.expectEqualStrings(expected, v.dark); - } - - // Light/dark - { - var v: Theme = undefined; - try v.parseCLI(alloc, " light:foo, dark : bar "); - try testing.expectEqualStrings("foo", v.light); - try testing.expectEqualStrings("bar", v.dark); } var v: Theme = undefined; From 68a2008b340959b2a94f27f37ca98257109f6396 Mon Sep 17 00:00:00 2001 From: phl Date: Sun, 16 Aug 2026 20:16:31 +0200 Subject: [PATCH 4/4] fix: format --- src/config/Config.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 0e1f44b30..ac854616c 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -9995,11 +9995,11 @@ pub const Theme = struct { /// Expand tilde paths in light/dark theme values. pub fn finalize(self: *Theme, alloc: Allocator) Allocator.Error!void { var buf: [std.fs.max_path_bytes]u8 = undefined; - + const light = expandHome(self.light, &buf); if (!std.mem.eql(u8, light, self.light)) self.light = try alloc.dupeZ(u8, light); - + const dark = expandHome(self.dark, &buf); if (!std.mem.eql(u8, dark, self.dark)) self.dark = try alloc.dupeZ(u8, dark);