diff --git a/src/config/Config.zig b/src/config/Config.zig index 38880f76a..8901c1500 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -24,6 +24,7 @@ const cli = @import("../cli.zig"); const Command = @import("../Command.zig"); const formatterpkg = @import("formatter.zig"); +const themepkg = @import("theme.zig"); const url = @import("url.zig"); const Key = @import("key.zig").Key; const KeyValue = @import("key.zig").Value; @@ -2205,88 +2206,12 @@ pub fn themeDir(alloc: std.mem.Allocator, type_: ThemeDirType) ?[]const u8 { fn loadTheme(self: *Config, theme: []const u8) !void { const alloc = self._arena.?.allocator(); - const file = file: { - if (std.fs.path.isAbsolute(theme)) { - // Theme is an absolute path, open that file or fail - break :file std.fs.openFileAbsolute(theme, .{}) catch |err| switch (err) { - error.FileNotFound => { - try self._errors.add(alloc, .{ - .message = try std.fmt.allocPrintZ( - alloc, - "failed to load theme from the path \"{s}\"", - .{theme}, - ), - }); - return; - }, - else => { - try self._errors.add(alloc, .{ - .message = try std.fmt.allocPrintZ( - alloc, - "failed to load theme from the path \"{s}\": {}", - .{ theme, err }, - ), - }); - return; - }, - }; - } - - // The theme is not an absolute path, search the user and system theme - // directories - - const dirs: []const struct { - type: ThemeDirType, - dir: ?[]const u8, - } = &.{ - .{ .type = .user, .dir = themeDir(alloc, .user) }, - .{ .type = .system, .dir = themeDir(alloc, .system) }, - }; - - const cwd = std.fs.cwd(); - for (dirs) |dir| { - if (dir.dir) |d| { - const path = try std.fs.path.join(alloc, &.{ - d, - theme, - }); - if (cwd.openFile(path, .{})) |file| { - break :file file; - } else |err| switch (err) { - error.FileNotFound => {}, - else => { - try self._errors.add(alloc, .{ - .message = try std.fmt.allocPrintZ( - alloc, - "failed to load theme \"{s}\" from the file \"{s}\": {}", - .{ theme, path, err }, - ), - }); - }, - } - } - } - - // If we get here, no file was found with the theme. Log some errors - // and bail. - for (dirs) |dir| { - if (dir.dir) |d| { - try self._errors.add(alloc, .{ - .message = try std.fmt.allocPrintZ( - alloc, - "theme \"{s}\" not found, tried {s} path \"{s}\"", - .{ - theme, - @tagName(dir.type), - d, - }, - ), - }); - } - } - - return; - }; + // Find our theme file and open it. See the open function for details. + const file: std.fs.File = (try themepkg.open( + alloc, + theme, + &self._errors, + )) orelse return; defer file.close(); // From this point onwards, we load the theme and do a bit of a dance diff --git a/src/config/theme.zig b/src/config/theme.zig new file mode 100644 index 000000000..16aa244fe --- /dev/null +++ b/src/config/theme.zig @@ -0,0 +1,181 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const global_state = &@import("../main.zig").state; +const internal_os = @import("../os/main.zig"); +const ErrorList = @import("ErrorList.zig"); + +/// 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 { + user, // xdg config dir + resources, // Ghostty resources dir + + /// Returns the directory for the given theme based on this location type. + /// + /// This will return null with no error if the directory type doesn't + /// exist or is invalid for any reason. For example, it is perfectly + /// valid to install and run Ghostty without the resources directory. + /// + /// This may allocate memory but it isn't guaranteed so the allocator + /// should be something like an arena. It isn't safe to always free the + /// resulting pointer. + pub fn dir( + self: Location, + alloc_arena: Allocator, + theme: []const u8, + ) error{OutOfMemory}!?[]const u8 { + if (comptime std.debug.runtime_safety) { + assert(!std.fs.path.isAbsolute(theme)); + } + + return switch (self) { + .user => user: { + var buf: [std.fs.max_path_bytes]u8 = undefined; + const subdir = std.fmt.bufPrint( + &buf, + "ghostty/themes/{s}", + .{theme}, + ) catch |err| switch (err) { + error.NoSpaceLeft => return error.OutOfMemory, + }; + + break :user internal_os.xdg.config( + alloc_arena, + .{ .subdir = subdir }, + ) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + error.BufferTooSmall => return error.OutOfMemory, + + // No home dir means we don't have an XDG config dir + // so we can just return null. + error.NoHomeDir => return null, + }; + }, + + .resources => try std.fs.path.join(alloc_arena, &.{ + global_state.resources_dir orelse return null, + "themes", + theme, + }), + }; + } +}; + +/// An iterator that returns all possible locations for a theme in order +/// of priority. +pub const LocationIterator = struct { + alloc_arena: Allocator, + theme: []const u8, + i: usize = 0, + + pub fn next(self: *LocationIterator) !?[]const u8 { + const max = @typeInfo(Location).Enum.fields.len; + while (true) { + if (self.i >= max) return null; + const loc: Location = @enumFromInt(self.i); + self.i += 1; + const dir_ = try loc.dir(self.alloc_arena, self.theme); + const dir = dir_ orelse continue; + return dir; + } + } + + pub fn reset(self: *LocationIterator) void { + self.i = 0; + } +}; + +/// Open the given named theme. If there are any errors then messages +/// will be appended to the given error list and null is returned. If +/// a non-null return value is returned, there are never any errors added. +/// +/// One error that is not recoverable and may be returned is OOM. This is +/// always a critical error for configuration loading so it is returned. +pub fn open( + alloc_arena: Allocator, + theme: []const u8, + errors: *ErrorList, +) error{OutOfMemory}!?std.fs.File { + // Absolute themes are loaded a different path. + if (std.fs.path.isAbsolute(theme)) return try openAbsolute( + alloc_arena, + theme, + errors, + ); + + // Iterate over the possible locations to try to find the + // one that exists. + var it: LocationIterator = .{ .alloc_arena = alloc_arena, .theme = theme }; + const cwd = std.fs.cwd(); + while (try it.next()) |path| { + if (cwd.openFile(path, .{})) |file| { + return file; + } else |err| switch (err) { + // Not an error, just continue to the next location. + error.FileNotFound => {}, + + // Anything else is an error we log and give up on. + else => { + try errors.add(alloc_arena, .{ + .message = try std.fmt.allocPrintZ( + alloc_arena, + "failed to load theme \"{s}\" from the file \"{s}\": {}", + .{ theme, path, err }, + ), + }); + + return null; + }, + } + } + + // Unlikely scenario: the theme doesn't exist. In this case, we reset + // our iterator, reiterate over in order to build a better error message. + // This does double allocate some memory but for errors I think thats + // fine. + it.reset(); + while (try it.next()) |path| { + try errors.add(alloc_arena, .{ + .message = try std.fmt.allocPrintZ( + alloc_arena, + "theme \"{s}\" not found, tried path \"{s}\"", + .{ theme, path }, + ), + }); + } + + return null; +} + +/// Open the given theme from an absolute path. If there are any errors +/// then messages will be appended to the given error list and null is +/// returned. If a non-null return value is returned, there are never any +/// errors added. +pub fn openAbsolute( + alloc_arena: Allocator, + theme: []const u8, + errors: *ErrorList, +) error{OutOfMemory}!?std.fs.File { + return std.fs.openFileAbsolute(theme, .{}) catch |err| { + switch (err) { + error.FileNotFound => try errors.add(alloc_arena, .{ + .message = try std.fmt.allocPrintZ( + alloc_arena, + "failed to load theme from the path \"{s}\"", + .{theme}, + ), + }), + else => try errors.add(alloc_arena, .{ + .message = try std.fmt.allocPrintZ( + alloc_arena, + "failed to load theme from the path \"{s}\": {}", + .{ theme, err }, + ), + }), + } + + return null; + }; +}