aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/cli/list_themes.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/cli/list_themes.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/cli/list_themes.zig')
-rw-r--r--vendor/libghostty-vt/src/cli/list_themes.zig1741
1 files changed, 1741 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/cli/list_themes.zig b/vendor/libghostty-vt/src/cli/list_themes.zig
new file mode 100644
index 00000000..42aff9d5
--- /dev/null
+++ b/vendor/libghostty-vt/src/cli/list_themes.zig
@@ -0,0 +1,1741 @@
+const std = @import("std");
+const args = @import("args.zig");
+const Action = @import("ghostty.zig").Action;
+const Config = @import("../config/Config.zig");
+const configpkg = @import("../config.zig");
+const themepkg = @import("../config/theme.zig");
+const tui = @import("tui.zig");
+const global_state = &@import("../global.zig").state;
+
+const vaxis = @import("vaxis");
+const zf = @import("zf");
+
+// When the number of filtered themes is less than or equal to this threshold,
+// the window position will be reset to 0 to show all results from the top.
+// This ensures better visibility for small result sets while maintaining
+// scroll position for larger lists.
+const SMALL_LIST_THRESHOLD = 10;
+
+const ColorScheme = enum { all, dark, light };
+
+pub const Options = struct {
+ /// If true, print the full path to the theme.
+ path: bool = false,
+
+ /// If true, force a plain list of themes.
+ plain: bool = false,
+
+ /// Specifies the color scheme of the themes to include in the list.
+ color: ColorScheme = .all,
+
+ pub fn deinit(self: Options) void {
+ _ = self;
+ }
+
+ /// Enables "-h" and "--help" to work.
+ pub fn help(self: Options) !void {
+ _ = self;
+ return Action.help_error;
+ }
+};
+
+const ThemeListElement = struct {
+ location: themepkg.Location,
+ path: []const u8,
+ theme: []const u8,
+ rank: ?f64 = null,
+
+ fn lessThan(_: void, lhs: @This(), rhs: @This()) bool {
+ // TODO: use Unicode-aware comparison
+ return std.ascii.orderIgnoreCase(lhs.theme, rhs.theme) == .lt;
+ }
+
+ pub fn toUri(self: *const ThemeListElement, alloc: std.mem.Allocator) ![]const u8 {
+ const uri = std.Uri{
+ .scheme = "file",
+ .host = .{ .raw = "" },
+ .path = .{ .raw = self.path },
+ };
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ errdefer buf.deinit();
+ try uri.writeToStream(
+ &buf.writer,
+ .{ .scheme = true, .authority = true, .path = true },
+ );
+ return buf.toOwnedSlice();
+ }
+};
+
+/// The `list-themes` command is used to preview or list all the available
+/// themes for Ghostty.
+///
+/// If this command is run from a TTY, a TUI preview of the themes will be
+/// shown. While in the preview, `F1` will bring up a help screen and `ESC` will
+/// exit the preview. Other keys that can be used to navigate the preview are
+/// listed in the help screen.
+///
+/// If this command is not run from a TTY, or the output is piped to another
+/// command, a plain list of theme names will be printed to the screen. A plain
+/// list can be forced using the `--plain` CLI flag.
+///
+/// Two different directories will be searched for themes.
+///
+/// The first directory is the `themes` subdirectory of your Ghostty
+/// configuration directory. This is `$XDG_CONFIG_HOME/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:
+///
+/// * `--path`: Show the full path to the theme.
+///
+/// * `--plain`: Force a plain listing of themes.
+///
+/// * `--color`: Specify the color scheme of the themes included in the list.
+/// This can be `dark`, `light`, or `all`. The default is `all`.
+pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
+ var opts: Options = .{};
+ defer opts.deinit();
+
+ {
+ var iter = try args.argsIterator(gpa_alloc);
+ defer iter.deinit();
+ try args.parse(Options, gpa_alloc, &opts, &iter);
+ }
+
+ var arena = std.heap.ArenaAllocator.init(gpa_alloc);
+ const alloc = arena.allocator();
+
+ var stdout_buf: [4096]u8 = undefined;
+ var stdout_file: std.fs.File = .stdout();
+ var stdout_writer = stdout_file.writer(&stdout_buf);
+ const stdout = &stdout_writer.interface;
+
+ var stderr_buf: [4096]u8 = undefined;
+ var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
+ const stderr = &stderr_writer.interface;
+
+ const resources_dir = global_state.resources_dir.app();
+ if (resources_dir == null)
+ try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++
+ "that Ghostty is installed correctly.\n", .{});
+
+ var count: usize = 0;
+
+ var themes: std.ArrayList(ThemeListElement) = .empty;
+
+ 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("error trying to open {s}: {}\n", .{ loc.dir, err });
+ continue;
+ },
+ };
+ defer dir.close();
+
+ var walker = dir.iterate();
+
+ while (try walker.next()) |entry| {
+ switch (entry.kind) {
+ .file, .sym_link => {
+ if (std.mem.eql(u8, entry.name, ".DS_Store"))
+ continue;
+ count += 1;
+
+ const path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name });
+ try themes.append(alloc, .{
+ .path = path,
+ .location = loc.location,
+ .theme = try alloc.dupe(u8, entry.name),
+ });
+ },
+ else => {},
+ }
+ }
+ }
+
+ if (count == 0) {
+ try stderr.print("No themes found, check to make sure that the themes were installed correctly.", .{});
+ return 1;
+ }
+
+ std.mem.sortUnstable(ThemeListElement, themes.items, {}, ThemeListElement.lessThan);
+
+ if (tui.can_pretty_print and !opts.plain and stdout_file.isTty()) {
+ try preview(gpa_alloc, themes.items, opts.color);
+ return 0;
+ }
+
+ var theme_config = try Config.default(gpa_alloc);
+ defer theme_config.deinit();
+ for (themes.items) |theme| {
+ try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path);
+ if (!shouldIncludeTheme(opts.color, theme_config)) {
+ continue;
+ }
+ if (opts.path)
+ try stdout.print("{s} ({t}) {s}\n", .{ theme.theme, theme.location, theme.path })
+ else
+ try stdout.print("{s} ({t})\n", .{ theme.theme, theme.location });
+ }
+
+ // Don't forget to flush!
+ try stdout.flush();
+ return 0;
+}
+
+fn resolveAutoThemePath(alloc: std.mem.Allocator) ![]u8 {
+ const main_cfg_path = try configpkg.preferredDefaultFilePath(alloc);
+ defer alloc.free(main_cfg_path);
+
+ const base_dir = std.fs.path.dirname(main_cfg_path) orelse return error.BadPathName;
+ return try std.fs.path.join(alloc, &.{ base_dir, "auto", "theme.ghostty" });
+}
+
+fn writeAutoThemeFile(alloc: std.mem.Allocator, theme_name: []const u8) !void {
+ const auto_path = try resolveAutoThemePath(alloc);
+ defer alloc.free(auto_path);
+
+ if (std.fs.path.dirname(auto_path)) |dir| {
+ try std.fs.cwd().makePath(dir);
+ }
+
+ var f = try std.fs.createFileAbsolute(auto_path, .{ .truncate = true });
+ defer f.close();
+
+ var buf: [128]u8 = undefined;
+ var w = f.writer(&buf);
+ try w.interface.print("theme = {s}\n", .{theme_name});
+ try w.interface.flush();
+}
+
+const Event = union(enum) {
+ key_press: vaxis.Key,
+ mouse: vaxis.Mouse,
+ color_scheme: vaxis.Color.Scheme,
+ winsize: vaxis.Winsize,
+};
+
+const Preview = struct {
+ allocator: std.mem.Allocator,
+ should_quit: bool,
+ tty: vaxis.Tty,
+ vx: vaxis.Vaxis,
+ mouse: ?vaxis.Mouse,
+ themes: []ThemeListElement,
+ filtered: std.ArrayList(usize),
+ current: usize,
+ window: usize,
+ hex: bool,
+ mode: enum {
+ normal,
+ help,
+ search,
+ save,
+ },
+ color_scheme: vaxis.Color.Scheme,
+ text_input: vaxis.widgets.TextInput,
+ theme_filter: ColorScheme,
+
+ pub fn init(
+ allocator: std.mem.Allocator,
+ themes: []ThemeListElement,
+ theme_filter: ColorScheme,
+ buf: []u8,
+ ) !*Preview {
+ const self = try allocator.create(Preview);
+
+ self.* = .{
+ .allocator = allocator,
+ .should_quit = false,
+ .tty = try .init(buf),
+ .vx = try vaxis.init(allocator, .{}),
+ .mouse = null,
+ .themes = themes,
+ .filtered = try .initCapacity(allocator, themes.len),
+ .current = 0,
+ .window = 0,
+ .hex = false,
+ .mode = .normal,
+ .color_scheme = .light,
+ .text_input = .init(allocator),
+ .theme_filter = theme_filter,
+ };
+
+ try self.updateFiltered();
+
+ return self;
+ }
+
+ pub fn deinit(self: *Preview) void {
+ const allocator = self.allocator;
+ self.filtered.deinit(allocator);
+ self.text_input.deinit();
+ self.vx.deinit(allocator, self.tty.writer());
+ self.tty.deinit();
+ allocator.destroy(self);
+ }
+
+ pub fn run(self: *Preview) !void {
+ var loop: vaxis.Loop(Event) = .{
+ .tty = &self.tty,
+ .vaxis = &self.vx,
+ };
+ try loop.init();
+ try loop.start();
+
+ const writer = self.tty.writer();
+
+ try self.vx.enterAltScreen(writer);
+ try self.vx.setTitle(writer, "👻 Ghostty Theme Preview 👻");
+ try self.vx.queryTerminal(writer, 1 * std.time.ns_per_s);
+ try self.vx.setMouseMode(writer, true);
+ if (self.vx.caps.color_scheme_updates)
+ try self.vx.subscribeToColorSchemeUpdates(writer);
+
+ while (!self.should_quit) {
+ var arena = std.heap.ArenaAllocator.init(self.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ loop.pollEvent();
+ while (loop.tryEvent()) |event| {
+ try self.update(event, alloc);
+ }
+ try self.draw(alloc);
+
+ try self.vx.render(writer);
+ try writer.flush();
+ }
+ }
+
+ fn updateFiltered(self: *Preview) !void {
+ const relative = self.current -| self.window;
+ var selected: []const u8 = undefined;
+ if (self.filtered.items.len > 0) {
+ selected = self.themes[self.filtered.items[self.current]].theme;
+ }
+
+ const hash_algorithm = std.hash.Wyhash;
+
+ const old_digest = d: {
+ var hash = hash_algorithm.init(0);
+ for (self.filtered.items) |item|
+ hash.update(std.mem.asBytes(&item));
+ break :d hash.final();
+ };
+
+ self.filtered.clearRetainingCapacity();
+
+ var theme_config = try Config.default(self.allocator);
+ defer theme_config.deinit();
+ if (self.text_input.buf.realLength() > 0) {
+ const first_half = self.text_input.buf.firstHalf();
+ const second_half = self.text_input.buf.secondHalf();
+
+ const buffer = try self.allocator.alloc(u8, first_half.len + second_half.len);
+ defer self.allocator.free(buffer);
+
+ @memcpy(buffer[0..first_half.len], first_half);
+ @memcpy(buffer[first_half.len..], second_half);
+
+ const string = try std.ascii.allocLowerString(self.allocator, buffer);
+ defer self.allocator.free(string);
+
+ var tokens: std.ArrayList([]const u8) = .empty;
+ defer tokens.deinit(self.allocator);
+
+ var it = std.mem.tokenizeScalar(u8, string, ' ');
+ while (it.next()) |token| try tokens.append(self.allocator, token);
+
+ for (self.themes, 0..) |*theme, i| {
+ try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path);
+ if (!shouldIncludeTheme(self.theme_filter, theme_config)) continue;
+
+ theme.rank = zf.rank(theme.theme, tokens.items, .{
+ .to_lower = true,
+ .plain = true,
+ });
+ if (theme.rank != null) try self.filtered.append(self.allocator, i);
+ }
+ } else {
+ for (self.themes, 0..) |*theme, i| {
+ try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path);
+ if (shouldIncludeTheme(self.theme_filter, theme_config)) {
+ try self.filtered.append(self.allocator, i);
+ theme.rank = null;
+ }
+ }
+ }
+
+ const new_digest = d: {
+ var hash = hash_algorithm.init(0);
+ for (self.filtered.items) |item|
+ hash.update(std.mem.asBytes(&item));
+ break :d hash.final();
+ };
+
+ if (old_digest == new_digest) return;
+
+ if (self.filtered.items.len == 0) {
+ self.current = 0;
+ self.window = 0;
+ return;
+ }
+
+ self.current, self.window = current: {
+ if (selected.len == 0) break :current .{ 0, 0 };
+
+ for (self.filtered.items, 0..) |index, i| {
+ if (std.mem.eql(u8, self.themes[index].theme, selected)) {
+ // Keep the relative position but ensure all search results are visible
+ const new_window = i -| relative;
+ // If the new window would hide some results at the top, adjust it
+ break :current .{ i, if (self.filtered.items.len <= SMALL_LIST_THRESHOLD) 0 else new_window };
+ }
+ }
+ break :current .{ 0, 0 };
+ };
+ }
+
+ fn up(self: *Preview, count: usize) void {
+ if (self.filtered.items.len == 0) {
+ self.current = 0;
+ return;
+ }
+ self.current -|= count;
+ }
+
+ fn down(self: *Preview, count: usize) void {
+ if (self.filtered.items.len == 0) {
+ self.current = 0;
+ return;
+ }
+ self.current += count;
+ if (self.current >= self.filtered.items.len)
+ self.current = self.filtered.items.len - 1;
+ }
+
+ pub fn update(self: *Preview, event: Event, alloc: std.mem.Allocator) !void {
+ switch (event) {
+ .key_press => |key| {
+ if (key.matches('c', .{ .ctrl = true }))
+ self.should_quit = true;
+ switch (self.mode) {
+ .normal => {
+ if (key.matchesAny(&.{ 'q', vaxis.Key.escape }, .{}))
+ self.should_quit = true;
+ if (key.matchesAny(&.{ '?', vaxis.Key.f1 }, .{}))
+ self.mode = .help;
+ if (key.matches('h', .{ .ctrl = true }))
+ self.mode = .help;
+ if (key.matches('/', .{}))
+ self.mode = .search;
+ if (key.matchesAny(&.{ vaxis.Key.enter, vaxis.Key.kp_enter }, .{}))
+ self.mode = .save;
+ if (key.matchesAny(&.{ 'x', '/' }, .{ .ctrl = true })) {
+ self.text_input.buf.clearRetainingCapacity();
+ try self.updateFiltered();
+ }
+ if (key.matchesAny(&.{ vaxis.Key.home, vaxis.Key.kp_home }, .{}))
+ self.current = 0;
+ if (key.matchesAny(&.{ vaxis.Key.end, vaxis.Key.kp_end }, .{}))
+ self.current = self.filtered.items.len - 1;
+ if (key.matchesAny(&.{ 'j', '+', vaxis.Key.down, vaxis.Key.kp_down, vaxis.Key.kp_add }, .{}))
+ self.down(1);
+ if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{}))
+ self.down(20);
+ if (key.matchesAny(&.{ 'k', '-', vaxis.Key.up, vaxis.Key.kp_up, vaxis.Key.kp_subtract }, .{}))
+ self.up(1);
+ if (key.matchesAny(&.{ vaxis.Key.page_up, vaxis.Key.kp_page_up }, .{}))
+ self.up(20);
+ if (key.matchesAny(&.{ 'h', 'x' }, .{}))
+ self.hex = true;
+ if (key.matches('d', .{}))
+ self.hex = false;
+ if (key.matches('c', .{}))
+ try self.vx.copyToSystemClipboard(
+ self.tty.writer(),
+ self.themes[self.filtered.items[self.current]].theme,
+ alloc,
+ )
+ else if (key.matches('c', .{ .shift = true }))
+ try self.vx.copyToSystemClipboard(
+ self.tty.writer(),
+ self.themes[self.filtered.items[self.current]].path,
+ alloc,
+ );
+ if (key.matches('f', .{})) {
+ switch (self.theme_filter) {
+ .all => self.theme_filter = .dark,
+ .dark => self.theme_filter = .light,
+ .light => self.theme_filter = .all,
+ }
+ try self.updateFiltered();
+ }
+ },
+ .help => {
+ if (key.matches('q', .{}))
+ self.should_quit = true;
+ if (key.matchesAny(&.{ '?', vaxis.Key.escape, vaxis.Key.f1 }, .{}))
+ self.mode = .normal;
+ if (key.matches('h', .{ .ctrl = true }))
+ self.mode = .normal;
+ },
+ .search => search: {
+ if (key.matchesAny(&.{ vaxis.Key.escape, vaxis.Key.enter }, .{})) {
+ self.mode = .normal;
+ break :search;
+ }
+ if (key.matchesAny(&.{ 'x', '/' }, .{ .ctrl = true })) {
+ self.text_input.clearRetainingCapacity();
+ try self.updateFiltered();
+ break :search;
+ }
+ try self.text_input.update(.{ .key_press = key });
+ try self.updateFiltered();
+ },
+ .save => {
+ if (key.matches('q', .{}))
+ self.should_quit = true;
+ if (key.matchesAny(&.{ vaxis.Key.escape, vaxis.Key.enter, vaxis.Key.kp_enter }, .{}))
+ self.mode = .normal;
+ if (key.matches('w', .{})) {
+ self.saveSelectedTheme();
+ }
+ },
+ }
+ },
+ .color_scheme => |color_scheme| self.color_scheme = color_scheme,
+ .mouse => |mouse| self.mouse = mouse,
+ .winsize => |ws| try self.vx.resize(self.allocator, self.tty.writer(), ws),
+ }
+ }
+
+ pub fn ui_fg(self: *Preview) vaxis.Color {
+ return switch (self.color_scheme) {
+ .light => .{ .rgb = [_]u8{ 0x00, 0x00, 0x00 } },
+ .dark => .{ .rgb = [_]u8{ 0xff, 0xff, 0xff } },
+ };
+ }
+
+ pub fn ui_bg(self: *Preview) vaxis.Color {
+ return switch (self.color_scheme) {
+ .light => .{ .rgb = [_]u8{ 0xff, 0xff, 0xff } },
+ .dark => .{ .rgb = [_]u8{ 0x00, 0x00, 0x00 } },
+ };
+ }
+
+ pub fn ui_standard(self: *Preview) vaxis.Style {
+ return .{
+ .fg = self.ui_fg(),
+ .bg = self.ui_bg(),
+ };
+ }
+
+ pub fn ui_hover_bg(self: *Preview) vaxis.Color {
+ return switch (self.color_scheme) {
+ .light => .{ .rgb = [_]u8{ 0xbb, 0xbb, 0xbb } },
+ .dark => .{ .rgb = [_]u8{ 0x22, 0x22, 0x22 } },
+ };
+ }
+
+ pub fn ui_highlighted(self: *Preview) vaxis.Style {
+ return .{
+ .fg = self.ui_fg(),
+ .bg = self.ui_hover_bg(),
+ };
+ }
+
+ pub fn ui_selected_fg(self: *Preview) vaxis.Color {
+ return switch (self.color_scheme) {
+ .light => .{ .rgb = [_]u8{ 0x00, 0xaa, 0x00 } },
+ .dark => .{ .rgb = [_]u8{ 0x00, 0xaa, 0x00 } },
+ };
+ }
+
+ pub fn ui_selected_bg(self: *Preview) vaxis.Color {
+ return switch (self.color_scheme) {
+ .light => .{ .rgb = [_]u8{ 0xaa, 0xaa, 0xaa } },
+ .dark => .{ .rgb = [_]u8{ 0x33, 0x33, 0x33 } },
+ };
+ }
+
+ pub fn ui_selected(self: *Preview) vaxis.Style {
+ return .{
+ .fg = self.ui_selected_fg(),
+ .bg = self.ui_selected_bg(),
+ };
+ }
+
+ pub fn ui_err_fg(self: *Preview) vaxis.Color {
+ return switch (self.color_scheme) {
+ .light => .{ .rgb = [_]u8{ 0xff, 0x00, 0x00 } },
+ .dark => .{ .rgb = [_]u8{ 0xff, 0x00, 0x00 } },
+ };
+ }
+
+ pub fn ui_err(self: *Preview) vaxis.Style {
+ return .{
+ .fg = self.ui_err_fg(),
+ .bg = self.ui_bg(),
+ };
+ }
+
+ pub fn draw(self: *Preview, alloc: std.mem.Allocator) !void {
+ const win = self.vx.window();
+ win.clear();
+
+ self.vx.setMouseShape(.default);
+
+ const theme_list = win.child(.{
+ .x_off = 0,
+ .y_off = 0,
+ .width = 32,
+ .height = win.height,
+ });
+
+ var highlight: ?usize = null;
+
+ if (self.mouse) |mouse| {
+ self.mouse = null;
+ if (self.mode == .normal) {
+ if (mouse.button == .wheel_up) {
+ self.up(1);
+ }
+ if (mouse.button == .wheel_down) {
+ self.down(1);
+ }
+ if (theme_list.hasMouse(mouse)) |_| {
+ if (mouse.button == .left and mouse.type == .release) {
+ const selection = self.window + mouse.row;
+ if (selection < self.filtered.items.len) {
+ self.current = selection;
+ }
+ }
+ highlight = mouse.row;
+ }
+ }
+ }
+
+ if (self.filtered.items.len == 0) {
+ self.current = 0;
+ self.window = 0;
+ } else {
+ const start = self.window;
+ const end = self.window + theme_list.height - 1;
+ if (self.current > end)
+ self.window = self.current - theme_list.height + 1;
+ if (self.current < start)
+ self.window = self.current;
+ if (self.window >= self.filtered.items.len)
+ self.window = self.filtered.items.len - 1;
+ }
+
+ theme_list.fill(.{ .style = self.ui_standard() });
+
+ for (0..theme_list.height) |row_capture| {
+ const row: u16 = @intCast(row_capture);
+ const index = self.window + row;
+ if (index >= self.filtered.items.len) break;
+
+ const theme = self.themes[self.filtered.items[index]];
+
+ const style: enum { normal, highlighted, selected } = style: {
+ if (index == self.current) break :style .selected;
+ if (highlight) |h| if (h == row) break :style .highlighted;
+ break :style .normal;
+ };
+
+ if (style == .selected) {
+ _ = theme_list.printSegment(
+ .{
+ .text = "❯ ",
+ .style = self.ui_selected(),
+ },
+ .{
+ .row_offset = row,
+ .col_offset = 0,
+ },
+ );
+ }
+ _ = theme_list.printSegment(
+ .{
+ .text = theme.theme,
+ .style = switch (style) {
+ .normal => self.ui_standard(),
+ .highlighted => self.ui_highlighted(),
+ .selected => self.ui_selected(),
+ },
+ .link = .{
+ .uri = try theme.toUri(alloc),
+ },
+ },
+ .{
+ .row_offset = row,
+ .col_offset = 2,
+ },
+ );
+ if (style == .selected) {
+ if (theme.theme.len < theme_list.width - 4) {
+ for (2 + theme.theme.len..theme_list.width - 2) |i_capture| {
+ const i: u16 = @intCast(i_capture);
+ _ = theme_list.printSegment(
+ .{
+ .text = " ",
+ .style = self.ui_selected(),
+ },
+ .{
+ .row_offset = row,
+ .col_offset = i,
+ },
+ );
+ }
+ }
+ _ = theme_list.printSegment(
+ .{
+ .text = " ❮",
+ .style = self.ui_selected(),
+ },
+ .{
+ .row_offset = row,
+ .col_offset = theme_list.width - 2,
+ },
+ );
+ }
+ }
+
+ try self.drawPreview(alloc, win, theme_list.x_off + theme_list.width);
+
+ switch (self.mode) {
+ .normal => {
+ win.hideCursor();
+ },
+ .help => {
+ win.hideCursor();
+ const width = 60;
+ const height = 22;
+ const child = win.child(
+ .{
+ .x_off = win.width / 2 -| width / 2,
+ .y_off = win.height / 2 -| height / 2,
+ .width = width,
+ .height = height,
+ .border = .{
+ .where = .all,
+ .style = self.ui_standard(),
+ },
+ },
+ );
+
+ child.fill(.{ .style = self.ui_standard() });
+
+ const key_help = [_]struct { keys: []const u8, help: []const u8 }{
+ .{ .keys = "^C, q, ESC", .help = "Quit." },
+ .{ .keys = "F1, ?, ^H", .help = "Toggle help window." },
+ .{ .keys = "f", .help = "Cycle through theme filters." },
+ .{ .keys = "k, ↑", .help = "Move up 1 theme." },
+ .{ .keys = "ScrollUp", .help = "Move up 1 theme." },
+ .{ .keys = "PgUp", .help = "Move up 20 themes." },
+ .{ .keys = "j, ↓", .help = "Move down 1 theme." },
+ .{ .keys = "ScrollDown", .help = "Move down 1 theme." },
+ .{ .keys = "PgDown", .help = "Move down 20 themes." },
+ .{ .keys = "h, x", .help = "Show palette numbers in hexadecimal." },
+ .{ .keys = "d", .help = "Show palette numbers in decimal." },
+ .{ .keys = "c", .help = "Copy theme name to the clipboard." },
+ .{ .keys = "C", .help = "Copy theme path to the clipboard." },
+ .{ .keys = "Home", .help = "Go to the start of the list." },
+ .{ .keys = "End", .help = "Go to the end of the list." },
+ .{ .keys = "/", .help = "Start search." },
+ .{ .keys = "^X, ^/", .help = "Clear search." },
+ .{ .keys = "⏎", .help = "Save theme or close search window." },
+ .{ .keys = "w", .help = "Write theme to auto config file." },
+ };
+
+ for (key_help, 0..) |help, captured_i| {
+ const i: u16 = @intCast(captured_i);
+ _ = child.printSegment(
+ .{
+ .text = help.keys,
+ .style = self.ui_standard(),
+ },
+ .{
+ .row_offset = i + 1,
+ .col_offset = 2,
+ },
+ );
+ _ = child.printSegment(
+ .{
+ .text = "—",
+ .style = self.ui_standard(),
+ },
+ .{
+ .row_offset = i + 1,
+ .col_offset = 15,
+ },
+ );
+ _ = child.printSegment(
+ .{
+ .text = help.help,
+ .style = self.ui_standard(),
+ },
+ .{
+ .row_offset = i + 1,
+ .col_offset = 17,
+ },
+ );
+ }
+ },
+ .search => {
+ const child = win.child(.{
+ .x_off = 20,
+ .y_off = win.height - 5,
+ .width = win.width - 40,
+ .height = 3,
+ .border = .{
+ .where = .all,
+ .style = self.ui_standard(),
+ },
+ });
+ child.fill(.{ .style = self.ui_standard() });
+ self.text_input.drawWithStyle(child, self.ui_standard());
+ },
+ .save => {
+ const theme = self.themes[self.filtered.items[self.current]];
+
+ const width = 92;
+ const height = 17;
+ const child = win.child(
+ .{
+ .x_off = win.width / 2 -| width / 2,
+ .y_off = win.height / 2 -| height / 2,
+ .width = width,
+ .height = height,
+ .border = .{
+ .where = .all,
+ .style = self.ui_standard(),
+ },
+ },
+ );
+
+ child.fill(.{ .style = self.ui_standard() });
+
+ const save_instructions = [_][]const u8{
+ "To apply this theme, add the following line to your Ghostty configuration:",
+ "",
+ try std.fmt.allocPrint(alloc, "theme = {s}", .{theme.theme}),
+ "",
+ "Save the configuration file and then reload it to apply the new theme.",
+ "",
+ "Or press 'w' to write an auto theme file to your system's preferred default config path.",
+ "Then add the following line to your Ghostty configuration and reload:",
+ "",
+ "config-file = ?auto/theme.ghostty",
+ "",
+ "For more details on configuration and themes, visit the Ghostty documentation:",
+ "",
+ "https://ghostty.org/docs/config/reference",
+ };
+
+ for (save_instructions, 0..) |instruction, captured_i| {
+ const i: u16 = @intCast(captured_i);
+ _ = child.printSegment(
+ .{
+ .text = instruction,
+ .style = self.ui_standard(),
+ },
+ .{
+ .row_offset = i + 1,
+ .col_offset = 2,
+ },
+ );
+ }
+ },
+ }
+ }
+
+ pub fn drawPreview(self: *Preview, alloc: std.mem.Allocator, win: vaxis.Window, x_off_unconverted: i17) !void {
+ const x_off: u16 = @intCast(x_off_unconverted);
+ const width: u16 = win.width - x_off;
+
+ if (self.filtered.items.len > 0) {
+ const theme = self.themes[self.filtered.items[self.current]];
+
+ var config = try Config.default(alloc);
+ defer config.deinit();
+
+ config.loadFile(config._arena.?.allocator(), theme.path) catch |err| {
+ const theme_path_len: u16 = @intCast(theme.path.len);
+
+ const child = win.child(
+ .{
+ .x_off = x_off,
+ .y_off = 0,
+ .width = width,
+ .height = win.height,
+ },
+ );
+ child.fill(.{ .style = self.ui_standard() });
+ const middle = child.height / 2;
+ {
+ const text = try std.fmt.allocPrint(alloc, "Unable to open {s} from:", .{theme.theme});
+ const text_len: u16 = @intCast(text.len);
+ _ = child.printSegment(
+ .{
+ .text = text,
+ .style = self.ui_err(),
+ },
+ .{
+ .row_offset = middle -| 1,
+ .col_offset = child.width / 2 -| text_len / 2,
+ },
+ );
+ }
+ {
+ _ = child.printSegment(
+ .{
+ .text = theme.path,
+ .style = self.ui_err(),
+ .link = .{
+ .uri = try theme.toUri(alloc),
+ },
+ },
+ .{
+ .row_offset = middle,
+ .col_offset = child.width / 2 -| theme_path_len / 2,
+ },
+ );
+ }
+ {
+ const text = try std.fmt.allocPrint(alloc, "{}", .{err});
+ const text_len: u16 = @intCast(text.len);
+ _ = child.printSegment(
+ .{
+ .text = text,
+ .style = self.ui_err(),
+ },
+ .{
+ .row_offset = middle + 1,
+ .col_offset = child.width / 2 -| text_len / 2,
+ },
+ );
+ }
+ return;
+ };
+
+ var next_start: u16 = 0;
+
+ const fg: vaxis.Color = .{
+ .rgb = [_]u8{
+ config.foreground.r,
+ config.foreground.g,
+ config.foreground.b,
+ },
+ };
+ const bg: vaxis.Color = .{
+ .rgb = [_]u8{
+ config.background.r,
+ config.background.g,
+ config.background.b,
+ },
+ };
+ const cursor_fg: vaxis.Color = if (config.@"cursor-text") |cursor_text| .{
+ .rgb = [_]u8{
+ cursor_text.color.r,
+ cursor_text.color.g,
+ cursor_text.color.b,
+ },
+ } else bg;
+ const cursor_bg: vaxis.Color = if (config.@"cursor-color") |cursor_bg| .{
+ .rgb = [_]u8{
+ cursor_bg.color.r,
+ cursor_bg.color.g,
+ cursor_bg.color.b,
+ },
+ } else fg;
+ const selection_fg: vaxis.Color = if (config.@"selection-foreground") |selection_fg| .{
+ .rgb = [_]u8{
+ selection_fg.color.r,
+ selection_fg.color.g,
+ selection_fg.color.b,
+ },
+ } else bg;
+ const selection_bg: vaxis.Color = if (config.@"selection-background") |selection_bg| .{
+ .rgb = [_]u8{
+ selection_bg.color.r,
+ selection_bg.color.g,
+ selection_bg.color.b,
+ },
+ } else fg;
+ const cursor: vaxis.Style = .{
+ .fg = cursor_fg,
+ .bg = cursor_bg,
+ };
+ const standard_selection: vaxis.Style = .{
+ .fg = selection_fg,
+ .bg = selection_bg,
+ };
+ const standard: vaxis.Style = .{
+ .fg = fg,
+ .bg = bg,
+ };
+ const standard_bold: vaxis.Style = .{
+ .fg = fg,
+ .bg = bg,
+ .bold = true,
+ };
+ const standard_italic: vaxis.Style = .{
+ .fg = fg,
+ .bg = bg,
+ .italic = true,
+ };
+ const standard_bold_italic: vaxis.Style = .{
+ .fg = fg,
+ .bg = bg,
+ .bold = true,
+ .italic = true,
+ };
+ const standard_underline: vaxis.Style = .{
+ .fg = fg,
+ .bg = bg,
+ .ul_style = .single,
+ };
+ const standard_double_underline: vaxis.Style = .{
+ .fg = fg,
+ .bg = bg,
+ .ul_style = .double,
+ };
+ const standard_dashed_underline: vaxis.Style = .{
+ .fg = fg,
+ .bg = bg,
+ .ul_style = .dashed,
+ };
+ const standard_curly_underline: vaxis.Style = .{
+ .fg = fg,
+ .bg = bg,
+ .ul_style = .curly,
+ };
+ const standard_dotted_underline: vaxis.Style = .{
+ .fg = fg,
+ .bg = bg,
+ .ul_style = .dotted,
+ };
+
+ {
+ const theme_len: u16 = @intCast(theme.theme.len);
+ const theme_path_len: u16 = @intCast(theme.path.len);
+ const child = win.child(
+ .{
+ .x_off = x_off,
+ .y_off = next_start,
+ .width = width,
+ .height = 4,
+ },
+ );
+ child.fill(.{ .style = standard });
+ _ = child.printSegment(
+ .{
+ .text = theme.theme,
+ .style = standard_bold_italic,
+ .link = .{
+ .uri = try theme.toUri(alloc),
+ },
+ },
+ .{
+ .row_offset = 1,
+ .col_offset = child.width / 2 -| theme_len / 2,
+ },
+ );
+ _ = child.printSegment(
+ .{
+ .text = theme.path,
+ .style = standard,
+ .link = .{
+ .uri = try theme.toUri(alloc),
+ },
+ },
+ .{
+ .row_offset = 2,
+ .col_offset = child.width / 2 -| theme_path_len / 2,
+ .wrap = .none,
+ },
+ );
+ next_start += child.height;
+ }
+
+ if (config._diagnostics.items().len > 0) {
+ const diagnostic_items_len: u16 = @intCast(config._diagnostics.items().len);
+ const child = win.child(
+ .{
+ .x_off = x_off,
+ .y_off = next_start,
+ .width = width,
+ .height = if (config._diagnostics.items().len == 0) 0 else 2 + diagnostic_items_len,
+ },
+ );
+ {
+ const text = "Problems were encountered trying to load the theme:";
+ const text_len: u16 = @intCast(text.len);
+ _ = child.printSegment(
+ .{
+ .text = text,
+ .style = self.ui_err(),
+ },
+ .{
+ .row_offset = 0,
+ .col_offset = child.width / 2 -| (text_len / 2),
+ },
+ );
+ }
+
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ defer buf.deinit();
+ for (config._diagnostics.items(), 0..) |diag, captured_i| {
+ const i: u16 = @intCast(captured_i);
+ try diag.format(&buf.writer);
+ _ = child.printSegment(
+ .{
+ .text = buf.written(),
+ .style = self.ui_err(),
+ },
+ .{
+ .row_offset = 2 + i,
+ .col_offset = 2,
+ },
+ );
+ buf.clearRetainingCapacity();
+ }
+ next_start += child.height;
+ }
+ {
+ const child = win.child(.{
+ .x_off = x_off,
+ .y_off = next_start,
+ .width = width,
+ .height = 6,
+ });
+
+ child.fill(.{ .style = standard });
+
+ for (0..16) |captured_i| {
+ const i: u16 = @intCast(captured_i);
+ const r = i / 8;
+ const c = i % 8;
+ const text = if (self.hex)
+ try std.fmt.allocPrint(alloc, " {x:0>2}", .{i})
+ else
+ try std.fmt.allocPrint(alloc, "{d:3}", .{i});
+ _ = child.printSegment(
+ .{
+ .text = text,
+ .style = standard,
+ },
+ .{
+ .row_offset = 3 * r,
+ .col_offset = c * 8,
+ },
+ );
+ _ = child.printSegment(
+ .{
+ .text = "████",
+ .style = .{
+ .fg = color(config, i),
+ .bg = bg,
+ },
+ },
+ .{
+ .row_offset = 3 * r,
+ .col_offset = 4 + c * 8,
+ },
+ );
+ _ = child.printSegment(
+ .{
+ .text = "████",
+ .style = .{
+ .fg = color(config, i),
+ .bg = bg,
+ },
+ },
+ .{
+ .row_offset = 3 * r + 1,
+ .col_offset = 4 + c * 8,
+ },
+ );
+ }
+ next_start += child.height;
+ }
+ {
+ const child = win.child(
+ .{
+ .x_off = x_off,
+ .y_off = next_start,
+ .width = width,
+ .height = 24,
+ },
+ );
+ const bold: vaxis.Style = .{
+ .fg = fg,
+ .bg = bg,
+ .bold = true,
+ };
+ const color1: vaxis.Style = .{
+ .fg = color(config, 1),
+ .bg = bg,
+ };
+ const color2: vaxis.Style = .{
+ .fg = color(config, 2),
+ .bg = bg,
+ };
+ const color3: vaxis.Style = .{
+ .fg = color(config, 3),
+ .bg = bg,
+ };
+ const color4: vaxis.Style = .{
+ .fg = color(config, 4),
+ .bg = bg,
+ };
+ const color5: vaxis.Style = .{
+ .fg = color(config, 5),
+ .bg = bg,
+ };
+ const color6: vaxis.Style = .{
+ .fg = color(config, 6),
+ .bg = bg,
+ };
+ const color6ul: vaxis.Style = .{
+ .fg = color(config, 6),
+ .bg = bg,
+ .ul_style = .single,
+ };
+ const color10: vaxis.Style = .{
+ .fg = color(config, 10),
+ .bg = bg,
+ };
+ const color12: vaxis.Style = .{
+ .fg = color(config, 12),
+ .bg = bg,
+ };
+ const color238: vaxis.Style = .{
+ .fg = color(config, 238),
+ .bg = bg,
+ };
+ child.fill(.{ .style = standard });
+ _ = child.print(
+ &.{
+ .{ .text = "→", .style = color2 },
+ .{ .text = " ", .style = standard },
+ .{ .text = "bat", .style = color4 },
+ .{ .text = " ", .style = standard },
+ .{ .text = "ziggzagg.zig", .style = color6ul },
+ },
+ .{
+ .row_offset = 0,
+ .col_offset = 2,
+ },
+ );
+ {
+ _ = child.print(
+ &.{
+ .{
+ .text = "───────┬",
+ .style = color238,
+ },
+ },
+ .{
+ .row_offset = 1,
+ .col_offset = 2,
+ },
+ );
+ if (child.width > 10) {
+ for (10..child.width) |captured_col| {
+ const col: u16 = @intCast(captured_col);
+ _ = child.print(
+ &.{
+ .{
+ .text = "─",
+ .style = color238,
+ },
+ },
+ .{
+ .row_offset = 1,
+ .col_offset = col,
+ },
+ );
+ }
+ }
+ }
+ _ = child.print(
+ &.{
+ .{
+ .text = " │ ",
+ .style = color238,
+ },
+
+ .{
+ .text = "File: ",
+ .style = standard,
+ },
+
+ .{
+ .text = "ziggzagg.zig",
+ .style = bold,
+ },
+ },
+ .{
+ .row_offset = 2,
+ .col_offset = 2,
+ },
+ );
+ {
+ _ = child.print(
+ &.{
+ .{
+ .text = "───────┼",
+ .style = color238,
+ },
+ },
+ .{
+ .row_offset = 3,
+ .col_offset = 2,
+ },
+ );
+ if (child.width > 10) {
+ for (10..child.width) |captured_col| {
+ const col: u16 = @intCast(captured_col);
+ _ = child.print(
+ &.{
+ .{
+ .text = "─",
+ .style = color238,
+ },
+ },
+ .{
+ .row_offset = 3,
+ .col_offset = col,
+ },
+ );
+ }
+ }
+ }
+ _ = child.print(
+ &.{
+ .{ .text = " 1 │ ", .style = color238 },
+ .{ .text = "const", .style = color5 },
+ .{ .text = " std ", .style = standard },
+ .{ .text = "= @import", .style = color5 },
+ .{ .text = "(", .style = standard },
+ .{ .text = "\"std\"", .style = color10 },
+ .{ .text = ");", .style = standard },
+ },
+ .{
+ .row_offset = 4,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 2 │", .style = color238 },
+ },
+ .{
+ .row_offset = 5,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 3 │ ", .style = color238 },
+ .{ .text = "pub ", .style = color5 },
+ .{ .text = "fn ", .style = color12 },
+ .{ .text = "main", .style = color2 },
+ .{ .text = "() ", .style = standard },
+ .{ .text = "!", .style = color5 },
+ .{ .text = "void", .style = color12 },
+ .{ .text = " {", .style = standard },
+ },
+ .{
+ .row_offset = 6,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 4 │ ", .style = color238 },
+ .{ .text = "const ", .style = color5 },
+ .{ .text = "stdout ", .style = standard },
+ .{ .text = "=", .style = color5 },
+ .{ .text = " std.Io.getStdOut().writer();", .style = standard },
+ },
+ .{
+ .row_offset = 7,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 5 │ ", .style = color238 },
+ .{ .text = "var ", .style = color5 },
+ .{ .text = "i:", .style = standard },
+ .{ .text = " usize", .style = color12 },
+ .{ .text = " =", .style = color5 },
+ .{ .text = " 1", .style = color4 },
+ .{ .text = ";", .style = standard },
+ },
+ .{
+ .row_offset = 8,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 6 │ ", .style = color238 },
+ .{ .text = "while ", .style = color5 },
+ .{ .text = "(i ", .style = standard },
+ .{ .text = "<= ", .style = color5 },
+ .{ .text = "16", .style = color4 },
+ .{ .text = ") : (i ", .style = standard },
+ .{ .text = "+= ", .style = color5 },
+ .{ .text = "1", .style = color4 },
+ .{ .text = ") {", .style = standard },
+ },
+ .{
+ .row_offset = 9,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 7 │ ", .style = color238 },
+ .{ .text = "if ", .style = color5 },
+ .{ .text = "(i ", .style = standard },
+ .{ .text = "% ", .style = color5 },
+ .{ .text = "15 ", .style = color4 },
+ .{ .text = "== ", .style = color5 },
+ .{ .text = "0", .style = color4 },
+ .{ .text = ") {", .style = standard },
+ },
+ .{
+ .row_offset = 10,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 8 │ ", .style = color238 },
+ .{ .text = "try ", .style = color5 },
+ .{ .text = "stdout.writeAll(", .style = standard },
+ .{ .text = "\"ZiggZagg", .style = color10 },
+ .{ .text = "\\n", .style = color12 },
+ .{ .text = "\"", .style = color10 },
+ .{ .text = ");", .style = standard },
+ },
+ .{
+ .row_offset = 11,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 9 │ ", .style = color238 },
+ .{ .text = "} ", .style = standard },
+ .{ .text = "else if ", .style = color5 },
+ .{ .text = "(i ", .style = standard },
+ .{ .text = "% ", .style = color5 },
+ .{ .text = "3 ", .style = color4 },
+ .{ .text = "== ", .style = color5 },
+ .{ .text = "0", .style = color4 },
+ .{ .text = ") {", .style = standard },
+ },
+ .{
+ .row_offset = 12,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 10 │ ", .style = color238 },
+ .{ .text = "try ", .style = color5 },
+ .{ .text = "stdout.writeAll(", .style = standard },
+ .{ .text = "\"Zigg", .style = color10 },
+ .{ .text = "\\n", .style = color12 },
+ .{ .text = "\"", .style = color10 },
+ .{ .text = ");", .style = standard },
+ },
+ .{
+ .row_offset = 13,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 11 │ ", .style = color238 },
+ .{ .text = "} ", .style = standard },
+ .{ .text = "else if ", .style = color5 },
+ .{ .text = "(i ", .style = standard },
+ .{ .text = "% ", .style = color5 },
+ .{ .text = "5 ", .style = color4 },
+ .{ .text = "== ", .style = color5 },
+ .{ .text = "0", .style = color4 },
+ .{ .text = ") {", .style = standard },
+ },
+ .{
+ .row_offset = 14,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 12 │ ", .style = color238 },
+ .{ .text = "try ", .style = color5 },
+ .{ .text = "stdout.writeAll(", .style = standard },
+ .{ .text = "\"Zagg", .style = color10 },
+ .{ .text = "\\n", .style = color12 },
+ .{ .text = "\"", .style = color10 },
+ .{ .text = ");", .style = standard },
+ },
+ .{
+ .row_offset = 15,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 13 │ ", .style = color238 },
+ .{ .text = "} ", .style = standard },
+ .{ .text = "else ", .style = color5 },
+ .{ .text = "{", .style = standard },
+ },
+ .{
+ .row_offset = 16,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 14 │ ", .style = color238 },
+ .{ .text = "try ", .style = color5 },
+ .{ .text = "stdout.print(\"{d}\\n\", .{i})", .style = standard_selection },
+ .{ .text = ";", .style = cursor },
+ },
+ .{
+ .row_offset = 17,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 15 │ ", .style = color238 },
+ .{ .text = "}", .style = standard },
+ },
+ .{
+ .row_offset = 18,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 16 │ ", .style = color238 },
+ .{ .text = "}", .style = standard },
+ },
+ .{
+ .row_offset = 19,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = " 17 │ ", .style = color238 },
+ .{ .text = "}", .style = standard },
+ },
+ .{
+ .row_offset = 20,
+ .col_offset = 2,
+ },
+ );
+ {
+ _ = child.print(
+ &.{
+ .{
+ .text = "───────┴",
+ .style = color238,
+ },
+ },
+ .{
+ .row_offset = 21,
+ .col_offset = 2,
+ },
+ );
+ if (child.width > 10) {
+ for (10..child.width) |captured_col| {
+ const col: u16 = @intCast(captured_col);
+ _ = child.print(
+ &.{
+ .{
+ .text = "─",
+ .style = color238,
+ },
+ },
+ .{
+ .row_offset = 21,
+ .col_offset = col,
+ },
+ );
+ }
+ }
+ }
+ _ = child.print(
+ &.{
+ .{ .text = "ghostty ", .style = color6 },
+ .{ .text = "on ", .style = standard },
+ .{ .text = " main ", .style = color4 },
+ .{ .text = "[+] ", .style = color1 },
+ .{ .text = "via ", .style = standard },
+ .{ .text = " v0.13.0 ", .style = color3 },
+ .{ .text = "via ", .style = standard },
+ .{ .text = " impure (ghostty-env)", .style = color4 },
+ },
+ .{
+ .row_offset = 22,
+ .col_offset = 2,
+ },
+ );
+ _ = child.print(
+ &.{
+ .{ .text = "✦ ", .style = color4 },
+ .{ .text = "at ", .style = standard },
+ .{ .text = "10:36:15 ", .style = color3 },
+ .{ .text = "→", .style = color2 },
+ },
+ .{
+ .row_offset = 23,
+ .col_offset = 2,
+ },
+ );
+ next_start += child.height;
+ }
+ if (next_start < win.height) {
+ const child = win.child(
+ .{
+ .x_off = x_off,
+ .y_off = next_start,
+ .width = width,
+ .height = win.height - next_start,
+ },
+ );
+ child.fill(.{ .style = standard });
+ var it = std.mem.splitAny(u8, lorem_ipsum, " \n");
+ var row: u16 = 1;
+ var col: u16 = 2;
+ while (row < child.height) {
+ const word = it.next() orelse line: {
+ it.reset();
+ break :line it.next() orelse unreachable;
+ };
+ const word_len: u16 = @intCast(word.len);
+ if (col + word.len > child.width) {
+ row += 1;
+ col = 2;
+ }
+ const style: vaxis.Style = style: {
+ if (std.mem.eql(u8, "ipsum", word)) break :style .{ .fg = color(config, 2), .bg = bg };
+ if (std.mem.eql(u8, "consectetur", word)) break :style standard_bold;
+ if (std.mem.eql(u8, "reprehenderit", word)) break :style standard_italic;
+ if (std.mem.eql(u8, "Praesent", word)) break :style standard_bold_italic;
+ if (std.mem.eql(u8, "auctor", word)) break :style standard_underline;
+ if (std.mem.eql(u8, "dui", word)) break :style standard_double_underline;
+ if (std.mem.eql(u8, "erat", word)) break :style standard_dashed_underline;
+ if (std.mem.eql(u8, "enim", word)) break :style standard_dotted_underline;
+ if (std.mem.eql(u8, "odio", word)) break :style standard_curly_underline;
+ break :style standard;
+ };
+ _ = child.printSegment(
+ .{
+ .text = word,
+ .style = style,
+ },
+ .{
+ .row_offset = row,
+ .col_offset = col,
+ },
+ );
+ col += word_len + 1;
+ }
+ }
+ } else {
+ const child = win.child(.{
+ .x_off = 0,
+ .y_off = 0,
+ .width = win.width,
+ .height = win.height,
+ });
+ child.fill(.{
+ .style = self.ui_standard(),
+ });
+
+ _ = child.printSegment(.{
+ .text = "No theme found!",
+ .style = self.ui_standard(),
+ }, .{
+ .row_offset = win.height / 2 - 1,
+ .col_offset = win.width / 2 - 7,
+ });
+ }
+ }
+
+ fn saveSelectedTheme(self: *Preview) void {
+ if (self.filtered.items.len == 0)
+ return;
+
+ const idx = self.filtered.items[self.current];
+ const theme = self.themes[idx];
+
+ writeAutoThemeFile(self.allocator, theme.theme) catch {
+ return;
+ };
+ }
+};
+
+fn color(config: Config, palette: usize) vaxis.Color {
+ return .{
+ .rgb = [_]u8{
+ config.palette.value[palette].r,
+ config.palette.value[palette].g,
+ config.palette.value[palette].b,
+ },
+ };
+}
+
+const lorem_ipsum = @embedFile("lorem_ipsum.txt");
+
+fn preview(allocator: std.mem.Allocator, themes: []ThemeListElement, theme_filter: ColorScheme) !void {
+ var buf: [4096]u8 = undefined;
+ var app = try Preview.init(
+ allocator,
+ themes,
+ theme_filter,
+ &buf,
+ );
+ defer app.deinit();
+ try app.run();
+}
+
+fn shouldIncludeTheme(theme_filter: ColorScheme, theme_config: Config) bool {
+ const rf = @as(f32, @floatFromInt(theme_config.background.r)) / 255.0;
+ const gf = @as(f32, @floatFromInt(theme_config.background.g)) / 255.0;
+ const bf = @as(f32, @floatFromInt(theme_config.background.b)) / 255.0;
+ const luminance = 0.2126 * rf + 0.7152 * gf + 0.0722 * bf;
+ const is_dark = luminance < 0.5;
+ return (theme_filter == .all) or (theme_filter == .dark and is_dark) or (theme_filter == .light and !is_dark);
+}