ghostty/src/cli/list_themes.zig
Jeffrey C. Ollie 9f543ceac2 cli: fix integer overflow in +list-themes if window is too narrow
Reproduction is to resize the window to it's minimum width and then
run `ghostty +list-themes`. Ghostty will crash because Zig for loops
don't like having a range where the end is smaller than the start.
2024-09-28 12:12:00 -05:00

1508 lines
53 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const std = @import("std");
const inputpkg = @import("../input.zig");
const args = @import("args.zig");
const Action = @import("action.zig").Action;
const Config = @import("../config/Config.zig");
const themepkg = @import("../config/theme.zig");
const tui = @import("tui.zig");
const internal_os = @import("../os/main.zig");
const global_state = &@import("../global.zig").state;
const vaxis = @import("vaxis");
const zf = @import("zf");
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,
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.ArrayList(u8).init(alloc);
errdefer buf.deinit();
try uri.writeToStream(.{ .scheme = true, .authority = true, .path = true }, buf.writer());
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_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:
///
/// * `--path`: Show the full path to the theme.
/// * `--plain`: Force a plain listing of themes.
pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
var opts: Options = .{};
defer opts.deinit();
{
var iter = try std.process.argsWithAllocator(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();
const stderr = std.io.getStdErr().writer();
const stdout = std.io.getStdOut().writer();
if (global_state.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).init(alloc);
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 => {
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),
});
},
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 std.posix.isatty(std.io.getStdOut().handle)) {
try preview(gpa_alloc, themes.items);
return 0;
}
for (themes.items) |theme| {
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) });
}
return 0;
}
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,
},
color_scheme: vaxis.Color.Scheme,
text_input: vaxis.widgets.TextInput,
pub fn init(allocator: std.mem.Allocator, themes: []ThemeListElement) !*Preview {
const self = try allocator.create(Preview);
self.* = .{
.allocator = allocator,
.should_quit = false,
.tty = try vaxis.Tty.init(),
.vx = try vaxis.init(allocator, .{}),
.mouse = null,
.themes = themes,
.filtered = try std.ArrayList(usize).initCapacity(allocator, themes.len),
.current = 0,
.window = 0,
.hex = false,
.mode = .normal,
.color_scheme = .light,
.text_input = vaxis.widgets.TextInput.init(allocator, &self.vx.unicode),
};
for (0..themes.len) |i| {
try self.filtered.append(i);
}
return self;
}
pub fn deinit(self: *Preview) void {
const allocator = self.allocator;
self.filtered.deinit();
self.text_input.deinit();
self.vx.deinit(allocator, self.tty.anyWriter());
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();
try self.vx.enterAltScreen(self.tty.anyWriter());
try self.vx.setTitle(self.tty.anyWriter(), "👻 Ghostty Theme Preview 👻");
try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s);
try self.vx.setMouseMode(self.tty.anyWriter(), true);
if (self.vx.caps.color_scheme_updates)
try self.vx.subscribeToColorSchemeUpdates(self.tty.anyWriter());
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);
var buffered = self.tty.bufferedWriter();
try self.vx.render(buffered.writer().any());
try buffered.flush();
}
}
fn updateFiltered(self: *Preview) !void {
const relative = self.current -| self.window;
const 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();
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).init(self.allocator);
defer tokens.deinit();
var it = std.mem.tokenizeScalar(u8, string, ' ');
while (it.next()) |token| try tokens.append(token);
for (self.themes, 0..) |*theme, i| {
theme.rank = zf.rank(theme.theme, tokens.items, false, true);
if (theme.rank) |_|
try self.filtered.append(i);
}
} else {
for (self.themes, 0..) |*theme, i| {
try self.filtered.append(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: {
for (self.filtered.items, 0..) |index, i| {
if (std.mem.eql(u8, self.themes[index].theme, selected))
break :current .{ i, i -| relative };
}
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(&.{ '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.anyWriter(),
self.themes[self.filtered.items[self.current]].theme,
alloc,
);
if (key.matches('c', .{ .shift = true }))
try self.vx.copyToSystemClipboard(
self.tty.anyWriter(),
self.themes[self.filtered.items[self.current]].path,
alloc,
);
},
.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();
},
}
},
.color_scheme => |color_scheme| self.color_scheme = color_scheme,
.mouse => |mouse| self.mouse = mouse,
.winsize => |ws| try self.vx.resize(self.allocator, self.tty.anyWriter(), 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 = .{ .limit = 32 },
.height = .{ .limit = win.height },
});
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;
}
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) {
self.current = self.window + mouse.row;
}
highlight = mouse.row;
}
}
}
theme_list.fill(.{ .style = self.ui_standard() });
for (0..theme_list.height) |row| {
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) {
_ = try theme_list.printSegment(
.{
.text = " ",
.style = self.ui_selected(),
},
.{
.row_offset = row,
.col_offset = 0,
},
);
}
_ = try 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|
_ = try theme_list.printSegment(
.{
.text = " ",
.style = self.ui_selected(),
},
.{
.row_offset = row,
.col_offset = i,
},
);
}
_ = try 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 = 20;
const child = win.child(
.{
.x_off = win.width / 2 -| width / 2,
.y_off = win.height / 2 -| height / 2,
.width = .{
.limit = width,
},
.height = .{
.limit = 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 = "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 = "Close search window." },
};
for (key_help, 0..) |help, i| {
_ = try child.printSegment(
.{
.text = help.keys,
.style = self.ui_standard(),
},
.{
.row_offset = i + 1,
.col_offset = 2,
},
);
_ = try child.printSegment(
.{
.text = "",
.style = self.ui_standard(),
},
.{
.row_offset = i + 1,
.col_offset = 15,
},
);
_ = try 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 = .{
.limit = win.width - 40,
},
.height = .{
.limit = 3,
},
.border = .{
.where = .all,
.style = self.ui_standard(),
},
});
child.fill(.{ .style = self.ui_standard() });
self.text_input.drawWithStyle(child, self.ui_standard());
},
}
}
pub fn drawPreview(self: *Preview, alloc: std.mem.Allocator, win: vaxis.Window, x_off: usize) !void {
const width = win.width - x_off;
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 child = win.child(
.{
.x_off = x_off,
.y_off = 0,
.width = .{
.limit = width,
},
.height = .{
.limit = 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});
_ = try child.printSegment(
.{
.text = text,
.style = self.ui_err(),
},
.{
.row_offset = middle -| 1,
.col_offset = child.width / 2 -| text.len / 2,
},
);
}
{
_ = try 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});
_ = try child.printSegment(
.{
.text = text,
.style = self.ui_err(),
},
.{
.row_offset = middle + 1,
.col_offset = child.width / 2 -| text.len / 2,
},
);
}
return;
};
var next_start: usize = 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 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 child = win.child(
.{
.x_off = x_off,
.y_off = next_start,
.width = .{
.limit = width,
},
.height = .{
.limit = 4,
},
},
);
child.fill(.{ .style = standard });
_ = try child.printSegment(
.{
.text = theme.theme,
.style = standard_bold_italic,
.link = .{
.uri = try theme.toUri(alloc),
},
},
.{
.row_offset = 1,
.col_offset = child.width / 2 -| theme.theme.len / 2,
},
);
_ = try 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._errors.empty()) {
const child = win.child(
.{
.x_off = x_off,
.y_off = next_start,
.width = .{
.limit = width,
},
.height = .{
.limit = if (config._errors.empty()) 0 else 2 + config._errors.list.items.len,
},
},
);
{
const text = "Problems were encountered trying to load the theme:";
_ = try child.printSegment(
.{
.text = text,
.style = self.ui_err(),
},
.{
.row_offset = 0,
.col_offset = child.width / 2 -| (text.len / 2),
},
);
}
for (config._errors.list.items, 0..) |err, i| {
_ = try child.printSegment(
.{
.text = err.message,
.style = self.ui_err(),
},
.{
.row_offset = 2 + i,
.col_offset = 2,
},
);
}
next_start += child.height;
}
{
const child = win.child(.{
.x_off = x_off,
.y_off = next_start,
.width = .{
.limit = width,
},
.height = .{
.limit = 6,
},
});
child.fill(.{ .style = standard });
for (0..16) |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});
_ = try child.printSegment(
.{
.text = text,
.style = standard,
},
.{
.row_offset = 3 * r,
.col_offset = c * 8,
},
);
_ = try child.printSegment(
.{
.text = "████",
.style = .{
.fg = color(config, i),
.bg = bg,
},
},
.{
.row_offset = 3 * r,
.col_offset = 4 + c * 8,
},
);
_ = try 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 = .{
.limit = width,
},
.height = .{
.limit = 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 });
_ = try 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,
},
);
{
_ = try child.print(
&.{
.{
.text = "───────┬",
.style = color238,
},
},
.{
.row_offset = 1,
.col_offset = 2,
},
);
if (child.width > 10) {
for (10..child.width) |col| {
_ = try child.print(
&.{
.{
.text = "",
.style = color238,
},
},
.{
.row_offset = 1,
.col_offset = col,
},
);
}
}
}
_ = try child.print(
&.{
.{
.text = "",
.style = color238,
},
.{
.text = "File: ",
.style = standard,
},
.{
.text = "ziggzag.zig",
.style = bold,
},
},
.{
.row_offset = 2,
.col_offset = 2,
},
);
{
_ = try child.print(
&.{
.{
.text = "───────┼",
.style = color238,
},
},
.{
.row_offset = 3,
.col_offset = 2,
},
);
if (child.width > 10) {
for (10..child.width) |col| {
_ = try child.print(
&.{
.{
.text = "",
.style = color238,
},
},
.{
.row_offset = 3,
.col_offset = col,
},
);
}
}
}
_ = try 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,
},
);
_ = try child.print(
&.{
.{ .text = " 2 │", .style = color238 },
},
.{
.row_offset = 5,
.col_offset = 2,
},
);
_ = try 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,
},
);
_ = try 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,
},
);
_ = try 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,
},
);
_ = try 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,
},
);
_ = try 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,
},
);
_ = try 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,
},
);
_ = try 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,
},
);
_ = try 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,
},
);
_ = try 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,
},
);
_ = try 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,
},
);
_ = try child.print(
&.{
.{ .text = " 13 │ ", .style = color238 },
.{ .text = "} ", .style = standard },
.{ .text = "else ", .style = color5 },
.{ .text = "{", .style = standard },
},
.{
.row_offset = 16,
.col_offset = 2,
},
);
_ = try child.print(
&.{
.{ .text = " 14 │ ", .style = color238 },
.{ .text = "try ", .style = color5 },
.{ .text = "stdout.print(", .style = standard },
.{ .text = "\"{d}", .style = color10 },
.{ .text = "\\n", .style = color12 },
.{ .text = "\"", .style = color10 },
.{ .text = ", .{i});", .style = standard },
},
.{
.row_offset = 17,
.col_offset = 2,
},
);
_ = try child.print(
&.{
.{ .text = " 15 │ ", .style = color238 },
.{ .text = "}", .style = standard },
},
.{
.row_offset = 18,
.col_offset = 2,
},
);
_ = try child.print(
&.{
.{ .text = " 16 │ ", .style = color238 },
.{ .text = "}", .style = standard },
},
.{
.row_offset = 19,
.col_offset = 2,
},
);
_ = try child.print(
&.{
.{ .text = " 17 │ ", .style = color238 },
.{ .text = "}", .style = standard },
},
.{
.row_offset = 20,
.col_offset = 2,
},
);
{
_ = try child.print(
&.{
.{
.text = "───────┴",
.style = color238,
},
},
.{
.row_offset = 21,
.col_offset = 2,
},
);
if (child.width > 10) {
for (10..child.width) |col| {
_ = try child.print(
&.{
.{
.text = "",
.style = color238,
},
},
.{
.row_offset = 21,
.col_offset = col,
},
);
}
}
}
_ = try 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,
},
);
_ = try 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 = .{
.limit = width,
},
.height = .{
.limit = win.height - next_start,
},
},
);
child.fill(.{ .style = standard });
var it = std.mem.splitAny(u8, lorem_ipsum, " \n");
var row: usize = 1;
var col: usize = 2;
while (row < child.height) {
const word = it.next() orelse line: {
it.reset();
break :line it.next() orelse unreachable;
};
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;
};
_ = try child.printSegment(
.{
.text = word,
.style = style,
},
.{
.row_offset = row,
.col_offset = col,
},
);
col += word.len + 1;
}
}
}
};
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) !void {
var app = try Preview.init(allocator, themes);
defer app.deinit();
try app.run();
}