feat: support adding entirely new themes via the config

This commit is contained in:
CJ van den Berg 2026-03-31 20:58:00 +02:00
parent d53d155c6d
commit 310221bb26
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
5 changed files with 126 additions and 71 deletions

View file

@ -895,15 +895,15 @@ pub fn write_theme(theme_name: []const u8, content: []const u8) !void {
pub fn list_themes(allocator: std.mem.Allocator) ![]const []const u8 { pub fn list_themes(allocator: std.mem.Allocator) ![]const []const u8 {
var dir = try std.fs.openDirAbsolute(try get_theme_directory(), .{ .iterate = true }); var dir = try std.fs.openDirAbsolute(try get_theme_directory(), .{ .iterate = true });
defer dir.close(); defer dir.close();
var result = std.ArrayList([]const u8).init(allocator); var result: std.ArrayList([]const u8) = .empty;
var iter = dir.iterateAssumeFirstIteration(); var iter = dir.iterateAssumeFirstIteration();
while (try iter.next()) |entry| { while (try iter.next()) |entry| {
switch (entry.kind) { switch (entry.kind) {
.file, .sym_link => try result.append(try allocator.dupe(u8, std.fs.path.stem(entry.name))), .file, .sym_link => try result.append(allocator, try allocator.dupe(u8, std.fs.path.stem(entry.name))),
else => continue, else => continue,
} }
} }
return result.toOwnedSlice(); return result.toOwnedSlice(allocator);
} }
pub fn get_config_dir() ConfigDirError![]const u8 { pub fn get_config_dir() ConfigDirError![]const u8 {

View file

@ -29,6 +29,7 @@ pub const root = struct {
pub const read_theme = if (@hasDecl(hard_root, "read_theme")) hard_root.read_theme else dummy.read_theme; pub const read_theme = if (@hasDecl(hard_root, "read_theme")) hard_root.read_theme else dummy.read_theme;
pub const write_theme = if (@hasDecl(hard_root, "write_theme")) hard_root.write_theme else dummy.write_theme; pub const write_theme = if (@hasDecl(hard_root, "write_theme")) hard_root.write_theme else dummy.write_theme;
pub const list_themes = if (@hasDecl(hard_root, "list_themes")) hard_root.list_themes else dummy.list_themes;
pub const get_theme_file_name = if (@hasDecl(hard_root, "get_theme_file_name")) hard_root.get_theme_file_name else dummy.get_theme_file_name; pub const get_theme_file_name = if (@hasDecl(hard_root, "get_theme_file_name")) hard_root.get_theme_file_name else dummy.get_theme_file_name;
pub const exit = if (@hasDecl(hard_root, "exit")) hard_root.exit else dummy.exit; pub const exit = if (@hasDecl(hard_root, "exit")) hard_root.exit else dummy.exit;
@ -109,6 +110,10 @@ const dummy = struct {
pub fn write_theme(_: []const u8, _: []const u8) !void { pub fn write_theme(_: []const u8, _: []const u8) !void {
@panic("dummy write_theme call"); @panic("dummy write_theme call");
} }
pub fn list_themes(_: std.mem.Allocator) ![]const []const u8 {
@panic("dummy list_themes call");
}
pub fn get_theme_file_name(_: []const u8) ![]const u8 { pub fn get_theme_file_name(_: []const u8) ![]const u8 {
@panic("dummy get_theme_file_name call"); @panic("dummy get_theme_file_name call");
} }

View file

@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const tp = @import("thespian"); const tp = @import("thespian");
const root = @import("soft_root").root;
const Plane = @import("renderer").Plane; const Plane = @import("renderer").Plane;
const EventHandler = @import("EventHandler"); const EventHandler = @import("EventHandler");
@ -9,7 +10,6 @@ const tui = @import("tui.zig");
pub const Box = @import("Box.zig"); pub const Box = @import("Box.zig");
pub const Pos = struct { y: i32 = 0, x: i32 = 0 }; pub const Pos = struct { y: i32 = 0, x: i32 = 0 };
pub const Theme = @import("theme"); pub const Theme = @import("theme");
pub const themes = @import("themes").themes;
pub const scopes = @import("themes").scopes; pub const scopes = @import("themes").scopes;
pub const Type = @import("config").WidgetType; pub const Type = @import("config").WidgetType;
pub const StyleTag = @import("config").WidgetStyle; pub const StyleTag = @import("config").WidgetStyle;
@ -42,6 +42,114 @@ pub const Layout = union(enum) {
} }
}; };
pub const ThemeInfo = struct {
name: []const u8,
storage: ?std.json.Parsed(Theme) = null,
pub fn get(self: *@This(), allocator: std.mem.Allocator) ?Theme {
if (load_theme_file(allocator, self.name) catch null) |parsed_theme| {
self.storage = parsed_theme;
return self.storage.?.value;
}
for (static_themes) |theme_| {
if (std.mem.eql(u8, theme_.name, self.name))
return theme_;
}
return null;
}
fn load_theme_file(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) {
return load_theme_file_internal(allocator, theme_name) catch |e| {
std.log.err("Error loading theme '{s}' from file: {t}", .{ theme_name, e });
return e;
};
}
fn load_theme_file_internal(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) {
const json_str = root.read_theme(allocator, theme_name) orelse return null;
defer allocator.free(json_str);
return try std.json.parseFromSlice(Theme, allocator, json_str, .{ .allocate = .alloc_always });
}
};
var themes_: ?std.StringHashMap(*ThemeInfo) = null;
var theme_names_: ?[]const []const u8 = null;
const static_themes = @import("themes").themes;
fn get_themes(allocator: std.mem.Allocator) *std.StringHashMap(*ThemeInfo) {
if (themes_) |*themes__| return themes__;
const theme_files = root.list_themes(allocator) catch @panic("OOM get_themes");
var themes: std.StringHashMap(*ThemeInfo) = .init(allocator);
defer allocator.free(theme_files);
for (theme_files) |file| {
const theme_info = allocator.create(ThemeInfo) catch @panic("OOM get_themes");
theme_info.* = .{
.name = file,
};
themes.put(theme_info.name, theme_info) catch @panic("OOM get_themes");
}
for (static_themes) |theme_| if (!themes.contains(theme_.name)) {
const theme_info = allocator.create(ThemeInfo) catch @panic("OOM get_themes");
theme_info.* = .{
.name = theme_.name,
};
themes.put(theme_info.name, theme_info) catch @panic("OOM get_themes");
};
themes_ = themes;
return &themes_.?;
}
fn get_theme_names() []const []const u8 {
if (theme_names_) |names_| return names_;
const themes = themes_ orelse return &.{};
var i = get_themes(themes.allocator).iterator();
var names: std.ArrayList([]const u8) = .empty;
while (i.next()) |theme_| names.append(themes.allocator, theme_.value_ptr.*.name) catch @panic("OOM get_theme_names");
std.mem.sort([]const u8, names.items, {}, struct {
fn cmp(_: void, lhs: []const u8, rhs: []const u8) bool {
return std.mem.order(u8, lhs, rhs) == .lt;
}
}.cmp);
theme_names_ = names.toOwnedSlice(themes.allocator) catch @panic("OOM get_theme_names");
return theme_names_.?;
}
pub fn get_theme_by_name(allocator: std.mem.Allocator, name_: []const u8) ?Theme {
const themes = get_themes(allocator);
const theme = themes.get(name_) orelse return null;
return theme.get(allocator);
}
pub fn get_next_theme_by_name(name_: []const u8) []const u8 {
const theme_names = get_theme_names();
var next = false;
for (theme_names) |theme_name| {
if (next)
return theme_name;
if (std.mem.eql(u8, theme_name, name_))
next = true;
}
return theme_names[0];
}
pub fn get_prev_theme_by_name(name_: []const u8) []const u8 {
const theme_names = get_theme_names();
const last = theme_names[theme_names.len - 1];
var prev: ?[]const u8 = null;
for (theme_names) |theme_name| {
if (std.mem.eql(u8, theme_name, name_))
return prev orelse last;
prev = theme_name;
}
return last;
}
pub fn list_themes() []const []const u8 {
return get_theme_names();
}
pub const VTable = struct { pub const VTable = struct {
deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void, deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void,
send: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) error{Exit}!bool, send: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) error{Exit}!bool,

View file

@ -33,7 +33,8 @@ pub fn load_entries(palette: *Type) !usize {
var longest_hint: usize = 0; var longest_hint: usize = 0;
var idx: usize = 0; var idx: usize = 0;
try set_previous_theme(palette, tui.theme().name); try set_previous_theme(palette, tui.theme().name);
for (Widget.themes) |theme| { for (Widget.list_themes()) |theme_name_| {
const theme = Widget.get_theme_by_name(palette.allocator, theme_name_) orelse continue;
idx += 1; idx += 1;
(try palette.entries.addOne(palette.allocator)).* = .{ (try palette.entries.addOne(palette.allocator)).* = .{
.label = theme.description, .label = theme.description,

View file

@ -62,9 +62,7 @@ logger: log.Logger,
drag_source: ?Widget = null, drag_source: ?Widget = null,
drag_button: input.MouseType = 0, drag_button: input.MouseType = 0,
dark_theme: Widget.Theme, dark_theme: Widget.Theme,
dark_parsed_theme: ?std.json.Parsed(Widget.Theme),
light_theme: Widget.Theme, light_theme: Widget.Theme,
light_parsed_theme: ?std.json.Parsed(Widget.Theme),
idle_frame_count: usize = 0, idle_frame_count: usize = 0,
unrendered_input_events_count: usize = 0, unrendered_input_events_count: usize = 0,
init_timer: ?tp.timeout, init_timer: ?tp.timeout,
@ -160,9 +158,9 @@ fn init(allocator: Allocator) InitError!*Self {
if (@hasDecl(renderer, "install_crash_handler") and conf.start_debugger_on_crash) if (@hasDecl(renderer, "install_crash_handler") and conf.start_debugger_on_crash)
renderer.jit_debugger_enabled = true; renderer.jit_debugger_enabled = true;
const dark_theme, const dark_parsed_theme = get_theme_by_name(allocator, conf.theme) orelse get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme; const dark_theme = Widget.get_theme_by_name(allocator, conf.theme) orelse Widget.get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme;
conf.theme = dark_theme.name; conf.theme = dark_theme.name;
const light_theme, const light_parsed_theme = get_theme_by_name(allocator, conf.light_theme) orelse get_theme_by_name(allocator, "default-light") orelse return error.UnknownTheme; const light_theme = Widget.get_theme_by_name(allocator, conf.light_theme) orelse Widget.get_theme_by_name(allocator, "default-light") orelse return error.UnknownTheme;
conf.light_theme = light_theme.name; conf.light_theme = light_theme.name;
if (build_options.gui) conf.enable_terminal_cursor = false; if (build_options.gui) conf.enable_terminal_cursor = false;
@ -203,8 +201,6 @@ fn init(allocator: Allocator) InitError!*Self {
.query_cache_ = try syntax.QueryCache.create(allocator, .{}), .query_cache_ = try syntax.QueryCache.create(allocator, .{}),
.dark_theme = dark_theme, .dark_theme = dark_theme,
.light_theme = light_theme, .light_theme = light_theme,
.dark_parsed_theme = dark_parsed_theme,
.light_parsed_theme = light_parsed_theme,
}; };
instance_ = self; instance_ = self;
defer instance_ = null; defer instance_ = null;
@ -987,21 +983,13 @@ fn refresh_input_mode(self: *Self) command.Result {
} }
fn set_theme_by_name(self: *Self, name: []const u8, action: enum { none, store }) !void { fn set_theme_by_name(self: *Self, name: []const u8, action: enum { none, store }) !void {
const theme_, const parsed_theme = get_theme_by_name(self.allocator, name) orelse { const theme_ = Widget.get_theme_by_name(self.allocator, name) orelse {
self.logger.print("theme not found: {s}", .{name}); self.logger.print("theme not found: {s}", .{name});
return; return;
}; };
switch (self.color_scheme) { switch (self.color_scheme) {
.dark => { .dark => self.dark_theme = theme_,
if (self.dark_parsed_theme) |p| p.deinit(); .light => self.light_theme = theme_,
self.dark_parsed_theme = parsed_theme;
self.dark_theme = theme_;
},
.light => {
if (self.light_parsed_theme) |p| p.deinit();
self.light_parsed_theme = parsed_theme;
self.light_theme = theme_;
},
} }
self.set_terminal_style(&theme_); self.set_terminal_style(&theme_);
self.logger.print("theme: {s}", .{theme_.description}); self.logger.print("theme: {s}", .{theme_.description});
@ -1141,13 +1129,13 @@ const cmds = struct {
pub const set_theme_meta: Meta = .{ .arguments = &.{.string} }; pub const set_theme_meta: Meta = .{ .arguments = &.{.string} };
pub fn theme_next(self: *Self, _: Ctx) Result { pub fn theme_next(self: *Self, _: Ctx) Result {
const name = get_next_theme_by_name(self.current_theme().name); const name = Widget.get_next_theme_by_name(self.current_theme().name);
return self.set_theme_by_name(name, .store); return self.set_theme_by_name(name, .store);
} }
pub const theme_next_meta: Meta = .{ .description = "Next color theme" }; pub const theme_next_meta: Meta = .{ .description = "Next color theme" };
pub fn theme_prev(self: *Self, _: Ctx) Result { pub fn theme_prev(self: *Self, _: Ctx) Result {
const name = get_prev_theme_by_name(self.current_theme().name); const name = Widget.get_prev_theme_by_name(self.current_theme().name);
return self.set_theme_by_name(name, .store); return self.set_theme_by_name(name, .store);
} }
pub const theme_prev_meta: Meta = .{ .description = "Previous color theme" }; pub const theme_prev_meta: Meta = .{ .description = "Previous color theme" };
@ -2020,40 +2008,6 @@ pub fn theme() *const Widget.Theme {
return current().current_theme(); return current().current_theme();
} }
pub fn get_theme_by_name(allocator: std.mem.Allocator, name: []const u8) ?struct { Widget.Theme, ?std.json.Parsed(Widget.Theme) } {
if (load_theme_file(allocator, name) catch null) |parsed_theme| {
std.log.info("loaded theme from file: {s}", .{name});
return .{ parsed_theme.value, parsed_theme };
}
for (Widget.themes) |theme_| {
if (std.mem.eql(u8, theme_.name, name))
return .{ theme_, null };
}
return null;
}
fn get_next_theme_by_name(name: []const u8) []const u8 {
var next = false;
for (Widget.themes) |theme_| {
if (next)
return theme_.name;
if (std.mem.eql(u8, theme_.name, name))
next = true;
}
return Widget.themes[0].name;
}
fn get_prev_theme_by_name(name: []const u8) []const u8 {
var prev: ?Widget.Theme = null;
for (Widget.themes) |theme_| {
if (std.mem.eql(u8, theme_.name, name))
return (prev orelse Widget.themes[Widget.themes.len - 1]).name;
prev = theme_;
}
return Widget.themes[Widget.themes.len - 1].name;
}
pub fn find_scope_style(theme_: *const Widget.Theme, scope: []const u8) ?Widget.Theme.Token { pub fn find_scope_style(theme_: *const Widget.Theme, scope: []const u8) ?Widget.Theme.Token {
return if (find_scope_fallback(scope)) |tm_scope| return if (find_scope_fallback(scope)) |tm_scope|
scope_to_theme_token(theme_, tm_scope) orelse scope_to_theme_token(theme_, tm_scope) orelse
@ -2442,19 +2396,6 @@ fn get_or_create_theme_file(self: *Self, allocator: std.mem.Allocator) ![]const
return try root.get_theme_file_name(theme_name); return try root.get_theme_file_name(theme_name);
} }
fn load_theme_file(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Widget.Theme) {
return load_theme_file_internal(allocator, theme_name) catch |e| {
std.log.err("loaded theme from file failed: {}", .{e});
return e;
};
}
fn load_theme_file_internal(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Widget.Theme) {
_ = std.json.Scanner;
const json_str = root.read_theme(allocator, theme_name) orelse return null;
defer allocator.free(json_str);
return try std.json.parseFromSlice(Widget.Theme, allocator, json_str, .{ .allocate = .alloc_always });
}
pub const WidgetType = @import("config").WidgetType; pub const WidgetType = @import("config").WidgetType;
pub const ConfigWidgetStyle = @import("config").WidgetStyle; pub const ConfigWidgetStyle = @import("config").WidgetStyle;
pub const WidgetStyle = @import("WidgetStyle.zig"); pub const WidgetStyle = @import("WidgetStyle.zig");