diff --git a/build.zig.zon b/build.zig.zon index c41c109..284c2cc 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -19,8 +19,8 @@ .hash = "thespian-0.0.1-owFOjnUTBgBUlBtQ-SbN_6S7bXE6e2mKmCgk4A-RGBMA", }, .themes = .{ - .url = "https://github.com/neurocyte/flow-themes/releases/download/master-59bf204551bcb238faddd06779063570e7e6d431/flow-themes.tar.gz", - .hash = "N-V-__8AAM9UFwCaITo5LqgOpcfd4SXnFhuwJ4rEuZ253yt6", + .url = "https://github.com/neurocyte/flow-themes/releases/download/master-ac2e3fe2df3419b71276f86fa9c45fd39d668f23/flow-themes.tar.gz", + .hash = "N-V-__8AAEtaFwAjAHCmWHRCrBxL7uSG4hQiIsSgS32Y67K6", }, .fuzzig = .{ .url = "https://github.com/fjebaker/fuzzig/archive/44c04733c7c0fee3db83672aaaaf4ed03e943156.tar.gz", diff --git a/src/main.zig b/src/main.zig index ffcb840..00091d8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -671,6 +671,34 @@ pub fn list_keybind_namespaces(allocator: std.mem.Allocator) ![]const []const u8 return result.toOwnedSlice(); } +pub fn read_theme(allocator: std.mem.Allocator, theme_name: []const u8) ?[]const u8 { + const file_name = get_theme_file_name(theme_name) catch return null; + var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; + defer file.close(); + return file.readToEndAlloc(allocator, 64 * 1024) catch null; +} + +pub fn write_theme(theme_name: []const u8, content: []const u8) !void { + const file_name = try get_theme_file_name(theme_name); + var file = try std.fs.createFileAbsolute(file_name, .{ .truncate = true }); + defer file.close(); + return file.writeAll(content); +} + +pub fn list_themes(allocator: std.mem.Allocator) ![]const []const u8 { + var dir = try std.fs.openDirAbsolute(try get_theme_directory(), .{ .iterate = true }); + defer dir.close(); + var result = std.ArrayList([]const u8).init(allocator); + var iter = dir.iterateAssumeFirstIteration(); + while (try iter.next()) |entry| { + switch (entry.kind) { + .file, .sym_link => try result.append(try allocator.dupe(u8, std.fs.path.stem(entry.name))), + else => continue, + } + } + return result.toOwnedSlice(); +} + pub fn get_config_dir() ![]const u8 { return get_app_config_dir(application_name); } @@ -723,6 +751,9 @@ fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { var keybind_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; std.fs.makeDirAbsolute(try std.fmt.bufPrint(&keybind_dir_buffer, "{s}/{s}", .{ config_dir, keybind_dir })) catch {}; + var theme_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + std.fs.makeDirAbsolute(try std.fmt.bufPrint(&theme_dir_buffer, "{s}/{s}", .{ config_dir, theme_dir })) catch {}; + return config_dir; } @@ -868,6 +899,28 @@ pub fn get_keybind_namespace_file_name(namespace_name: []const u8) ![]const u8 { return try std.fmt.bufPrint(&local.file_buffer, "{s}/{s}.json", .{ dir, namespace_name }); } +const theme_dir = "themes"; + +fn get_theme_directory() ![]const u8 { + const local = struct { + var dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + const a = std.heap.c_allocator; + if (std.process.getEnvVarOwned(a, "FLOW_THEMES_DIR") catch null) |dir| { + defer a.free(dir); + return try std.fmt.bufPrint(&local.dir_buffer, "{s}", .{dir}); + } + return try std.fmt.bufPrint(&local.dir_buffer, "{s}/{s}", .{ try get_app_config_dir(application_name), theme_dir }); +} + +pub fn get_theme_file_name(theme_name: []const u8) ![]const u8 { + const dir = try get_theme_directory(); + const local = struct { + var file_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + return try std.fmt.bufPrint(&local.file_buffer, "{s}/{s}.json", .{ dir, theme_name }); +} + fn restart() noreturn { var executable: [:0]const u8 = std.mem.span(std.os.argv[0]); var is_basename = true; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 49d6e11..9339dd1 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -44,6 +44,7 @@ commands: Commands = undefined, logger: log.Logger, drag_source: ?*Widget = null, theme_: Widget.Theme, +parsed_theme: ?std.json.Parsed(Widget.Theme), idle_frame_count: usize = 0, unrendered_input_events_count: usize = 0, init_timer: ?tp.timeout, @@ -101,7 +102,7 @@ fn init(allocator: Allocator) InitError!*Self { var conf, const conf_bufs = root.read_config(@import("config"), allocator); defer root.free_config(allocator, conf_bufs); - const theme_ = get_theme_by_name(conf.theme) orelse get_theme_by_name("dark_modern") orelse return error.UnknownTheme; + const theme_, const parsed_theme = get_theme_by_name(allocator, conf.theme) orelse get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme; conf.theme = theme_.name; conf.whitespace_mode = try allocator.dupe(u8, conf.whitespace_mode); conf.input_mode = try allocator.dupe(u8, conf.input_mode); @@ -135,6 +136,7 @@ fn init(allocator: Allocator) InitError!*Self { .theme_ = theme_, .no_sleep = tp.env.get().is("no-sleep"), .query_cache_ = try syntax.QueryCache.create(allocator, .{}), + .parsed_theme = parsed_theme, }; instance_ = self; defer instance_ = null; @@ -687,6 +689,19 @@ fn refresh_input_mode(self: *Self) command.Result { self.input_mode_ = new_mode; } +fn set_theme_by_name(self: *Self, name: []const u8) !void { + const old = self.parsed_theme; + defer if (old) |p| p.deinit(); + self.theme_, self.parsed_theme = get_theme_by_name(self.allocator, name) orelse { + self.logger.print("theme not found: {s}", .{name}); + return; + }; + self.config_.theme = self.theme_.name; + self.set_terminal_style(); + self.logger.print("theme: {s}", .{self.theme_.description}); + try save_config(); +} + const cmds = struct { pub const Target = Self; const Ctx = command.Context; @@ -709,32 +724,19 @@ const cmds = struct { var name: []const u8 = undefined; if (!try ctx.args.match(.{tp.extract(&name)})) return tp.exit_error(error.InvalidSetThemeArgument, null); - self.theme_ = get_theme_by_name(name) orelse { - self.logger.print("theme not found: {s}", .{name}); - return; - }; - self.config_.theme = self.theme_.name; - self.set_terminal_style(); - self.logger.print("theme: {s}", .{self.theme_.description}); - try save_config(); + return self.set_theme_by_name(name); } pub const set_theme_meta: Meta = .{ .arguments = &.{.string} }; pub fn theme_next(self: *Self, _: Ctx) Result { - self.theme_ = get_next_theme_by_name(self.theme_.name); - self.config_.theme = self.theme_.name; - self.set_terminal_style(); - self.logger.print("theme: {s}", .{self.theme_.description}); - try save_config(); + const name = get_next_theme_by_name(self.theme_.name); + return self.set_theme_by_name(name); } pub const theme_next_meta: Meta = .{ .description = "Next color theme" }; pub fn theme_prev(self: *Self, _: Ctx) Result { - self.theme_ = get_prev_theme_by_name(self.theme_.name); - self.config_.theme = self.theme_.name; - self.set_terminal_style(); - self.logger.print("theme: {s}", .{self.theme_.description}); - try save_config(); + const name = get_prev_theme_by_name(self.theme_.name); + return self.set_theme_by_name(name); } pub const theme_prev_meta: Meta = .{ .description = "Previous color theme" }; @@ -967,6 +969,13 @@ const cmds = struct { } pub const open_keybind_config_meta: Meta = .{ .description = "Edit key bindings" }; + pub fn open_custom_theme(self: *Self, _: Ctx) Result { + const file_name = try self.get_or_create_theme_file(self.allocator); + try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_name } }); + self.logger.print("restart flow to use changed theme", .{}); + } + pub const open_custom_theme_meta: Meta = .{ .description = "Customize theme" }; + pub fn run_async(self: *Self, ctx: Ctx) Result { var iter = ctx.args.buf; var len = try cbor.decodeArrayHeader(&iter); @@ -1173,33 +1182,38 @@ pub fn theme() *const Widget.Theme { return ¤t().theme_; } -pub fn get_theme_by_name(name: []const u8) ?Widget.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_; + return .{ theme_, null }; } return null; } -pub fn get_next_theme_by_name(name: []const u8) Widget.Theme { +fn get_next_theme_by_name(name: []const u8) []const u8 { var next = false; for (Widget.themes) |theme_| { if (next) - return theme_; + return theme_.name; if (std.mem.eql(u8, theme_.name, name)) next = true; } - return Widget.themes[0]; + return Widget.themes[0].name; } -pub fn get_prev_theme_by_name(name: []const u8) Widget.Theme { +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]; + return (prev orelse Widget.themes[Widget.themes.len - 1]).name; prev = theme_; } - return Widget.themes[Widget.themes.len - 1]; + return Widget.themes[Widget.themes.len - 1].name; } pub fn find_scope_style(theme_: *const Widget.Theme, scope: []const u8) ?Widget.Theme.Token { @@ -1342,3 +1356,32 @@ pub fn render_match_cell(self: *renderer.Plane, y: usize, x: usize, theme_: *con cell.set_style(theme_.editor_match); _ = self.putc(&cell) catch {}; } + +fn get_or_create_theme_file(self: *Self, allocator: std.mem.Allocator) ![]const u8 { + const theme_name = self.theme_.name; + if (root.read_theme(allocator, theme_name)) |content| { + allocator.free(content); + } else { + var buf = std.ArrayList(u8).init(self.allocator); + defer buf.deinit(); + try std.json.stringify(self.theme_, .{ .whitespace = .indent_2 }, buf.writer()); + try root.write_theme( + theme_name, + buf.items, + ); + } + 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 }); +}