feat: expand tildes in config theme path to HOME (#13840)

When loading a theme from a path that includes a tilde:
```
theme="~/.cache/wal/colors-ghostty"
```
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.
This commit is contained in:
Jeffrey C. Ollie
2026-08-16 15:19:16 -05:00
committed by GitHub

View File

@@ -4517,6 +4517,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
@@ -4616,14 +4639,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.
@@ -4637,8 +4663,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();
@@ -5443,20 +5467,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) };
@@ -9979,6 +9991,19 @@ 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);
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Theme, alloc: Allocator) Allocator.Error!Theme {
return .{
@@ -10035,6 +10060,34 @@ pub const Theme = struct {
try testing.expectEqualStrings("bar", v.dark);
}
// Expand tilde to 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,
"~/",
&home_buf,
);
var v: Theme = undefined;
try v.parseCLI(alloc, "light:~/foo, dark:~/bar");
try v.finalize(alloc);
var expected_buf: [std.fs.max_path_bytes]u8 = undefined;
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,
);
}
var v: Theme = undefined;
try testing.expectError(error.ValueRequired, v.parseCLI(alloc, null));
try testing.expectError(error.ValueRequired, v.parseCLI(alloc, ""));