From 7a11b22c5fea1b7b4e202a390f6f77e451f8afe9 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 11 Aug 2024 21:55:31 -0500 Subject: [PATCH 01/11] themes: allow loading from absolute paths and from user config dir --- src/cli/list_themes.zig | 77 ++++++++++++++------ src/config/Config.zig | 151 ++++++++++++++++++++++++++++++---------- 2 files changed, 171 insertions(+), 57 deletions(-) diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index beceab112..6539ad8d7 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -5,6 +5,7 @@ const Action = @import("action.zig").Action; const Arena = std.heap.ArenaAllocator; const Allocator = std.mem.Allocator; const Config = @import("../config/Config.zig"); +const internal_os = @import("../os/main.zig"); const global_state = &@import("../global.zig").state; pub const Options = struct { @@ -41,40 +42,76 @@ pub fn run(alloc: Allocator) !u8 { const stderr = std.io.getStdErr().writer(); const stdout = std.io.getStdOut().writer(); - const resources_dir = global_state.resources_dir orelse { + if (global_state.resources_dir == null) try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++ "that Ghostty is installed correctly.\n", .{}); - return 1; + + const paths: []const struct { + type: Config.ThemeDirType, + dir: ?[]const u8, + } = &.{ + .{ + .type = .user, + .dir = Config.themeDir(alloc, .user), + }, + .{ + .type = .system, + .dir = Config.themeDir(alloc, .system), + }, }; - const path = try std.fs.path.join(alloc, &.{ resources_dir, "themes" }); - defer alloc.free(path); + const ThemeListElement = struct { + type: Config.ThemeDirType, + path: []const u8, + theme: []const u8, + fn deinit(self: *const @This(), alloc_: std.mem.Allocator) void { + alloc_.free(self.path); + alloc_.free(self.theme); + } + fn lessThan(_: void, lhs: @This(), rhs: @This()) bool { + return std.ascii.orderIgnoreCase(lhs.theme, rhs.theme) == .lt; + } + }; - var dir = try std.fs.cwd().openDir(path, .{ .iterate = true }); - defer dir.close(); + var count: usize = 0; - var walker = try dir.walk(alloc); - defer walker.deinit(); - - var themes = std.ArrayList([]const u8).init(alloc); + var themes = std.ArrayList(ThemeListElement).init(alloc); defer { - for (themes.items) |v| alloc.free(v); + for (themes.items) |v| v.deinit(alloc); themes.deinit(); } - while (try walker.next()) |entry| { - if (entry.kind != .file) continue; - try themes.append(try alloc.dupe(u8, entry.basename)); + for (paths) |path| { + if (path.dir) |p| { + defer alloc.free(p); + + var dir = try std.fs.cwd().openDir(p, .{ .iterate = true }); + defer dir.close(); + + var walker = try dir.walk(alloc); + defer walker.deinit(); + + while (try walker.next()) |entry| { + if (entry.kind != .file) continue; + count += 1; + try themes.append(.{ + .type = path.type, + .path = try std.fs.path.join(alloc, &.{ p, entry.basename }), + .theme = try alloc.dupe(u8, entry.basename), + }); + } + } } - std.mem.sortUnstable([]const u8, themes.items, {}, struct { - fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool { - return std.ascii.orderIgnoreCase(lhs, rhs) == .lt; - } - }.lessThan); + std.mem.sortUnstable(ThemeListElement, themes.items, {}, ThemeListElement.lessThan); for (themes.items) |theme| { - try stdout.print("{s}\n", .{theme}); + try stdout.print("{s} ({s})\n", .{ theme.theme, @tagName(theme.type) }); + } + + if (count == 0) { + try stderr.print("No themes found, check to make sure that the themes were installed correctly.", .{}); + return 1; } return 0; diff --git a/src/config/Config.zig b/src/config/Config.zig index 681c0523e..38880f76a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -240,19 +240,29 @@ const c = @cImport({ /// terminals. Only new terminals will use the new configuration. @"grapheme-width-method": GraphemeWidthMethod = .unicode, -/// A named theme to use. The available themes are currently hardcoded to the -/// themes that ship with Ghostty. On macOS, this list is in the `Ghostty.app/ -/// Contents/Resources/ghostty/themes` directory. On Linux, this list is in the -/// `share/ghostty/themes` directory (wherever you installed the Ghostty "share" +/// A theme to use. If the theme is an absolute pathname, Ghostty will attempt +/// to load that file as a theme. If that file does not exist or is inaccessible, +/// an error will be logged and no other directories will be searched. +/// +/// If the theme is not an absolute pathname, two different directories will be +/// searched for a file name that matches the theme. This is case sensitive on +/// systems with case-sensitive filesystems. +/// +/// The first directory is the `themes` subdirectory of your Ghostty +/// configuration directory. This is `$XDG_CONFIG_DIR/ghostty/themes` or +/// `~/.config/ghostty/themes`. +/// +/// The second directory is the `themes` subdirectory of the Ghostty resources +/// directory. Ghostty ships with a multitude of themes that will be installed +/// into this directory. On macOS, this list is in the `Ghostty.app/Contents/ +/// Resources/ghostty/themes` directory. On Linux, this list is in the `share/ +/// ghostty/themes` directory (wherever you installed the Ghostty "share" /// directory. /// /// To see a list of available themes, run `ghostty +list-themes`. /// /// Any additional colors specified via background, foreground, palette, etc. /// will override the colors specified in the theme. -/// -/// A future update will allow custom themes to be installed in certain -/// directories. theme: ?[]const u8 = null, /// Background color for the window. @@ -2174,40 +2184,107 @@ fn expandPaths(self: *Config, base: []const u8) !void { } } +pub const ThemeDirType = enum { + user, + system, +}; + +pub fn themeDir(alloc: std.mem.Allocator, type_: ThemeDirType) ?[]const u8 { + return switch (type_) { + .user => internal_os.xdg.config(alloc, .{ .subdir = "ghostty/themes" }) catch null, + .system => result: { + const resources_dir = global_state.resources_dir orelse break :result null; + break :result std.fs.path.join(alloc, &.{ + resources_dir, + "themes", + }) catch null; + }, + }; +} + fn loadTheme(self: *Config, theme: []const u8) !void { const alloc = self._arena.?.allocator(); - const resources_dir = global_state.resources_dir orelse { - try self._errors.add(alloc, .{ - .message = "no resources directory found, themes will not work", - }); - return; - }; - const path = try std.fs.path.join(alloc, &.{ - resources_dir, - "themes", - theme, - }); - - const cwd = std.fs.cwd(); - var file = cwd.openFile(path, .{}) catch |err| { - switch (err) { - error.FileNotFound => try self._errors.add(alloc, .{ - .message = try std.fmt.allocPrintZ( - alloc, - "theme \"{s}\" not found, path={s}", - .{ theme, path }, - ), - }), - - else => try self._errors.add(alloc, .{ - .message = try std.fmt.allocPrintZ( - alloc, - "failed to load theme \"{s}\": {}", - .{ theme, err }, - ), - }), + 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; }; defer file.close(); From 87791ed562227bda183f763ebeff5688f4debbfc Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 11 Aug 2024 22:06:17 -0500 Subject: [PATCH 02/11] themes: add a switch that shows a small preview of each theme --- src/cli/list_themes.zig | 63 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 6539ad8d7..5de9351b8 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -9,6 +9,9 @@ const internal_os = @import("../os/main.zig"); const global_state = &@import("../global.zig").state; pub const Options = struct { + /// If true, show a small preview of the theme. + preview: bool = false, + pub fn deinit(self: Options) void { _ = self; } @@ -23,12 +26,26 @@ pub const Options = struct { /// The `list-themes` command is used to list all the available themes for /// Ghostty. /// -/// Themes require that Ghostty have access to the resources directory. On macOS -/// this is embedded in the app bundle. On Linux, this is usually in `/usr/ -/// share/ghostty`. If you're compiling from source, this is the `zig-out/share/ -/// ghostty` directory. You can also set the `GHOSTTY_RESOURCES_DIR` environment -/// variable to point to the resources directory. Themes live in the `themes` -/// subdirectory of the resources directory. +/// Two different directories will be searched for themes. +/// +/// The first directory is the `themes` subdirectory of your Ghostty +/// configuration directory. This is `$XDG_CONFIG_DIR/ghostty/themes` or +/// `~/.config/ghostty/themes`. +/// +/// The second directory is the `themes` subdirectory of the Ghostty resources +/// directory. Ghostty ships with a multitude of themes that will be installed +/// into this directory. On macOS, this directory is the `Ghostty.app/Contents/ +/// Resources/ghostty/themes`. On Linux, this directory is the `share/ghostty/ +/// themes` (wherever you installed the Ghostty "share" directory). If you're +/// running Ghostty from the source, this is the `zig-out/share/ghostty/themes` +/// directory. +/// +/// You can also set the `GHOSTTY_RESOURCES_DIR` environment variable to point +/// to the resources directory. +/// +/// Flags: +/// +/// * `--preview`: Show a short preview of the theme colors. pub fn run(alloc: Allocator) !u8 { var opts: Options = .{}; defer opts.deinit(); @@ -107,6 +124,40 @@ pub fn run(alloc: Allocator) !u8 { for (themes.items) |theme| { try stdout.print("{s} ({s})\n", .{ theme.theme, @tagName(theme.type) }); + + if (opts.preview) { + var config = try Config.default(alloc); + defer config.deinit(); + if (config.loadFile(config._arena.?.allocator(), theme.path)) |_| { + if (!config._errors.empty()) { + try stderr.print(" Problems were encountered trying to load the theme:\n", .{}); + for (config._errors.list.items) |err| { + try stderr.print(" {s}\n", .{err.message}); + } + } + try stdout.print("\n ", .{}); + for (0..8) |i| { + try stdout.print(" {d:2} \x1b[38;2;{d};{d};{d}m██\x1b[0m", .{ + i, + config.palette.value[i].r, + config.palette.value[i].g, + config.palette.value[i].b, + }); + } + try stdout.print("\n ", .{}); + for (8..16) |i| { + try stdout.print(" {d:2} \x1b[38;2;{d};{d};{d}m██\x1b[0m", .{ + i, + config.palette.value[i].r, + config.palette.value[i].g, + config.palette.value[i].b, + }); + } + try stdout.print("\n\n", .{}); + } else |err| { + try stderr.print("unable to load {s}: {}", .{ theme.path, err }); + } + } } if (count == 0) { From d3182c8d7c01461244b407a1c240ec5829446dc7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2024 21:36:07 -0700 Subject: [PATCH 03/11] config: move theme loading to dedicated file --- src/config/Config.zig | 89 ++------------------- src/config/theme.zig | 181 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 82 deletions(-) create mode 100644 src/config/theme.zig 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; + }; +} From 5e33d77160b2e7178d33cd78ba65f511de63e0d8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2024 21:39:18 -0700 Subject: [PATCH 04/11] config: use else to catch all errors since there are many --- src/config/theme.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/config/theme.zig b/src/config/theme.zig index 16aa244fe..62b1468fa 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -48,9 +48,10 @@ pub const Location = enum { 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, + // Any other error we treat as the XDG directory not + // existing. Windows in particularly can return a LOT + // of errors here. + else => return null, }; }, From 8c4cfc3bbbfe9bc2f1010f0612d0824c2c6e6413 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 12 Aug 2024 11:58:57 -0500 Subject: [PATCH 05/11] themes: finish refactor --- src/cli/list_themes.zig | 84 ++++++++++++--------------- src/config/Config.zig | 7 +-- src/config/theme.zig | 126 ++++++++++++++++++++++++---------------- 3 files changed, 115 insertions(+), 102 deletions(-) diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 5de9351b8..0509bffe9 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -2,13 +2,15 @@ const std = @import("std"); const inputpkg = @import("../input.zig"); const args = @import("args.zig"); const Action = @import("action.zig").Action; -const Arena = std.heap.ArenaAllocator; -const Allocator = std.mem.Allocator; const Config = @import("../config/Config.zig"); +const themepkg = @import("../config/theme.zig"); const internal_os = @import("../os/main.zig"); const global_state = &@import("../global.zig").state; pub const Options = struct { + /// If true, print the full path to the theme. + path: bool = false, + /// If true, show a small preview of the theme. preview: bool = false, @@ -45,17 +47,21 @@ pub const Options = struct { /// /// Flags: /// +/// * `--path`: Show the full path to the theme. /// * `--preview`: Show a short preview of the theme colors. -pub fn run(alloc: Allocator) !u8 { +pub fn run(gpa_alloc: std.mem.Allocator) !u8 { var opts: Options = .{}; defer opts.deinit(); { - var iter = try std.process.argsWithAllocator(alloc); + var iter = try std.process.argsWithAllocator(gpa_alloc); defer iter.deinit(); - try args.parse(Options, alloc, &opts, &iter); + try args.parse(Options, gpa_alloc, &opts, &iter); } + var arena = std.heap.ArenaAllocator.init(gpa_alloc); + const alloc = arena.allocator(); + const stderr = std.io.getStdErr().writer(); const stdout = std.io.getStdOut().writer(); @@ -63,29 +69,12 @@ pub fn run(alloc: Allocator) !u8 { try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++ "that Ghostty is installed correctly.\n", .{}); - const paths: []const struct { - type: Config.ThemeDirType, - dir: ?[]const u8, - } = &.{ - .{ - .type = .user, - .dir = Config.themeDir(alloc, .user), - }, - .{ - .type = .system, - .dir = Config.themeDir(alloc, .system), - }, - }; - const ThemeListElement = struct { - type: Config.ThemeDirType, + location: themepkg.Location, path: []const u8, theme: []const u8, - fn deinit(self: *const @This(), alloc_: std.mem.Allocator) void { - alloc_.free(self.path); - alloc_.free(self.theme); - } fn lessThan(_: void, lhs: @This(), rhs: @This()) bool { + // TODO: use Unicode-aware comparison return std.ascii.orderIgnoreCase(lhs.theme, rhs.theme) == .lt; } }; @@ -93,40 +82,41 @@ pub fn run(alloc: Allocator) !u8 { var count: usize = 0; var themes = std.ArrayList(ThemeListElement).init(alloc); - defer { - for (themes.items) |v| v.deinit(alloc); - themes.deinit(); - } - for (paths) |path| { - if (path.dir) |p| { - defer alloc.free(p); + var it = themepkg.LocationIterator{ .arena = &arena }; - var dir = try std.fs.cwd().openDir(p, .{ .iterate = true }); - defer dir.close(); + while (try it.next()) |loc| { + var dir = std.fs.cwd().openDir(loc.dir, .{ .iterate = true }) catch |err| switch (err) { + error.FileNotFound => continue, + else => { + std.debug.print("err: {}\n", .{err}); + continue; + }, + }; + defer dir.close(); + var walker = dir.iterate(); - var walker = try dir.walk(alloc); - defer walker.deinit(); - - while (try walker.next()) |entry| { - if (entry.kind != .file) continue; - count += 1; - try themes.append(.{ - .type = path.type, - .path = try std.fs.path.join(alloc, &.{ p, entry.basename }), - .theme = try alloc.dupe(u8, entry.basename), - }); - } + while (try walker.next()) |entry| { + if (entry.kind != .file) continue; + count += 1; + try themes.append(.{ + .location = loc.location, + .path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name }), + .theme = try alloc.dupe(u8, entry.name), + }); } } std.mem.sortUnstable(ThemeListElement, themes.items, {}, ThemeListElement.lessThan); for (themes.items) |theme| { - try stdout.print("{s} ({s})\n", .{ theme.theme, @tagName(theme.type) }); + if (opts.path) + try stdout.print("{s} ({s}) {s}\n", .{ theme.theme, @tagName(theme.location), theme.path }) + else + try stdout.print("{s} ({s})\n", .{ theme.theme, @tagName(theme.location) }); if (opts.preview) { - var config = try Config.default(alloc); + var config = try Config.default(gpa_alloc); defer config.deinit(); if (config.loadFile(config._arena.?.allocator(), theme.path)) |_| { if (!config._errors.empty()) { diff --git a/src/config/Config.zig b/src/config/Config.zig index 8901c1500..ee1b49983 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -247,7 +247,8 @@ const c = @cImport({ /// /// If the theme is not an absolute pathname, two different directories will be /// searched for a file name that matches the theme. This is case sensitive on -/// systems with case-sensitive filesystems. +/// systems with case-sensitive filesystems. It is an error for it to include +/// path separators. /// /// The first directory is the `themes` subdirectory of your Ghostty /// configuration directory. This is `$XDG_CONFIG_DIR/ghostty/themes` or @@ -2204,11 +2205,9 @@ pub fn themeDir(alloc: std.mem.Allocator, type_: ThemeDirType) ?[]const u8 { } fn loadTheme(self: *Config, theme: []const u8) !void { - const alloc = self._arena.?.allocator(); - // Find our theme file and open it. See the open function for details. const file: std.fs.File = (try themepkg.open( - alloc, + &self._arena.?, theme, &self._errors, )) orelse return; diff --git a/src/config/theme.zig b/src/config/theme.zig index 62b1468fa..e962324a6 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -1,48 +1,43 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; -const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; 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). +/// 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 + 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 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. + /// Due to the way allocations are handled, a pointer to an Arena allocator + /// must be used. pub fn dir( self: Location, - alloc_arena: Allocator, - theme: []const u8, + arena: *ArenaAllocator, ) error{OutOfMemory}!?[]const u8 { - if (comptime std.debug.runtime_safety) { - assert(!std.fs.path.isAbsolute(theme)); - } + const alloc = arena.allocator(); + + // 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, - }; + const subdir = std.fs.path.join(alloc, &.{ + "ghostty", "themes", + }) catch return error.OutOfMemory; break :user internal_os.xdg.config( - alloc_arena, + alloc, .{ .subdir = subdir }, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, @@ -55,32 +50,37 @@ pub const Location = enum { }; }, - .resources => try std.fs.path.join(alloc_arena, &.{ + .resources => try std.fs.path.join(alloc, &.{ global_state.resources_dir orelse return null, "themes", - theme, }), }; } }; -/// An iterator that returns all possible locations for a theme in order -/// of priority. +/// An iterator that returns all possible directories for finding themes in +/// order of priority. pub const LocationIterator = struct { - alloc_arena: Allocator, - theme: []const u8, + arena: *ArenaAllocator, i: usize = 0, - pub fn next(self: *LocationIterator) !?[]const u8 { + pub fn next(self: *LocationIterator) !?struct { + location: Location, + dir: []const u8, + } { const max = @typeInfo(Location).Enum.fields.len; - while (true) { - if (self.i >= max) return null; - const loc: Location = @enumFromInt(self.i); + std.debug.print("a: {d} {d}\n", .{ self.i, max }); + while (self.i < max) { + std.debug.print("b: {d}\n", .{self.i}); + const location: Location = @enumFromInt(self.i); self.i += 1; - const dir_ = try loc.dir(self.alloc_arena, self.theme); - const dir = dir_ orelse continue; - return dir; + if (try location.dir(self.arena)) |dir| + return .{ + .location = location, + .dir = dir, + }; } + return null; } pub fn reset(self: *LocationIterator) void { @@ -94,23 +94,42 @@ pub const LocationIterator = struct { /// /// 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. +/// +/// Due to the way allocations are handled, a pointer to an Arena allocator +/// must be used. pub fn open( - alloc_arena: Allocator, + arena: *ArenaAllocator, 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, + arena, theme, errors, ); + const alloc = arena.allocator(); + + const basename = std.fs.path.basename(theme); + if (!std.mem.eql(u8, theme, basename)) { + try errors.add(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "theme \"{s}\" cannot include path separators unless it is an absolute path", + .{theme}, + ), + }); + return null; + } + // Iterate over the possible locations to try to find the // one that exists. - var it: LocationIterator = .{ .alloc_arena = alloc_arena, .theme = theme }; + var it: LocationIterator = .{ .arena = arena }; const cwd = std.fs.cwd(); - while (try it.next()) |path| { + while (try it.next()) |loc| { + const path = try std.fs.path.join(alloc, &.{ loc.dir, theme }); if (cwd.openFile(path, .{})) |file| { return file; } else |err| switch (err) { @@ -119,9 +138,9 @@ pub fn open( // Anything else is an error we log and give up on. else => { - try errors.add(alloc_arena, .{ + try errors.add(alloc, .{ .message = try std.fmt.allocPrintZ( - alloc_arena, + alloc, "failed to load theme \"{s}\" from the file \"{s}\": {}", .{ theme, path, err }, ), @@ -137,10 +156,11 @@ pub fn open( // 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, .{ + while (try it.next()) |loc| { + const path = try std.fs.path.join(alloc, &.{ loc.dir, theme }); + try errors.add(alloc, .{ .message = try std.fmt.allocPrintZ( - alloc_arena, + alloc, "theme \"{s}\" not found, tried path \"{s}\"", .{ theme, path }, ), @@ -154,23 +174,27 @@ pub fn open( /// 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. +/// +/// Due to the way allocations are handled, a pointer to an Arena allocator +/// must be used. pub fn openAbsolute( - alloc_arena: Allocator, + arena: *ArenaAllocator, theme: []const u8, errors: *ErrorList, ) error{OutOfMemory}!?std.fs.File { + const alloc = arena.allocator(); return std.fs.openFileAbsolute(theme, .{}) catch |err| { switch (err) { - error.FileNotFound => try errors.add(alloc_arena, .{ + error.FileNotFound => try errors.add(alloc, .{ .message = try std.fmt.allocPrintZ( - alloc_arena, + alloc, "failed to load theme from the path \"{s}\"", .{theme}, ), }), - else => try errors.add(alloc_arena, .{ + else => try errors.add(alloc, .{ .message = try std.fmt.allocPrintZ( - alloc_arena, + alloc, "failed to load theme from the path \"{s}\": {}", .{ theme, err }, ), From 50c31ba173083b2a411e5727f61ac21dda147d06 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 12 Aug 2024 12:00:36 -0500 Subject: [PATCH 06/11] themes: fix comment --- 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 ee1b49983..d1d820cf9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -247,8 +247,8 @@ const c = @cImport({ /// /// If the theme is not an absolute pathname, two different directories will be /// searched for a file name that matches the theme. This is case sensitive on -/// systems with case-sensitive filesystems. It is an error for it to include -/// path separators. +/// systems with case-sensitive filesystems. It is an error for a theme name to +/// include path separators unless it is an absolute pathname. /// /// The first directory is the `themes` subdirectory of your Ghostty /// configuration directory. This is `$XDG_CONFIG_DIR/ghostty/themes` or From 7de692c955e4e96c5bf9c0dbe88986e8343e639e Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 12 Aug 2024 12:18:48 -0500 Subject: [PATCH 07/11] themes: don't use arena directly and cleanup debug code --- src/cli/list_themes.zig | 5 +-- src/config/Config.zig | 2 +- src/config/theme.zig | 75 +++++++++++++++++++---------------------- 3 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 0509bffe9..4a90df1c5 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -83,17 +83,18 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { var themes = std.ArrayList(ThemeListElement).init(alloc); - var it = themepkg.LocationIterator{ .arena = &arena }; + var it = themepkg.LocationIterator{ .arena_alloc = arena.allocator() }; while (try it.next()) |loc| { var dir = std.fs.cwd().openDir(loc.dir, .{ .iterate = true }) catch |err| switch (err) { error.FileNotFound => continue, else => { - std.debug.print("err: {}\n", .{err}); + std.debug.print("error trying to open {s}: {}\n", .{ loc.dir, err }); continue; }, }; defer dir.close(); + var walker = dir.iterate(); while (try walker.next()) |entry| { diff --git a/src/config/Config.zig b/src/config/Config.zig index d1d820cf9..ecb93f8f5 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2207,7 +2207,7 @@ pub fn themeDir(alloc: std.mem.Allocator, type_: ThemeDirType) ?[]const u8 { fn loadTheme(self: *Config, theme: []const u8) !void { // Find our theme file and open it. See the open function for details. const file: std.fs.File = (try themepkg.open( - &self._arena.?, + self._arena.?.allocator(), theme, &self._errors, )) orelse return; diff --git a/src/config/theme.zig b/src/config/theme.zig index e962324a6..810365eff 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -1,7 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; -const ArenaAllocator = std.heap.ArenaAllocator; +const Allocator = std.mem.Allocator; const global_state = &@import("../main.zig").state; const internal_os = @import("../os/main.zig"); const ErrorList = @import("ErrorList.zig"); @@ -18,26 +18,21 @@ pub const Location = enum { /// or is invalid for any reason. For example, it is perfectly valid to /// install and run Ghostty without the resources directory. /// - /// Due to the way allocations are handled, a pointer to an Arena allocator - /// must be used. + /// Due to the way allocations are handled, an Arena allocator (or another + /// similar allocator implementation) should be used. It may not be safe to + /// free the returned allocations. pub fn dir( self: Location, - arena: *ArenaAllocator, + arena_alloc: Allocator, ) error{OutOfMemory}!?[]const u8 { - const alloc = arena.allocator(); - - // if (comptime std.debug.runtime_safety) { - // assert(!std.fs.path.isAbsolute(theme)); - // } - return switch (self) { .user => user: { - const subdir = std.fs.path.join(alloc, &.{ + const subdir = std.fs.path.join(arena_alloc, &.{ "ghostty", "themes", }) catch return error.OutOfMemory; break :user internal_os.xdg.config( - alloc, + arena_alloc, .{ .subdir = subdir }, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, @@ -50,7 +45,7 @@ pub const Location = enum { }; }, - .resources => try std.fs.path.join(alloc, &.{ + .resources => try std.fs.path.join(arena_alloc, &.{ global_state.resources_dir orelse return null, "themes", }), @@ -61,7 +56,10 @@ pub const Location = enum { /// An iterator that returns all possible directories for finding themes in /// order of priority. pub const LocationIterator = struct { - arena: *ArenaAllocator, + /// Due to the way allocations are handled, an Arena allocator (or another + /// similar allocator implementation) should be used. It may not be safe to + /// free the returned allocations. + arena_alloc: Allocator, i: usize = 0, pub fn next(self: *LocationIterator) !?struct { @@ -69,12 +67,10 @@ pub const LocationIterator = struct { dir: []const u8, } { const max = @typeInfo(Location).Enum.fields.len; - std.debug.print("a: {d} {d}\n", .{ self.i, max }); while (self.i < max) { - std.debug.print("b: {d}\n", .{self.i}); const location: Location = @enumFromInt(self.i); self.i += 1; - if (try location.dir(self.arena)) |dir| + if (try location.dir(self.arena_alloc)) |dir| return .{ .location = location, .dir = dir, @@ -95,28 +91,27 @@ pub const LocationIterator = struct { /// 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. /// -/// Due to the way allocations are handled, a pointer to an Arena allocator -/// must be used. +/// Due to the way allocations are handled, an Arena allocator (or another +/// similar allocator implementation) should be used. It may not be safe to +/// free the returned allocations. pub fn open( - arena: *ArenaAllocator, + arena_alloc: 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( - arena, + arena_alloc, theme, errors, ); - const alloc = arena.allocator(); - const basename = std.fs.path.basename(theme); if (!std.mem.eql(u8, theme, basename)) { - try errors.add(alloc, .{ + try errors.add(arena_alloc, .{ .message = try std.fmt.allocPrintZ( - alloc, + arena_alloc, "theme \"{s}\" cannot include path separators unless it is an absolute path", .{theme}, ), @@ -126,10 +121,10 @@ pub fn open( // Iterate over the possible locations to try to find the // one that exists. - var it: LocationIterator = .{ .arena = arena }; + var it: LocationIterator = .{ .arena_alloc = arena_alloc }; const cwd = std.fs.cwd(); while (try it.next()) |loc| { - const path = try std.fs.path.join(alloc, &.{ loc.dir, theme }); + const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme }); if (cwd.openFile(path, .{})) |file| { return file; } else |err| switch (err) { @@ -138,9 +133,9 @@ pub fn open( // Anything else is an error we log and give up on. else => { - try errors.add(alloc, .{ + try errors.add(arena_alloc, .{ .message = try std.fmt.allocPrintZ( - alloc, + arena_alloc, "failed to load theme \"{s}\" from the file \"{s}\": {}", .{ theme, path, err }, ), @@ -157,10 +152,10 @@ pub fn open( // fine. it.reset(); while (try it.next()) |loc| { - const path = try std.fs.path.join(alloc, &.{ loc.dir, theme }); - try errors.add(alloc, .{ + const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme }); + try errors.add(arena_alloc, .{ .message = try std.fmt.allocPrintZ( - alloc, + arena_alloc, "theme \"{s}\" not found, tried path \"{s}\"", .{ theme, path }, ), @@ -175,26 +170,26 @@ pub fn open( /// returned. If a non-null return value is returned, there are never any /// errors added. /// -/// Due to the way allocations are handled, a pointer to an Arena allocator -/// must be used. +/// Due to the way allocations are handled, an Arena allocator (or another +/// similar allocator implementation) should be used. It may not be safe to +/// free the returned allocations. pub fn openAbsolute( - arena: *ArenaAllocator, + arena_alloc: Allocator, theme: []const u8, errors: *ErrorList, ) error{OutOfMemory}!?std.fs.File { - const alloc = arena.allocator(); return std.fs.openFileAbsolute(theme, .{}) catch |err| { switch (err) { - error.FileNotFound => try errors.add(alloc, .{ + error.FileNotFound => try errors.add(arena_alloc, .{ .message = try std.fmt.allocPrintZ( - alloc, + arena_alloc, "failed to load theme from the path \"{s}\"", .{theme}, ), }), - else => try errors.add(alloc, .{ + else => try errors.add(arena_alloc, .{ .message = try std.fmt.allocPrintZ( - alloc, + arena_alloc, "failed to load theme from the path \"{s}\": {}", .{ theme, err }, ), From c7e0173158d6f5280a3b021ec9ecaac6b353d690 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 18 Aug 2024 19:11:54 -0700 Subject: [PATCH 08/11] config: remove unused types and function --- src/config/Config.zig | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index ecb93f8f5..6d45ab4ec 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2186,24 +2186,6 @@ fn expandPaths(self: *Config, base: []const u8) !void { } } -pub const ThemeDirType = enum { - user, - system, -}; - -pub fn themeDir(alloc: std.mem.Allocator, type_: ThemeDirType) ?[]const u8 { - return switch (type_) { - .user => internal_os.xdg.config(alloc, .{ .subdir = "ghostty/themes" }) catch null, - .system => result: { - const resources_dir = global_state.resources_dir orelse break :result null; - break :result std.fs.path.join(alloc, &.{ - resources_dir, - "themes", - }) catch null; - }, - }; -} - fn loadTheme(self: *Config, theme: []const u8) !void { // Find our theme file and open it. See the open function for details. const file: std.fs.File = (try themepkg.open( From b79d80dc82602eccf03937eab9383a1e6e71012b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 18 Aug 2024 19:23:43 -0700 Subject: [PATCH 09/11] Fix broken import from rebase --- src/config/theme.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/theme.zig b/src/config/theme.zig index 810365eff..aabf306ee 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -2,7 +2,7 @@ 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 global_state = &@import("../global.zig").state; const internal_os = @import("../os/main.zig"); const ErrorList = @import("ErrorList.zig"); From ea0e3057caa154d81dd32f587d5e7a3aa2dae07a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 18 Aug 2024 19:31:36 -0700 Subject: [PATCH 10/11] config: fix build on iOS for error set --- src/config/theme.zig | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/config/theme.zig b/src/config/theme.zig index aabf306ee..0054ae089 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -34,14 +34,24 @@ pub const Location = enum { break :user internal_os.xdg.config( arena_alloc, .{ .subdir = subdir }, - ) catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, - error.BufferTooSmall => return error.OutOfMemory, + ) catch |err| { + // We need to do some comptime trick sot get the right + // error set since some platforms don't support some + // error types. + const Error = @TypeOf(err) || switch (builtin.os.tag) { + .ios => error{BufferTooSmall}, + else => error{}, + }; - // Any other error we treat as the XDG directory not - // existing. Windows in particularly can return a LOT - // of errors here. - else => return null, + switch (@as(Error, err)) { + error.OutOfMemory => return error.OutOfMemory, + error.BufferTooSmall => return error.OutOfMemory, + + // Any other error we treat as the XDG directory not + // existing. Windows in particularly can return a LOT + // of errors here. + else => return null, + } }; }, From 00f642a28f3da30287cfa3080f17cf981727802a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 18 Aug 2024 19:34:11 -0700 Subject: [PATCH 11/11] typos --- src/config/theme.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/theme.zig b/src/config/theme.zig index 0054ae089..fdb5dd08a 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -35,7 +35,7 @@ pub const Location = enum { arena_alloc, .{ .subdir = subdir }, ) catch |err| { - // We need to do some comptime trick sot get the right + // We need to do some comptime tricks to get the right // error set since some platforms don't support some // error types. const Error = @TypeOf(err) || switch (builtin.os.tag) {