From 680c6f770e89b988b9e58e24e8de4ab832b1ba03 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 9 Aug 2025 18:06:12 +0200 Subject: [PATCH 01/87] refactor: use openFileAbsolute in mainview.read_restore_info --- src/tui/mainview.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index ce8c911..2ac5a5f 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -1315,7 +1315,7 @@ pub fn write_restore_info(self: *Self) void { fn read_restore_info(self: *Self) !void { const file_name = try root.get_restore_file_name(); - const file = try std.fs.cwd().openFile(file_name, .{ .mode = .read_only }); + const file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }); defer file.close(); const stat = try file.stat(); var buf = try self.allocator.alloc(u8, @intCast(stat.size)); From ca33259ba4fcdaa599fe7246b6ed69f75b15fe3e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 9 Aug 2025 18:06:49 +0200 Subject: [PATCH 02/87] feat: return file type and icon along with file names from the project manager --- src/Project.zig | 66 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/src/Project.zig b/src/Project.zig index 23eaae1..342d209 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -8,6 +8,7 @@ const Buffer = @import("Buffer"); const fuzzig = @import("fuzzig"); const tracy = @import("tracy"); const git = @import("git"); +const file_type_config = @import("file_type_config"); const builtin = @import("builtin"); const LSP = @import("LSP.zig"); @@ -52,6 +53,9 @@ pub const LspOrClientError = (LspError || ClientError); const File = struct { path: []const u8, + type: []const u8, + icon: []const u8, + color: u24, mtime: i128, pos: FilePos = .{}, visited: bool = false, @@ -341,7 +345,7 @@ pub fn request_n_most_recent_file(self: *Self, from: tp.pid_ref, n: usize) Clien pub fn request_recent_files(self: *Self, from: tp.pid_ref, max: usize) ClientError!void { defer from.send(.{ "PRJ", "recent_done", self.longest_file_path, "" }) catch {}; for (self.files.items, 0..) |file, i| { - from.send(.{ "PRJ", "recent", self.longest_file_path, file.path }) catch return error.ClientFailed; + from.send(.{ "PRJ", "recent", self.longest_file_path, file.path, file.type, file.icon, file.color }) catch return error.ClientFailed; if (i >= max) return; } } @@ -356,7 +360,7 @@ fn simple_query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: [ defer self.allocator.free(matches); var n: usize = 0; while (n < query.len) : (n += 1) matches[n] = idx + n; - from.send(.{ "PRJ", "recent", self.longest_file_path, file.path, matches }) catch return error.ClientFailed; + from.send(.{ "PRJ", "recent", self.longest_file_path, file.path, file.type, file.icon, file.color, matches }) catch return error.ClientFailed; i += 1; if (i >= max) return i; } @@ -379,6 +383,9 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []co const Match = struct { path: []const u8, + type: []const u8, + icon: []const u8, + color: u24, score: i32, matches: []const usize, }; @@ -389,6 +396,9 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []co if (match.score) |score| { (try matches.addOne()).* = .{ .path = file.path, + .type = file.type, + .icon = file.icon, + .color = file.color, .score = score, .matches = try self.allocator.dupe(usize, match.matches), }; @@ -404,13 +414,24 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []co std.mem.sort(Match, matches.items, {}, less_fn); for (matches.items[0..@min(max, matches.items.len)]) |match| - from.send(.{ "PRJ", "recent", self.longest_file_path, match.path, match.matches }) catch return error.ClientFailed; + from.send(.{ "PRJ", "recent", self.longest_file_path, match.path, match.type, match.icon, match.color, match.matches }) catch return error.ClientFailed; return @min(max, matches.items.len); } -pub fn walk_tree_entry(self: *Self, file_path: []const u8, mtime: i128) OutOfMemoryError!void { +pub fn walk_tree_entry( + self: *Self, + file_path: []const u8, + mtime: i128, +) OutOfMemoryError!void { + const file_type: []const u8, const file_icon: []const u8, const file_color: u24 = guess_file_type(file_path); self.longest_file_path = @max(self.longest_file_path, file_path.len); - (try self.pending.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, file_path), .mtime = mtime }; + (try self.pending.addOne(self.allocator)).* = .{ + .path = try self.allocator.dupe(u8, file_path), + .type = file_type, + .icon = file_icon, + .color = file_color, + .mtime = mtime, + }; } pub fn walk_tree_done(self: *Self, parent: tp.pid_ref) OutOfMemoryError!void { @@ -420,6 +441,25 @@ pub fn walk_tree_done(self: *Self, parent: tp.pid_ref) OutOfMemoryError!void { return self.loaded(parent); } +fn guess_file_type(file_path: []const u8) struct { []const u8, []const u8, u24 } { + var buf: [1024]u8 = undefined; + const content: []const u8 = blk: { + const file = std.fs.cwd().openFile(file_path, .{ .mode = .read_only }) catch break :blk &.{}; + defer file.close(); + const size = file.read(&buf) catch break :blk &.{}; + break :blk buf[0..size]; + }; + return if (file_type_config.guess_file_type(file_path, content)) |ft| .{ + ft.name, + ft.icon orelse file_type_config.default.icon, + ft.color orelse file_type_config.default.color, + } else .{ + file_type_config.default.name, + file_type_config.default.icon, + file_type_config.default.color, + }; +} + fn merge_pending_files(self: *Self) OutOfMemoryError!void { defer self.sort_files_by_mtime(); const existing = try self.files.toOwnedSlice(self.allocator); @@ -469,9 +509,13 @@ fn update_mru_internal(self: *Self, file_path: []const u8, mtime: i128, row: usi } return; } + const file_type: []const u8, const file_icon: []const u8, const file_color: u24 = guess_file_type(file_path); if (row != 0) { (try self.files.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, file_path), + .type = file_type, + .icon = file_icon, + .color = file_color, .mtime = mtime, .pos = .{ .row = row, .col = col }, .visited = true, @@ -479,6 +523,9 @@ fn update_mru_internal(self: *Self, file_path: []const u8, mtime: i128, row: usi } else { (try self.files.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, file_path), + .type = file_type, + .icon = file_icon, + .color = file_color, .mtime = mtime, }; } @@ -1941,7 +1988,14 @@ pub fn process_git(self: *Self, parent: tp.pid_ref, m: tp.message) (OutOfMemoryE } else if (try m.match(.{ tp.any, tp.any, "workspace_files", tp.extract(&path) })) { self.longest_file_path = @max(self.longest_file_path, path.len); const stat = std.fs.cwd().statFile(path) catch return; - (try self.pending.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, path), .mtime = stat.mtime }; + const file_type: []const u8, const file_icon: []const u8, const file_color: u24 = guess_file_type(path); + (try self.pending.addOne(self.allocator)).* = .{ + .path = try self.allocator.dupe(u8, path), + .type = file_type, + .icon = file_icon, + .color = file_color, + .mtime = stat.mtime, + }; } else if (try m.match(.{ tp.any, tp.any, "workspace_files", tp.null_ })) { self.state.workspace_files = .done; try self.loaded(parent); From 72d97f61f5cdfa5ee14c43f9f84c13c17e1f4379 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 9 Aug 2025 18:09:30 +0200 Subject: [PATCH 03/87] feat: display file icons in open_recent palette --- src/tui/mode/overlay/open_recent.zig | 69 +++++++++++++++++++++------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index b3c05dc..d284a84 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -2,6 +2,7 @@ const std = @import("std"); const tp = @import("thespian"); const log = @import("log"); const cbor = @import("cbor"); +const file_type_config = @import("file_type_config"); const root = @import("root"); const Plane = @import("renderer").Plane; @@ -111,13 +112,25 @@ fn on_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: button.plane.home(); } var file_path: []const u8 = undefined; + var file_type: []const u8 = undefined; + var file_icon: []const u8 = undefined; + var file_color: u24 = undefined; var iter = button.opts.label; // label contains cbor, first the file name, then multiple match indexes - if (!(cbor.matchString(&iter, &file_path) catch false)) - file_path = "#ERROR#"; + if (!(cbor.matchString(&iter, &file_path) catch false)) file_path = "#ERROR#"; + if (!(cbor.matchString(&iter, &file_type) catch false)) file_type = file_type_config.default.name; + if (!(cbor.matchString(&iter, &file_icon) catch false)) file_icon = file_type_config.default.icon; + if (!(cbor.matchInt(u24, &iter, &file_color) catch false)) file_icon = file_type_config.default.icon; + button.plane.set_style(style_keybind); const dirty = if (self.buffer_manager) |bm| if (bm.is_buffer_dirty(file_path)) "" else " " else " "; const pointer = if (selected) "⏵" else dirty; _ = button.plane.print("{s}", .{pointer}) catch {}; + + if (tui.config().show_fileicons) { + tui.render_file_icon(&button.plane, file_icon, file_color); + _ = button.plane.print(" ", .{}) catch {}; + } + var buf: [std.fs.max_path_bytes]u8 = undefined; var removed_prefix: usize = 0; const max_len = max_menu_width() - 2; @@ -125,25 +138,17 @@ fn on_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: _ = button.plane.print("{s} ", .{ if (file_path.len > max_len) root.shorten_path(&buf, file_path, &removed_prefix, max_len) else file_path, }) catch {}; + var index: usize = 0; var len = cbor.decodeArrayHeader(&iter) catch return false; while (len > 0) : (len -= 1) { if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { - const cell_idx = if (index < removed_prefix) 1 else index + 1 - removed_prefix; - render_cell(&button.plane, 0, cell_idx, theme.editor_match) catch break; + tui.render_match_cell(&button.plane, 0, index + 4, theme) catch break; } else break; } return false; } -fn render_cell(plane: *Plane, y: usize, x: usize, style: Widget.Theme.Style) !void { - plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return; - var cell = plane.cell_init(); - _ = plane.at_cursor_cell(&cell) catch return; - cell.set_style(style); - _ = plane.putc(&cell) catch {}; -} - fn on_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); } @@ -156,11 +161,21 @@ fn menu_action_open_file(menu: **Menu.State(*Self), button: *Button.State(*Menu. tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); } -fn add_item(self: *Self, file_name: []const u8, matches: ?[]const u8) !void { +fn add_item( + self: *Self, + file_name: []const u8, + file_type: []const u8, + file_icon: []const u8, + file_color: u24, + matches: ?[]const u8, +) !void { var label = std.ArrayList(u8).init(self.allocator); defer label.deinit(); const writer = label.writer(); try cbor.writeValue(writer, file_name); + try cbor.writeValue(writer, file_type); + try cbor.writeValue(writer, file_icon); + try cbor.writeValue(writer, file_color); if (matches) |cb| _ = try writer.write(cb); try self.menu.add_item_with_handler(label.items, menu_action_open_file); } @@ -175,20 +190,40 @@ fn receive_project_manager(self: *Self, _: tp.pid_ref, m: tp.message) MessageFil fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void { var file_name: []const u8 = undefined; + var file_type: []const u8 = undefined; + var file_icon: []const u8 = undefined; + var file_color: u24 = undefined; var matches: []const u8 = undefined; var query: []const u8 = undefined; - if (try cbor.match(m.buf, .{ "PRJ", "recent", tp.extract(&self.longest), tp.extract(&file_name), tp.extract_cbor(&matches) })) { + if (try cbor.match(m.buf, .{ + "PRJ", + "recent", + tp.extract(&self.longest), + tp.extract(&file_name), + tp.extract(&file_type), + tp.extract(&file_icon), + tp.extract(&file_color), + tp.extract_cbor(&matches), + })) { if (self.need_reset) self.reset_results(); - try self.add_item(file_name, matches); + try self.add_item(file_name, file_type, file_icon, file_color, matches); self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); if (self.need_select_first) { self.menu.select_down(); self.need_select_first = false; } tui.need_render(); - } else if (try cbor.match(m.buf, .{ "PRJ", "recent", tp.extract(&self.longest), tp.extract(&file_name) })) { + } else if (try cbor.match(m.buf, .{ + "PRJ", + "recent", + tp.extract(&self.longest), + tp.extract(&file_name), + tp.extract(&file_type), + tp.extract(&file_icon), + tp.extract(&file_color), + })) { if (self.need_reset) self.reset_results(); - try self.add_item(file_name, null); + try self.add_item(file_name, file_type, file_icon, file_color, null); self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); if (self.need_select_first) { self.menu.select_down(); From de6ca62f6de302614d930091690ff2d09e6a91c2 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 9 Aug 2025 18:17:15 +0200 Subject: [PATCH 04/87] fix: allocate file_type_name in file_type_config cache --- src/file_type_config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/file_type_config.zig b/src/file_type_config.zig index 008bb9d..1412aae 100644 --- a/src/file_type_config.zig +++ b/src/file_type_config.zig @@ -84,7 +84,7 @@ pub fn get(file_type_name: []const u8) !?@This() { break :file_type if (syntax.FileType.get_by_name_static(file_type_name)) |ft| from_file_type(ft) else null; } }; - try cache.put(cache_allocator, file_type_name, file_type); + try cache.put(cache_allocator, try cache_allocator.dupe(u8, file_type_name), file_type); break :self file_type; }; } From 8789e8b89cb37aca717d6d27fb181a900e85e151 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 9 Aug 2025 19:12:19 +0200 Subject: [PATCH 05/87] fix: use proper platform path separators for config files --- src/main.zig | 76 ++++++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/main.zig b/src/main.zig index f337409..f36f642 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ const color = @import("color"); const flags = @import("flags"); const builtin = @import("builtin"); const bin_path = @import("bin_path"); +const sep = std.fs.path.sep; const list_languages = @import("list_languages.zig"); pub const file_link = @import("file_link.zig"); @@ -398,7 +399,7 @@ fn trace_to_file(m: thespian.message.c_buffer_type) callconv(.C) void { const a = std.heap.c_allocator; var path = std.ArrayList(u8).init(a); defer path.deinit(); - path.writer().print("{s}/trace.log", .{get_state_dir() catch return}) catch return; + path.writer().print("{s}{c}trace.log", .{ get_state_dir() catch return, sep }) catch return; const file = std.fs.createFileAbsolute(path.items, .{ .truncate = true }) catch return; State.state = .{ .file = file, @@ -502,12 +503,12 @@ pub fn parse_text_config_file(T: type, allocator: std.mem.Allocator, conf: *T, b lineno += 1; if (line.len == 0 or line[0] == '#') continue; - const sep = std.mem.indexOfScalar(u8, line, ' ') orelse { + const spc = std.mem.indexOfScalar(u8, line, ' ') orelse { std.log.err("{s}:{}: {s} missing value", .{ file_name, lineno, line }); continue; }; - const name = line[0..sep]; - const value_str = line[sep + 1 ..]; + const name = line[0..spc]; + const value_str = line[spc + 1 ..]; const cb = cbor.fromJsonAlloc(allocator, value_str) catch { std.log.err("{s}:{}: {s} has bad value: {s}", .{ file_name, lineno, name, value_str }); continue; @@ -781,6 +782,11 @@ pub const ConfigDirError = error{ AppConfigDirUnavailable, }; +fn make_dir_error(path: []const u8, err: anytype) @TypeOf(err) { + std.log.err("failed to create directory: '{s}'", .{path}); + return err; +} + fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { const a = std.heap.c_allocator; const local = struct { @@ -791,22 +797,22 @@ fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { dir else if (std.process.getEnvVarOwned(a, "XDG_CONFIG_HOME") catch null) |xdg| ret: { defer a.free(xdg); - break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/{s}", .{ xdg, appname }); + break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}{s}", .{ xdg, sep, appname }); } else if (std.process.getEnvVarOwned(a, "HOME") catch null) |home| ret: { defer a.free(home); - const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config", .{home}); + const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}.config", .{ home, sep }); std.fs.makeDirAbsolute(dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return error.MakeHomeConfigDirFailed, + else => return make_dir_error(dir, error.MakeHomeConfigDirFailed), }; - break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config/{s}", .{ home, appname }); + break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}.config{c}{s}", .{ home, sep, sep, appname }); } else if (builtin.os.tag == .windows) ret: { if (std.process.getEnvVarOwned(a, "APPDATA") catch null) |appdata| { defer a.free(appdata); - const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/{s}", .{ appdata, appname }); + const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}{s}", .{ appdata, sep, appname }); std.fs.makeDirAbsolute(dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return error.MakeAppConfigDirFailed, + else => return make_dir_error(dir, error.MakeAppConfigDirFailed), }; break :ret dir; } else return error.AppConfigDirUnavailable; @@ -815,14 +821,14 @@ fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { local.config_dir = config_dir; std.fs.makeDirAbsolute(config_dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return error.MakeConfigDirFailed, + else => return make_dir_error(config_dir, error.MakeConfigDirFailed), }; 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 {}; + std.fs.makeDirAbsolute(try std.fmt.bufPrint(&keybind_dir_buffer, "{s}{c}{s}", .{ config_dir, sep, 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 {}; + std.fs.makeDirAbsolute(try std.fmt.bufPrint(&theme_dir_buffer, "{s}{c}{s}", .{ config_dir, sep, theme_dir })) catch {}; return config_dir; } @@ -841,22 +847,22 @@ fn get_app_cache_dir(appname: []const u8) ![]const u8 { dir else if (std.process.getEnvVarOwned(a, "XDG_CACHE_HOME") catch null) |xdg| ret: { defer a.free(xdg); - break :ret try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}/{s}", .{ xdg, appname }); + break :ret try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}{s}", .{ xdg, sep, appname }); } else if (std.process.getEnvVarOwned(a, "HOME") catch null) |home| ret: { defer a.free(home); - const dir = try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}/.cache", .{home}); + const dir = try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}.cache", .{ home, sep }); std.fs.makeDirAbsolute(dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return e, + else => return make_dir_error(dir, e), }; - break :ret try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}/.cache/{s}", .{ home, appname }); + break :ret try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}.cache{c}{s}", .{ home, sep, sep, appname }); } else if (builtin.os.tag == .windows) ret: { if (std.process.getEnvVarOwned(a, "APPDATA") catch null) |appdata| { defer a.free(appdata); - const dir = try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}/{s}", .{ appdata, appname }); + const dir = try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}{s}", .{ appdata, sep, appname }); std.fs.makeDirAbsolute(dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return e, + else => return make_dir_error(dir, e), }; break :ret dir; } else return error.AppCacheDirUnavailable; @@ -865,7 +871,7 @@ fn get_app_cache_dir(appname: []const u8) ![]const u8 { local.cache_dir = cache_dir; std.fs.makeDirAbsolute(cache_dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return e, + else => return make_dir_error(cache_dir, e), }; return cache_dir; } @@ -884,27 +890,27 @@ fn get_app_state_dir(appname: []const u8) ![]const u8 { dir else if (std.process.getEnvVarOwned(a, "XDG_STATE_HOME") catch null) |xdg| ret: { defer a.free(xdg); - break :ret try std.fmt.bufPrint(&local.state_dir_buffer, "{s}/{s}", .{ xdg, appname }); + break :ret try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}{s}", .{ xdg, sep, appname }); } else if (std.process.getEnvVarOwned(a, "HOME") catch null) |home| ret: { defer a.free(home); - var dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}/.local", .{home}); + var dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}.local", .{ home, sep }); std.fs.makeDirAbsolute(dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return e, + else => return make_dir_error(dir, e), }; - dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}/.local/state", .{home}); + dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}.local{c}state", .{ home, sep, sep }); std.fs.makeDirAbsolute(dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return e, + else => return make_dir_error(dir, e), }; - break :ret try std.fmt.bufPrint(&local.state_dir_buffer, "{s}/.local/state/{s}", .{ home, appname }); + break :ret try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}.local{c}state{c}{s}", .{ home, sep, sep, sep, appname }); } else if (builtin.os.tag == .windows) ret: { if (std.process.getEnvVarOwned(a, "APPDATA") catch null) |appdata| { defer a.free(appdata); - const dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}/{s}", .{ appdata, appname }); + const dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}{s}", .{ appdata, sep, appname }); std.fs.makeDirAbsolute(dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return e, + else => return make_dir_error(dir, e), }; break :ret dir; } else return error.AppCacheDirUnavailable; @@ -913,7 +919,7 @@ fn get_app_state_dir(appname: []const u8) ![]const u8 { local.state_dir = state_dir; std.fs.makeDirAbsolute(state_dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return e, + else => return make_dir_error(state_dir, e), }; return state_dir; } @@ -926,7 +932,7 @@ fn get_app_config_dir_file_name(appname: []const u8, comptime config_file_name: const local = struct { var config_file_buffer: [std.posix.PATH_MAX]u8 = undefined; }; - return std.fmt.bufPrint(&local.config_file_buffer, "{s}/{s}", .{ try get_app_config_dir(appname), config_file_name }); + return std.fmt.bufPrint(&local.config_file_buffer, "{s}{c}{s}", .{ try get_app_config_dir(appname), sep, config_file_name }); } pub fn get_config_file_name(T: type) ![]const u8 { @@ -942,7 +948,7 @@ pub fn get_restore_file_name() ![]const u8 { const restore_file = if (local.restore_file) |file| file else - try std.fmt.bufPrint(&local.restore_file_buffer, "{s}/{s}", .{ try get_app_state_dir(application_name), restore_file_name }); + try std.fmt.bufPrint(&local.restore_file_buffer, "{s}{c}{s}", .{ try get_app_state_dir(application_name), sep, restore_file_name }); local.restore_file = restore_file; return restore_file; } @@ -958,7 +964,7 @@ fn get_keybind_namespaces_directory() ![]const u8 { 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), keybind_dir }); + return try std.fmt.bufPrint(&local.dir_buffer, "{s}{c}{s}", .{ try get_app_config_dir(application_name), sep, keybind_dir }); } pub fn get_keybind_namespace_file_name(namespace_name: []const u8) ![]const u8 { @@ -966,7 +972,7 @@ pub fn get_keybind_namespace_file_name(namespace_name: []const u8) ![]const u8 { const local = struct { var file_buffer: [std.posix.PATH_MAX]u8 = undefined; }; - return try std.fmt.bufPrint(&local.file_buffer, "{s}/{s}.json", .{ dir, namespace_name }); + return try std.fmt.bufPrint(&local.file_buffer, "{s}{c}{s}.json", .{ dir, sep, namespace_name }); } const theme_dir = "themes"; @@ -980,7 +986,7 @@ fn get_theme_directory() ![]const u8 { 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 }); + return try std.fmt.bufPrint(&local.dir_buffer, "{s}{c}{s}", .{ try get_app_config_dir(application_name), sep, theme_dir }); } pub fn get_theme_file_name(theme_name: []const u8) ![]const u8 { @@ -988,7 +994,7 @@ pub fn get_theme_file_name(theme_name: []const u8) ![]const u8 { 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 }); + return try std.fmt.bufPrint(&local.file_buffer, "{s}{c}{s}.json", .{ dir, sep, theme_name }); } fn restart() noreturn { From b913b8ad87df928b9f6df0a432e2c03e4c040614 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 9 Aug 2025 19:15:01 +0200 Subject: [PATCH 06/87] feat: write early log output to stderr/stdout until TUI is initialized --- src/log.zig | 31 ++++++++++++++++++++++++++++++- src/tui/tui.zig | 4 ++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/log.zig b/src/log.zig index 71d3521..1beff73 100644 --- a/src/log.zig +++ b/src/log.zig @@ -11,6 +11,8 @@ subscriber: ?tp.pid, heap: [32 + 1024]u8, fba: std.heap.FixedBufferAllocator, msg_store: MsgStoreT, +no_stdout: bool = false, +no_stderr: bool = false, const MsgStoreT = std.DoublyLinkedList([]u8); const Receiver = tp.Receiver(*Self); @@ -79,12 +81,23 @@ fn store_reset(self: *Self) void { fn receive(self: *Self, from: tp.pid_ref, m: tp.message) tp.result { errdefer self.deinit(); - if (try m.match(.{ "log", tp.more })) { + var output: []const u8 = undefined; + if (try m.match(.{ "log", "error", tp.string, "std.log", "->", tp.extract(&output) })) { if (self.subscriber) |subscriber| { subscriber.send_raw(m) catch {}; } else { self.store(m); } + if (!self.no_stderr) + std.io.getStdErr().writer().print("{s}\n", .{output}) catch {}; + } else if (try m.match(.{ "log", tp.string, tp.extract(&output) })) { + if (self.subscriber) |subscriber| { + subscriber.send_raw(m) catch {}; + } else { + self.store(m); + } + if (!self.no_stdout) + std.io.getStdOut().writer().print("{s}\n", .{output}) catch {}; } else if (try m.match(.{"subscribe"})) { // log("subscribed"); if (self.subscriber) |*s| s.deinit(); @@ -95,6 +108,14 @@ fn receive(self: *Self, from: tp.pid_ref, m: tp.message) tp.result { if (self.subscriber) |*s| s.deinit(); self.subscriber = null; self.store_reset(); + } else if (try m.match(.{ "stdout", "enable" })) { + self.no_stdout = false; + } else if (try m.match(.{ "stdout", "disable" })) { + self.no_stdout = true; + } else if (try m.match(.{ "stderr", "enable" })) { + self.no_stderr = false; + } else if (try m.match(.{ "stderr", "disable" })) { + self.no_stderr = true; } else if (try m.match(.{"shutdown"})) { return tp.exit_normal(); } @@ -202,6 +223,14 @@ pub fn unsubscribe() tp.result { return tp.env.get().proc("log").send(.{"unsubscribe"}); } +pub fn stdout(state: enum { enable, disable }) void { + tp.env.get().proc("log").send(.{ "stdout", state }) catch {}; +} + +pub fn stderr(state: enum { enable, disable }) void { + tp.env.get().proc("log").send(.{ "stderr", state }) catch {}; +} + var std_log_pid: ?tp.pid_ref = null; pub fn set_std_log_pid(pid: ?tp.pid_ref) void { diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 5aa13f6..3a3e7c8 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -103,6 +103,8 @@ const InitError = error{ keybind.LoadError; fn init(allocator: Allocator) InitError!*Self { + log.stdout(.disable); + var conf, const conf_bufs = root.read_config(@import("config"), allocator); if (@hasDecl(renderer, "install_crash_handler") and conf.start_debugger_on_crash) @@ -157,6 +159,8 @@ fn init(allocator: Allocator) InitError!*Self { self.rdr_.dispatch_event = dispatch_event; try self.rdr_.run(); + log.stderr(.disable); + try project_manager.start(); try frame_clock.start(); From ab0a8f3c2cd9a1613e471d5a1b803c4d613ed837 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 9 Aug 2025 22:20:17 +0200 Subject: [PATCH 07/87] feat: show file type icon in open_file completion --- src/Project.zig | 22 +++++--- src/file_type_config.zig | 2 + src/project_manager.zig | 17 +++++-- src/tui/mode/mini/file_browser.zig | 82 +++++++++++++++++++----------- 4 files changed, 83 insertions(+), 40 deletions(-) diff --git a/src/Project.zig b/src/Project.zig index 342d209..6613215 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -441,7 +441,21 @@ pub fn walk_tree_done(self: *Self, parent: tp.pid_ref) OutOfMemoryError!void { return self.loaded(parent); } -fn guess_file_type(file_path: []const u8) struct { []const u8, []const u8, u24 } { +fn default_ft() struct { []const u8, []const u8, u24 } { + return .{ + file_type_config.default.name, + file_type_config.default.icon, + file_type_config.default.color, + }; +} + +pub fn guess_path_file_type(path: []const u8, file_name: []const u8) struct { []const u8, []const u8, u24 } { + var buf: [4096]u8 = undefined; + const file_path = std.fmt.bufPrint(&buf, "{s}{}{s}", .{ path, std.fs.path.sep, file_name }) catch return default_ft(); + return guess_file_type(file_path); +} + +pub fn guess_file_type(file_path: []const u8) struct { []const u8, []const u8, u24 } { var buf: [1024]u8 = undefined; const content: []const u8 = blk: { const file = std.fs.cwd().openFile(file_path, .{ .mode = .read_only }) catch break :blk &.{}; @@ -453,11 +467,7 @@ fn guess_file_type(file_path: []const u8) struct { []const u8, []const u8, u24 } ft.name, ft.icon orelse file_type_config.default.icon, ft.color orelse file_type_config.default.color, - } else .{ - file_type_config.default.name, - file_type_config.default.icon, - file_type_config.default.color, - }; + } else default_ft(); } fn merge_pending_files(self: *Self) OutOfMemoryError!void { diff --git a/src/file_type_config.zig b/src/file_type_config.zig index 1412aae..944ea16 100644 --- a/src/file_type_config.zig +++ b/src/file_type_config.zig @@ -20,6 +20,8 @@ pub const default = struct { pub const color = 0x000000; }; +pub const folder_icon = ""; + fn from_file_type(file_type: syntax.FileType) @This() { return .{ .name = file_type.name, diff --git a/src/project_manager.zig b/src/project_manager.zig index c29a78a..1c4d6e5 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -793,12 +793,19 @@ fn request_path_files_async(a_: std.mem.Allocator, parent_: tp.pid_ref, project_ var iter = self.dir.iterateAssumeFirstIteration(); errdefer |e| self.parent.send(.{ "PRJ", "path_error", self.project_name, self.path, e }) catch {}; while (try iter.next()) |entry| { - switch (entry.kind) { - .directory => try self.parent.send(.{ "PRJ", "path_entry", self.project_name, self.path, "DIR", entry.name }), - .sym_link => try self.parent.send(.{ "PRJ", "path_entry", self.project_name, self.path, "LINK", entry.name }), - .file => try self.parent.send(.{ "PRJ", "path_entry", self.project_name, self.path, "FILE", entry.name }), + const event_type = switch (entry.kind) { + .directory => "DIR", + .sym_link => "LINK", + .file => "FILE", else => continue, - } + }; + const default = file_type_config.default; + const file_type, const icon, const color = switch (entry.kind) { + .directory => .{ "directory", file_type_config.folder_icon, default.color }, + .sym_link, .file => Project.guess_path_file_type(self.path, entry.name), + else => .{ default.name, default.icon, default.color }, + }; + try self.parent.send(.{ "PRJ", "path_entry", self.project_name, self.path, event_type, entry.name, file_type, icon, color }); count += 1; if (count >= self.max) break; } diff --git a/src/tui/mode/mini/file_browser.zig b/src/tui/mode/mini/file_browser.zig index c47d088..37747d1 100644 --- a/src/tui/mode/mini/file_browser.zig +++ b/src/tui/mode/mini/file_browser.zig @@ -2,6 +2,7 @@ const std = @import("std"); const tp = @import("thespian"); const cbor = @import("cbor"); const log = @import("log"); +const file_type_config = @import("file_type_config"); const root = @import("root"); const input = @import("input"); @@ -20,6 +21,7 @@ pub fn Create(options: type) type { return struct { allocator: std.mem.Allocator, file_path: std.ArrayList(u8), + rendered_mini_buffer: std.ArrayListUnmanaged(u8) = .empty, query: std.ArrayList(u8), match: std.ArrayList(u8), entries: std.ArrayList(Entry), @@ -33,8 +35,12 @@ pub fn Create(options: type) type { const Entry = struct { name: []const u8, - type: enum { dir, file, link }, + type: EntryType, + file_type: []const u8, + icon: []const u8, + color: u24, }; + const EntryType = enum { dir, file, link }; pub fn create(allocator: std.mem.Allocator, _: command.Context) !struct { tui.Mode, tui.MiniMode } { const self = try allocator.create(Self); @@ -66,6 +72,7 @@ pub fn Create(options: type) type { self.match.deinit(); self.query.deinit(); self.file_path.deinit(); + self.rendered_mini_buffer.deinit(self.allocator); self.allocator.destroy(self); } @@ -80,7 +87,11 @@ pub fn Create(options: type) type { } fn clear_entries(self: *Self) void { - for (self.entries.items) |entry| self.allocator.free(entry.name); + for (self.entries.items) |entry| { + self.allocator.free(entry.name); + self.allocator.free(entry.file_type); + self.allocator.free(entry.icon); + } self.entries.clearRetainingCapacity(); } @@ -112,14 +123,11 @@ pub fn Create(options: type) type { self.complete_trigger_count = 0; self.file_path.clearRetainingCapacity(); if (self.match.items.len > 0) { - try self.construct_path(self.query.items, .{ .name = self.match.items, .type = .file }, 0); + try self.construct_path(self.query.items, self.match.items, .file, 0); } else { try self.file_path.appendSlice(self.query.items); } - if (tui.mini_mode()) |mini_mode| { - mini_mode.text = self.file_path.items; - mini_mode.cursor = tui.egc_chunk_width(self.file_path.items, 0, 1); - } + self.update_mini_mode_text(); return; } self.complete_trigger_count -= 1; @@ -139,12 +147,7 @@ pub fn Create(options: type) type { } fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void { - defer { - if (tui.mini_mode()) |mini_mode| { - mini_mode.text = self.file_path.items; - mini_mode.cursor = tui.egc_chunk_width(self.file_path.items, 0, 1); - } - } + defer self.update_mini_mode_text(); var count: usize = undefined; if (try cbor.match(m.buf, .{ "PRJ", "path_entry", tp.more })) { return self.process_path_entry(m); @@ -158,18 +161,31 @@ pub fn Create(options: type) type { fn process_path_entry(self: *Self, m: tp.message) MessageFilter.Error!void { var path: []const u8 = undefined; var file_name: []const u8 = undefined; - if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "DIR", tp.extract(&file_name) })) { - (try self.entries.addOne()).* = .{ .name = try self.allocator.dupe(u8, file_name), .type = .dir }; - } else if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "LINK", tp.extract(&file_name) })) { - (try self.entries.addOne()).* = .{ .name = try self.allocator.dupe(u8, file_name), .type = .link }; - } else if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "FILE", tp.extract(&file_name) })) { - (try self.entries.addOne()).* = .{ .name = try self.allocator.dupe(u8, file_name), .type = .file }; + var file_type: []const u8 = undefined; + var icon: []const u8 = undefined; + var color: u24 = undefined; + if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "DIR", tp.extract(&file_name), tp.extract(&file_type), tp.extract(&icon), tp.extract(&color) })) { + try self.add_entry(file_name, .dir, file_type, icon, color); + } else if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "LINK", tp.extract(&file_name), tp.extract(&file_type), tp.extract(&icon), tp.extract(&color) })) { + try self.add_entry(file_name, .link, file_type, icon, color); + } else if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "FILE", tp.extract(&file_name), tp.extract(&file_type), tp.extract(&icon), tp.extract(&color) })) { + try self.add_entry(file_name, .file, file_type, icon, color); } else { log.logger("file_browser").err("receive", tp.unexpected(m)); } tui.need_render(); } + fn add_entry(self: *Self, file_name: []const u8, entry_type: EntryType, file_type: []const u8, icon: []const u8, color: u24) !void { + (try self.entries.addOne()).* = .{ + .name = try self.allocator.dupe(u8, file_name), + .type = entry_type, + .file_type = try self.allocator.dupe(u8, file_type), + .icon = try self.allocator.dupe(u8, icon), + .color = color, + }; + } + fn do_complete(self: *Self) !void { self.complete_trigger_count = @min(self.complete_trigger_count, self.entries.items.len); self.file_path.clearRetainingCapacity(); @@ -179,9 +195,10 @@ pub fn Create(options: type) type { if (self.total_matches == 1) self.complete_trigger_count = 0; } else if (self.entries.items.len > 0) { - try self.construct_path(self.query.items, self.entries.items[self.complete_trigger_count - 1], self.complete_trigger_count - 1); + const entry = self.entries.items[self.complete_trigger_count - 1]; + try self.construct_path(self.query.items, entry.name, entry.type, self.complete_trigger_count - 1); } else { - try self.construct_path(self.query.items, .{ .name = "", .type = .file }, 0); + try self.construct_path(self.query.items, "", .file, 0); } if (self.match.items.len > 0) if (self.total_matches > 1) @@ -192,14 +209,14 @@ pub fn Create(options: type) type { message("{d}/{d}", .{ self.matched_entry + 1, self.entries.items.len }); } - fn construct_path(self: *Self, path_: []const u8, entry: Entry, entry_no: usize) error{OutOfMemory}!void { + fn construct_path(self: *Self, path_: []const u8, entry_name: []const u8, entry_type: EntryType, entry_no: usize) error{OutOfMemory}!void { self.matched_entry = entry_no; const path = project_manager.normalize_file_path(path_); try self.file_path.appendSlice(path); if (path.len > 0 and path[path.len - 1] != std.fs.path.sep) try self.file_path.append(std.fs.path.sep); - try self.file_path.appendSlice(entry.name); - if (entry.type == .dir) + try self.file_path.appendSlice(entry_name); + if (entry_type == .dir) try self.file_path.append(std.fs.path.sep); } @@ -212,7 +229,7 @@ pub fn Create(options: type) type { if (try prefix_compare_icase(self.allocator, self.match.items, entry.name)) { matched += 1; if (matched == self.complete_trigger_count) { - try self.construct_path(self.query.items, entry, i); + try self.construct_path(self.query.items, entry.name, entry.type, i); found_match = i; } last = entry; @@ -222,11 +239,11 @@ pub fn Create(options: type) type { self.total_matches = matched; if (found_match) |_| return; if (last) |entry| { - try self.construct_path(self.query.items, entry, last_no); + try self.construct_path(self.query.items, entry.name, entry.type, last_no); self.complete_trigger_count = matched; } else { message("no match for '{s}'", .{self.match.items}); - try self.construct_path(self.query.items, .{ .name = self.match.items, .type = .file }, 0); + try self.construct_path(self.query.items, self.match.items, .file, 0); } } @@ -264,8 +281,15 @@ pub fn Create(options: type) type { fn update_mini_mode_text(self: *Self) void { if (tui.mini_mode()) |mini_mode| { - mini_mode.text = self.file_path.items; - mini_mode.cursor = tui.egc_chunk_width(self.file_path.items, 0, 1); + const icon = if (self.entries.items.len > 0 and self.complete_trigger_count > 0) + self.entries.items[self.complete_trigger_count - 1].icon + else + " "; + self.rendered_mini_buffer.clearRetainingCapacity(); + const writer = self.rendered_mini_buffer.writer(self.allocator); + writer.print("{s} {s}", .{ icon, self.file_path.items }) catch {}; + mini_mode.text = self.rendered_mini_buffer.items; + mini_mode.cursor = tui.egc_chunk_width(self.file_path.items, 0, 1) + 3; } } From e886b7064a441bdbf9b6eb42e2ddb84d57ede3ad Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 10 Aug 2025 21:53:36 +0200 Subject: [PATCH 08/87] fix: check that the second click in a double click is in the same cell --- src/tui/editor.zig | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index f9fa3de..701626d 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -6029,6 +6029,8 @@ pub const EditorWidget = struct { last_btn: input.Mouse = .none, last_btn_time_ms: i64 = 0, last_btn_count: usize = 0, + last_btn_x: c_int = 0, + last_btn_y: c_int = 0, hover: bool = false, hover_timer: ?tp.Cancellable = null, @@ -6193,9 +6195,16 @@ pub const EditorWidget = struct { fn mouse_click_button1(self: *Self, y: c_int, x: c_int, _: c_int, xoffset: c_int) Result { const y_, const x_ = self.mouse_pos_abs(y, x, xoffset); + defer { + self.last_btn_y = y_; + self.last_btn_x = x_; + } if (self.last_btn == input.mouse.BUTTON1) { const click_time_ms = time.milliTimestamp() - self.last_btn_time_ms; - if (click_time_ms <= double_click_time_ms) { + if (click_time_ms <= double_click_time_ms and + self.last_btn_y == y_ and + self.last_btn_x == x_) + { if (self.last_btn_count == 2) { self.last_btn_count = 3; try self.editor.primary_triple_click(y_, x_); From 1632061144884aeb3804bc61a091fbe2df035324 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 11 Aug 2025 14:07:11 +0200 Subject: [PATCH 09/87] refactor: goto minimode into a reusable numeric input minimode --- src/keybind/builtin/flow.json | 4 +- src/tui/mode/mini/goto.zig | 140 ++------------------------- src/tui/mode/mini/numeric_input.zig | 145 ++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 134 deletions(-) create mode 100644 src/tui/mode/mini/numeric_input.zig diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 339972b..314f676 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -328,7 +328,7 @@ ["right_control", "palette_menu_activate_quick"] ] }, - "mini/goto": { + "mini/numeric": { "press": [ ["ctrl+q", "quit"], ["ctrl+v", "system_paste"], @@ -338,7 +338,7 @@ ["ctrl+l", "scroll_view_center_cycle"], ["ctrl+space", "mini_mode_cancel"], ["escape", "mini_mode_cancel"], - ["enter", "exit_mini_mode"], + ["enter", "mini_mode_select"], ["backspace", "mini_mode_delete_backwards"] ] }, diff --git a/src/tui/mode/mini/goto.zig b/src/tui/mode/mini/goto.zig index 94d5c6d..13abeec 100644 --- a/src/tui/mode/mini/goto.zig +++ b/src/tui/mode/mini/goto.zig @@ -1,140 +1,16 @@ -const tp = @import("thespian"); - -const key = @import("renderer").input.key; -const mod = @import("renderer").input.modifier; -const event_type = @import("renderer").input.event_type; -const keybind = @import("keybind"); const command = @import("command"); -const EventHandler = @import("EventHandler"); -const tui = @import("../../tui.zig"); +pub const Type = @import("numeric_input.zig").Create(@This()); +pub const create = Type.create; -const Allocator = @import("std").mem.Allocator; -const fmt = @import("std").fmt; - -const Self = @This(); -const name = "#goto"; - -const Commands = command.Collection(cmds); - -allocator: Allocator, -buf: [30]u8 = undefined, -input: ?usize = null, -start: usize, -commands: Commands = undefined, - -pub fn create(allocator: Allocator, _: command.Context) !struct { tui.Mode, tui.MiniMode } { - const editor = tui.get_active_editor() orelse return error.NotFound; - const self = try allocator.create(Self); - errdefer allocator.destroy(self); - self.* = .{ - .allocator = allocator, - .start = editor.get_primary().cursor.row + 1, - }; - try self.commands.init(self); - var mode = try keybind.mode("mini/goto", allocator, .{ - .insert_command = "mini_mode_insert_bytes", - }); - mode.event_handler = EventHandler.to_owned(self); - return .{ mode, .{ .name = name } }; +pub fn name(_: *Type) []const u8 { + return "#goto"; } -pub fn deinit(self: *Self) void { - self.commands.deinit(); - self.allocator.destroy(self); -} +pub const preview = goto; +pub const apply = goto; +pub const cancel = goto; -pub fn receive(self: *Self, _: tp.pid_ref, _: tp.message) error{Exit}!bool { - self.update_mini_mode_text(); - return false; -} - -fn update_mini_mode_text(self: *Self) void { - if (tui.mini_mode()) |mini_mode| { - mini_mode.text = if (self.input) |linenum| - (fmt.bufPrint(&self.buf, "{d}", .{linenum}) catch "") - else - ""; - mini_mode.cursor = tui.egc_chunk_width(mini_mode.text, 0, 1); - } -} - -fn goto(self: *Self) void { +fn goto(self: *Type) void { command.executeName("goto_line", command.fmt(.{self.input orelse self.start})) catch {}; } - -fn insert_char(self: *Self, char: u8) void { - switch (char) { - '0' => { - if (self.input) |linenum| self.input = linenum * 10; - }, - '1'...'9' => { - const digit: usize = @intCast(char - '0'); - self.input = if (self.input) |x| x * 10 + digit else digit; - }, - else => {}, - } -} - -fn insert_bytes(self: *Self, bytes: []const u8) void { - for (bytes) |c| self.insert_char(c); -} - -const cmds = struct { - pub const Target = Self; - const Ctx = command.Context; - const Meta = command.Metadata; - const Result = command.Result; - - pub fn mini_mode_reset(self: *Self, _: Ctx) Result { - self.input = null; - self.update_mini_mode_text(); - } - pub const mini_mode_reset_meta: Meta = .{ .description = "Clear input" }; - - pub fn mini_mode_cancel(self: *Self, _: Ctx) Result { - self.input = null; - self.update_mini_mode_text(); - self.goto(); - command.executeName("exit_mini_mode", .{}) catch {}; - } - pub const mini_mode_cancel_meta: Meta = .{ .description = "Cancel input" }; - - pub fn mini_mode_delete_backwards(self: *Self, _: Ctx) Result { - if (self.input) |linenum| { - const newval = if (linenum < 10) 0 else linenum / 10; - self.input = if (newval == 0) null else newval; - self.update_mini_mode_text(); - self.goto(); - } - } - pub const mini_mode_delete_backwards_meta: Meta = .{ .description = "Delete backwards" }; - - pub fn mini_mode_insert_code_point(self: *Self, ctx: Ctx) Result { - var keypress: usize = 0; - if (!try ctx.args.match(.{tp.extract(&keypress)})) - return error.InvalidGotoInsertCodePointArgument; - switch (keypress) { - '0'...'9' => self.insert_char(@intCast(keypress)), - else => {}, - } - self.update_mini_mode_text(); - self.goto(); - } - pub const mini_mode_insert_code_point_meta: Meta = .{ .arguments = &.{.integer} }; - - pub fn mini_mode_insert_bytes(self: *Self, ctx: Ctx) Result { - var bytes: []const u8 = undefined; - if (!try ctx.args.match(.{tp.extract(&bytes)})) - return error.InvalidGotoInsertBytesArgument; - self.insert_bytes(bytes); - self.update_mini_mode_text(); - self.goto(); - } - pub const mini_mode_insert_bytes_meta: Meta = .{ .arguments = &.{.string} }; - - pub fn mini_mode_paste(self: *Self, ctx: Ctx) Result { - return mini_mode_insert_bytes(self, ctx); - } - pub const mini_mode_paste_meta: Meta = .{ .arguments = &.{.string} }; -}; diff --git a/src/tui/mode/mini/numeric_input.zig b/src/tui/mode/mini/numeric_input.zig new file mode 100644 index 0000000..2781a45 --- /dev/null +++ b/src/tui/mode/mini/numeric_input.zig @@ -0,0 +1,145 @@ +const tp = @import("thespian"); + +const key = @import("renderer").input.key; +const mod = @import("renderer").input.modifier; +const event_type = @import("renderer").input.event_type; +const keybind = @import("keybind"); +const command = @import("command"); +const EventHandler = @import("EventHandler"); + +const tui = @import("../../tui.zig"); + +const Allocator = @import("std").mem.Allocator; +const fmt = @import("std").fmt; + +pub fn Create(options: type) type { + return struct { + const Self = @This(); + + const Commands = command.Collection(cmds); + + allocator: Allocator, + buf: [30]u8 = undefined, + input: ?usize = null, + start: usize, + commands: Commands = undefined, + + pub fn create(allocator: Allocator, _: command.Context) !struct { tui.Mode, tui.MiniMode } { + const editor = tui.get_active_editor() orelse return error.NotFound; + const self = try allocator.create(Self); + errdefer allocator.destroy(self); + self.* = .{ + .allocator = allocator, + .start = editor.get_primary().cursor.row + 1, + }; + try self.commands.init(self); + var mode = try keybind.mode("mini/numeric", allocator, .{ + .insert_command = "mini_mode_insert_bytes", + }); + mode.event_handler = EventHandler.to_owned(self); + return .{ mode, .{ .name = options.name(self) } }; + } + + pub fn deinit(self: *Self) void { + self.commands.deinit(); + self.allocator.destroy(self); + } + + pub fn receive(self: *Self, _: tp.pid_ref, _: tp.message) error{Exit}!bool { + self.update_mini_mode_text(); + return false; + } + + fn update_mini_mode_text(self: *Self) void { + if (tui.mini_mode()) |mini_mode| { + mini_mode.text = if (self.input) |linenum| + (fmt.bufPrint(&self.buf, "{d}", .{linenum}) catch "") + else + ""; + mini_mode.cursor = tui.egc_chunk_width(mini_mode.text, 0, 1); + } + } + + fn insert_char(self: *Self, char: u8) void { + switch (char) { + '0' => { + if (self.input) |linenum| self.input = linenum * 10; + }, + '1'...'9' => { + const digit: usize = @intCast(char - '0'); + self.input = if (self.input) |x| x * 10 + digit else digit; + }, + else => {}, + } + } + + fn insert_bytes(self: *Self, bytes: []const u8) void { + for (bytes) |c| self.insert_char(c); + } + + const cmds = struct { + pub const Target = Self; + const Ctx = command.Context; + const Meta = command.Metadata; + const Result = command.Result; + + pub fn mini_mode_reset(self: *Self, _: Ctx) Result { + self.input = null; + self.update_mini_mode_text(); + } + pub const mini_mode_reset_meta: Meta = .{ .description = "Clear input" }; + + pub fn mini_mode_cancel(self: *Self, _: Ctx) Result { + self.input = null; + self.update_mini_mode_text(); + options.cancel(self); + command.executeName("exit_mini_mode", .{}) catch {}; + } + pub const mini_mode_cancel_meta: Meta = .{ .description = "Cancel input" }; + + pub fn mini_mode_delete_backwards(self: *Self, _: Ctx) Result { + if (self.input) |linenum| { + const newval = if (linenum < 10) 0 else linenum / 10; + self.input = if (newval == 0) null else newval; + self.update_mini_mode_text(); + options.preview(self); + } + } + pub const mini_mode_delete_backwards_meta: Meta = .{ .description = "Delete backwards" }; + + pub fn mini_mode_insert_code_point(self: *Self, ctx: Ctx) Result { + var keypress: usize = 0; + if (!try ctx.args.match(.{tp.extract(&keypress)})) + return error.InvalidGotoInsertCodePointArgument; + switch (keypress) { + '0'...'9' => self.insert_char(@intCast(keypress)), + else => {}, + } + self.update_mini_mode_text(); + options.preview(self); + } + pub const mini_mode_insert_code_point_meta: Meta = .{ .arguments = &.{.integer} }; + + pub fn mini_mode_insert_bytes(self: *Self, ctx: Ctx) Result { + var bytes: []const u8 = undefined; + if (!try ctx.args.match(.{tp.extract(&bytes)})) + return error.InvalidGotoInsertBytesArgument; + self.insert_bytes(bytes); + self.update_mini_mode_text(); + options.preview(self); + } + pub const mini_mode_insert_bytes_meta: Meta = .{ .arguments = &.{.string} }; + + pub fn mini_mode_paste(self: *Self, ctx: Ctx) Result { + return mini_mode_insert_bytes(self, ctx); + } + pub const mini_mode_paste_meta: Meta = .{ .arguments = &.{.string} }; + + pub fn mini_mode_select(self: *Self, _: Ctx) Result { + options.apply(self); + command.executeName("exit_mini_mode", .{}) catch {}; + } + pub const mini_mode_select_meta: Meta = .{ .description = "Select" }; + }; + }; +} From 3e0e75c9c8f5efcfb43a4fa4797e3533b6da947a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 11 Aug 2025 14:29:23 +0200 Subject: [PATCH 10/87] feat: add interactive and non-interactive commands to set the current buffer's tab_width --- src/tui/editor.zig | 18 ++++++++++++++++++ src/tui/mode/mini/tab_width.zig | 16 ++++++++++++++++ src/tui/tui.zig | 5 +++++ 3 files changed, 39 insertions(+) create mode 100644 src/tui/mode/mini/tab_width.zig diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 701626d..b9fce38 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -432,6 +432,7 @@ pub const Editor = struct { tp.extract_cbor(&cursels_cbor), })) return error.RestoreStateMatch; + self.refresh_tab_width(); if (op == .open_file) try self.open(file_path); self.clipboard = if (clipboard.len > 0) try self.allocator.dupe(u8, clipboard) else null; @@ -702,6 +703,23 @@ pub const Editor = struct { return; } + fn refresh_tab_width(self: *Self) void { + self.metrics = self.plane.metrics(self.tab_width); + switch (self.indent_mode) { + .spaces, .auto => {}, + .tabs => self.indent_size = self.tab_width, + } + } + + pub fn set_tab_width(self: *Self, ctx: Context) Result { + var tab_width: usize = 0; + if (!try ctx.args.match(.{tp.extract(&tab_width)})) + return error.InvalidSetTabWidthArgument; + self.tab_width = tab_width; + self.refresh_tab_width(); + } + pub const set_tab_width_meta: Meta = .{ .arguments = &.{.integer} }; + fn close(self: *Self) !void { var meta = std.ArrayListUnmanaged(u8).empty; defer meta.deinit(self.allocator); diff --git a/src/tui/mode/mini/tab_width.zig b/src/tui/mode/mini/tab_width.zig new file mode 100644 index 0000000..a2ca836 --- /dev/null +++ b/src/tui/mode/mini/tab_width.zig @@ -0,0 +1,16 @@ +const command = @import("command"); + +pub const Type = @import("numeric_input.zig").Create(@This()); +pub const create = Type.create; + +pub fn name(_: *Type) []const u8 { + return " tab size"; +} + +pub const preview = goto; +pub const apply = goto; +pub const cancel = goto; + +fn goto(self: *Type) void { + command.executeName("set_tab_width", command.fmt(.{self.input orelse self.start})) catch {}; +} diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 3a3e7c8..f762102 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -744,6 +744,11 @@ const cmds = struct { } pub const force_terminate_meta: Meta = .{ .description = "Force quit without saving" }; + pub fn tab_width(self: *Self, ctx: Ctx) Result { + return enter_mini_mode(self, @import("mode/mini/tab_width.zig"), ctx); + } + pub const tab_width_meta: Meta = .{ .description = "Set tab width" }; + pub fn set_theme(self: *Self, ctx: Ctx) Result { var name: []const u8 = undefined; if (!try ctx.args.match(.{tp.extract(&name)})) From fb99aebfa9baeb1ec3bb81666f55c8eb6cf25d18 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 11 Aug 2025 17:46:49 +0200 Subject: [PATCH 11/87] feat: minor improvements to flow SELECT mode keybindings --- src/keybind/builtin/flow.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 314f676..427295e 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -236,6 +236,8 @@ ["page_down", "select_page_down"], ["ctrl+page_up", "select_scroll_page_up"], ["ctrl+page_down", "select_scroll_page_down"], + ["ctrl+b", "move_to_char", "select_to_char_left"], + ["ctrl+t", "move_to_char", "select_to_char_right"], ["ctrl+space", "enter_mode", "normal"], ["ctrl+x", ["cut"], ["enter_mode", "normal"], ["cancel"]], ["ctrl+c", ["copy"], ["enter_mode", "normal"], ["cancel"]], @@ -347,6 +349,8 @@ ["ctrl+g", "mini_mode_cancel"], ["ctrl+c", "mini_mode_cancel"], ["ctrl+l", "scroll_view_center_cycle"], + ["tab", "mini_mode_insert_bytes", "\t"], + ["enter", "mini_mode_insert_bytes", "\n"], ["escape", "mini_mode_cancel"], ["backspace", "mini_mode_cancel"] ] From 1fcec1bab5943a56919806396693198f36c29575 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 12 Aug 2025 12:02:41 +0200 Subject: [PATCH 12/87] feat: add support for numeric arguments in cli exec calls --- src/main.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index f36f642..e914235 100644 --- a/src/main.zig +++ b/src/main.zig @@ -331,7 +331,12 @@ pub fn main() anyerror!void { try cbor.writeValue(writer, cmd_); try cbor.writeArrayHeader(writer, count - 1); - while (cmd_args.next()) |arg| try cbor.writeValue(writer, arg); + while (cmd_args.next()) |arg| { + if (std.fmt.parseInt(isize, arg, 10) catch null) |i| + try cbor.writeValue(writer, i) + else + try cbor.writeValue(writer, arg); + } try tui_proc.send_raw(.{ .buf = msg.items }); } From 63a527726a16b762df4b55ce05fb162d6e8f517b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 12 Aug 2025 12:04:36 +0200 Subject: [PATCH 13/87] feat: add support for arguments to mini/numeric_input modes --- src/tui/mode/mini/goto.zig | 9 ++++++++- src/tui/mode/mini/numeric_input.zig | 19 +++++++++++-------- src/tui/mode/mini/tab_width.zig | 28 ++++++++++++++++++++++------ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/tui/mode/mini/goto.zig b/src/tui/mode/mini/goto.zig index 13abeec..6607dc6 100644 --- a/src/tui/mode/mini/goto.zig +++ b/src/tui/mode/mini/goto.zig @@ -1,5 +1,7 @@ const command = @import("command"); +const tui = @import("../../tui.zig"); + pub const Type = @import("numeric_input.zig").Create(@This()); pub const create = Type.create; @@ -7,10 +9,15 @@ pub fn name(_: *Type) []const u8 { return "#goto"; } +pub fn start(_: *Type) usize { + const editor = tui.get_active_editor() orelse return 1; + return editor.get_primary().cursor.row + 1; +} + pub const preview = goto; pub const apply = goto; pub const cancel = goto; -fn goto(self: *Type) void { +fn goto(self: *Type, _: command.Context) void { command.executeName("goto_line", command.fmt(.{self.input orelse self.start})) catch {}; } diff --git a/src/tui/mode/mini/numeric_input.zig b/src/tui/mode/mini/numeric_input.zig index 2781a45..5b6be0f 100644 --- a/src/tui/mode/mini/numeric_input.zig +++ b/src/tui/mode/mini/numeric_input.zig @@ -22,16 +22,18 @@ pub fn Create(options: type) type { buf: [30]u8 = undefined, input: ?usize = null, start: usize, + ctx: command.Context, commands: Commands = undefined, - pub fn create(allocator: Allocator, _: command.Context) !struct { tui.Mode, tui.MiniMode } { - const editor = tui.get_active_editor() orelse return error.NotFound; + pub fn create(allocator: Allocator, ctx: command.Context) !struct { tui.Mode, tui.MiniMode } { const self = try allocator.create(Self); errdefer allocator.destroy(self); self.* = .{ .allocator = allocator, - .start = editor.get_primary().cursor.row + 1, + .ctx = .{ .args = try ctx.args.clone(allocator) }, + .start = 0, }; + self.start = options.start(self); try self.commands.init(self); var mode = try keybind.mode("mini/numeric", allocator, .{ .insert_command = "mini_mode_insert_bytes", @@ -41,6 +43,7 @@ pub fn Create(options: type) type { } pub fn deinit(self: *Self) void { + self.allocator.free(self.ctx.args.buf); self.commands.deinit(); self.allocator.destroy(self); } @@ -92,7 +95,7 @@ pub fn Create(options: type) type { pub fn mini_mode_cancel(self: *Self, _: Ctx) Result { self.input = null; self.update_mini_mode_text(); - options.cancel(self); + options.cancel(self, self.ctx); command.executeName("exit_mini_mode", .{}) catch {}; } pub const mini_mode_cancel_meta: Meta = .{ .description = "Cancel input" }; @@ -102,7 +105,7 @@ pub fn Create(options: type) type { const newval = if (linenum < 10) 0 else linenum / 10; self.input = if (newval == 0) null else newval; self.update_mini_mode_text(); - options.preview(self); + options.preview(self, self.ctx); } } pub const mini_mode_delete_backwards_meta: Meta = .{ .description = "Delete backwards" }; @@ -116,7 +119,7 @@ pub fn Create(options: type) type { else => {}, } self.update_mini_mode_text(); - options.preview(self); + options.preview(self, self.ctx); } pub const mini_mode_insert_code_point_meta: Meta = .{ .arguments = &.{.integer} }; @@ -126,7 +129,7 @@ pub fn Create(options: type) type { return error.InvalidGotoInsertBytesArgument; self.insert_bytes(bytes); self.update_mini_mode_text(); - options.preview(self); + options.preview(self, self.ctx); } pub const mini_mode_insert_bytes_meta: Meta = .{ .arguments = &.{.string} }; @@ -136,7 +139,7 @@ pub fn Create(options: type) type { pub const mini_mode_paste_meta: Meta = .{ .arguments = &.{.string} }; pub fn mini_mode_select(self: *Self, _: Ctx) Result { - options.apply(self); + options.apply(self, self.ctx); command.executeName("exit_mini_mode", .{}) catch {}; } pub const mini_mode_select_meta: Meta = .{ .description = "Select" }; diff --git a/src/tui/mode/mini/tab_width.zig b/src/tui/mode/mini/tab_width.zig index a2ca836..a617973 100644 --- a/src/tui/mode/mini/tab_width.zig +++ b/src/tui/mode/mini/tab_width.zig @@ -1,5 +1,8 @@ +const cbor = @import("cbor"); const command = @import("command"); +const tui = @import("../../tui.zig"); + pub const Type = @import("numeric_input.zig").Create(@This()); pub const create = Type.create; @@ -7,10 +10,23 @@ pub fn name(_: *Type) []const u8 { return " tab size"; } -pub const preview = goto; -pub const apply = goto; -pub const cancel = goto; - -fn goto(self: *Type) void { - command.executeName("set_tab_width", command.fmt(.{self.input orelse self.start})) catch {}; +pub fn start(self: *Type) usize { + const tab_width = if (tui.get_active_editor()) |editor| editor.tab_width else tui.config().tab_width; + self.input = tab_width; + return tab_width; +} + +const default_cmd = "set_editor_tab_width"; + +pub const cancel = preview; + +pub fn preview(self: *Type, _: command.Context) void { + command.executeName(default_cmd, command.fmt(.{self.input orelse self.start})) catch {}; +} + +pub fn apply(self: *Type, ctx: command.Context) void { + var cmd: []const u8 = undefined; + if (!(ctx.args.match(.{cbor.extract(&cmd)}) catch false)) + cmd = default_cmd; + command.executeName(cmd, command.fmt(.{self.input orelse self.start})) catch {}; } From 4037d67fe9037565f599cabb16deb8a48f534efa Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 12 Aug 2025 12:29:08 +0200 Subject: [PATCH 14/87] feat: add support for session local tab_width setting --- src/tui/editor.zig | 2 +- src/tui/mode/mini/tab_width.zig | 2 +- src/tui/tui.zig | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index b9fce38..911a5b2 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -469,7 +469,7 @@ pub const Editor = struct { fn init(self: *Self, allocator: Allocator, n: Plane, buffer_manager: *Buffer.Manager) void { const logger = log.logger("editor"); const frame_rate = tp.env.get().num("frame-rate"); - const tab_width = tui.config().tab_width; + const tab_width = tui.get_tab_width(); const indent_mode = tui.config().indent_mode; const indent_size = if (indent_mode == .tabs) tab_width else tui.config().indent_size; self.* = Self{ diff --git a/src/tui/mode/mini/tab_width.zig b/src/tui/mode/mini/tab_width.zig index a617973..53e4868 100644 --- a/src/tui/mode/mini/tab_width.zig +++ b/src/tui/mode/mini/tab_width.zig @@ -11,7 +11,7 @@ pub fn name(_: *Type) []const u8 { } pub fn start(self: *Type) usize { - const tab_width = if (tui.get_active_editor()) |editor| editor.tab_width else tui.config().tab_width; + const tab_width = if (tui.get_active_editor()) |editor| editor.tab_width else tui.get_tab_width(); self.input = tab_width; return tab_width; } diff --git a/src/tui/tui.zig b/src/tui/tui.zig index f762102..84ecc1e 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -24,6 +24,7 @@ allocator: Allocator, rdr_: renderer, config_: @import("config"), config_bufs: [][]const u8, +session_tab_width: ?usize = null, highlight_columns_: []const u16, highlight_columns_configured: []const u16, frame_time: usize, // in microseconds @@ -1122,6 +1123,11 @@ pub fn config() *const @import("config") { return ¤t().config_; } +pub fn get_tab_width() usize { + const self = current(); + return self.session_tab_width orelse self.config_.tab_width; +} + pub fn highlight_columns() []const u16 { return current().highlight_columns_; } From 80002e4d6ba31bf69deb5b978e8137f1398a99dc Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 12 Aug 2025 12:54:34 +0200 Subject: [PATCH 15/87] feat: add set_buffer_tab_width and set_session_tab_width commands Also, fold the tab_width and set_tab_width commands into one. The default command (set_tab_width) now stores the tab_width in the persistent config. --- src/tui/editor.zig | 4 ++-- src/tui/tui.zig | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 911a5b2..03b5900 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -711,14 +711,14 @@ pub const Editor = struct { } } - pub fn set_tab_width(self: *Self, ctx: Context) Result { + pub fn set_editor_tab_width(self: *Self, ctx: Context) Result { var tab_width: usize = 0; if (!try ctx.args.match(.{tp.extract(&tab_width)})) return error.InvalidSetTabWidthArgument; self.tab_width = tab_width; self.refresh_tab_width(); } - pub const set_tab_width_meta: Meta = .{ .arguments = &.{.integer} }; + pub const set_editor_tab_width_meta: Meta = .{ .arguments = &.{.integer} }; fn close(self: *Self) !void { var meta = std.ArrayListUnmanaged(u8).empty; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 84ecc1e..70bc51d 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -745,10 +745,39 @@ const cmds = struct { } pub const force_terminate_meta: Meta = .{ .description = "Force quit without saving" }; - pub fn tab_width(self: *Self, ctx: Ctx) Result { - return enter_mini_mode(self, @import("mode/mini/tab_width.zig"), ctx); + pub fn set_tab_width(self: *Self, ctx: Ctx) Result { + var tab_width: usize = 0; + if (!try ctx.args.match(.{tp.extract(&tab_width)})) + return enter_mini_mode(self, @import("mode/mini/tab_width.zig"), Ctx.fmt(.{"set_tab_width"})); + + self.config_.tab_width = tab_width; + self.session_tab_width = null; + command.executeName("set_editor_tab_width", ctx) catch {}; + try save_config(); + self.logger.print("tab width {}", .{tab_width}); } - pub const tab_width_meta: Meta = .{ .description = "Set tab width" }; + pub const set_tab_width_meta: Meta = .{ .description = "Set tab width" }; + + pub fn set_buffer_tab_width(self: *Self, ctx: Ctx) Result { + var tab_width: usize = 0; + if (!try ctx.args.match(.{tp.extract(&tab_width)})) + return enter_mini_mode(self, @import("mode/mini/tab_width.zig"), Ctx.fmt(.{"set_buffer_tab_width"})); + + command.executeName("set_editor_tab_width", ctx) catch {}; + self.logger.print("buffer tab width {}", .{tab_width}); + } + pub const set_buffer_tab_width_meta: Meta = .{ .description = "Set tab width for current buffer" }; + + pub fn set_session_tab_width(self: *Self, ctx: Ctx) Result { + var tab_width: usize = 0; + if (!try ctx.args.match(.{tp.extract(&tab_width)})) + return enter_mini_mode(self, @import("mode/mini/tab_width.zig"), Ctx.fmt(.{"set_session_tab_width"})); + + self.session_tab_width = tab_width; + command.executeName("set_editor_tab_width", ctx) catch {}; + self.logger.print("session tab width {}", .{tab_width}); + } + pub const set_session_tab_width_meta: Meta = .{ .description = "Set tab width for current session" }; pub fn set_theme(self: *Self, ctx: Ctx) Result { var name: []const u8 = undefined; From 21fe6103bf86c53db0643c50606124fea4c2dc16 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 12 Aug 2025 13:04:35 +0200 Subject: [PATCH 16/87] feat: add open_most_recent_file command --- src/tui/mainview.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 2ac5a5f..c726c6f 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -938,6 +938,12 @@ const cmds = struct { } pub const open_previous_file_meta: Meta = .{ .description = "Open the previous file" }; + pub fn open_most_recent_file(self: *Self, _: Ctx) Result { + if (try project_manager.request_most_recent_file(self.allocator)) |file_path| + self.show_file_async(file_path); + } + pub const open_most_recent_file_meta: Meta = .{ .description = "Open the last changed file" }; + pub fn system_paste(self: *Self, _: Ctx) Result { if (builtin.os.tag == .windows) { const text = try @import("renderer").request_windows_clipboard(self.allocator); From 3ed13a4ab8b72070f009cf2645430c8bf846f476 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 12 Aug 2025 14:05:26 +0200 Subject: [PATCH 17/87] fix: add missing arguments meta declartions on set_tab_width commands --- src/tui/tui.zig | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 70bc51d..0969e7c 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -756,7 +756,10 @@ const cmds = struct { try save_config(); self.logger.print("tab width {}", .{tab_width}); } - pub const set_tab_width_meta: Meta = .{ .description = "Set tab width" }; + pub const set_tab_width_meta: Meta = .{ + .description = "Set tab width", + .arguments = &.{.integer}, + }; pub fn set_buffer_tab_width(self: *Self, ctx: Ctx) Result { var tab_width: usize = 0; @@ -766,7 +769,10 @@ const cmds = struct { command.executeName("set_editor_tab_width", ctx) catch {}; self.logger.print("buffer tab width {}", .{tab_width}); } - pub const set_buffer_tab_width_meta: Meta = .{ .description = "Set tab width for current buffer" }; + pub const set_buffer_tab_width_meta: Meta = .{ + .description = "Set tab width for current buffer", + .arguments = &.{.integer}, + }; pub fn set_session_tab_width(self: *Self, ctx: Ctx) Result { var tab_width: usize = 0; @@ -777,7 +783,10 @@ const cmds = struct { command.executeName("set_editor_tab_width", ctx) catch {}; self.logger.print("session tab width {}", .{tab_width}); } - pub const set_session_tab_width_meta: Meta = .{ .description = "Set tab width for current session" }; + pub const set_session_tab_width_meta: Meta = .{ + .description = "Set tab width for current session", + .arguments = &.{.integer}, + }; pub fn set_theme(self: *Self, ctx: Ctx) Result { var name: []const u8 = undefined; From e7324dc11040d3033ce4843492d75f074795e61d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 12 Aug 2025 14:06:20 +0200 Subject: [PATCH 18/87] feat: add flow mode keybind for set_session_tab_width command --- src/keybind/builtin/flow.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 427295e..4988117 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -38,6 +38,7 @@ ["f5", ["create_scratch_buffer", "*test*"], ["shell_execute_insert", "zig", "build", "test"]], ["f7", ["create_scratch_buffer", "*build*"], ["shell_execute_insert", "zig", "build"]], ["ctrl+f6", "open_version_info"], + ["alt+shift+t", "set_session_tab_width"], ["alt+d", ["shell_execute_insert", "date", "--iso-8601"]], ["ctrl+alt+shift+d", ["shell_execute_insert", "date", "--iso-8601=seconds"]] ] From ac2a7cfa83d336b98db515b056f066e7ea5bbc55 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 12 Aug 2025 14:06:42 +0200 Subject: [PATCH 19/87] feat: add flow mode global keybind for restart command --- src/keybind/builtin/flow.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 4988117..80e7e8f 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -1,6 +1,7 @@ { "project": { "press": [ + ["ctrl+alt+shift+r", "restart"], ["ctrl+e", "find_file"], ["ctrl+shift+n", "create_new_file"], ["ctrl+r", "open_recent_project"], From 83a0adccc77e005f705c79896d176f77bb62d9d1 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 12 Aug 2025 22:29:10 +0200 Subject: [PATCH 20/87] feat: add menu border styles --- src/tui/Menu.zig | 27 +++-- src/tui/Widget.zig | 46 ++++++++ src/tui/WidgetList.zig | 161 +++++++++++++++++++++++---- src/tui/mode/overlay/open_recent.zig | 28 +++-- src/tui/mode/overlay/palette.zig | 26 ++++- 5 files changed, 245 insertions(+), 43 deletions(-) diff --git a/src/tui/Menu.zig b/src/tui/Menu.zig index 284dc03..813051c 100644 --- a/src/tui/Menu.zig +++ b/src/tui/Menu.zig @@ -20,7 +20,8 @@ pub fn Options(context: type) type { on_click5: *const fn (menu: **State(Context), button: *Button.State(*State(Context))) void = do_nothing_click, on_render: *const fn (ctx: context, button: *Button.State(*State(Context)), theme: *const Widget.Theme, selected: bool) bool = on_render_default, on_layout: *const fn (ctx: context, button: *Button.State(*State(Context))) Widget.Layout = on_layout_default, - on_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) void = on_resize_default, + prepare_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) Widget.Box = prepare_resize_default, + after_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) void = after_resize_default, on_scroll: ?EventHandler = null, pub const Context = context; @@ -46,23 +47,26 @@ pub fn Options(context: type) type { return .{ .static = 1 }; } - pub fn on_resize_default(_: context, state: *State(Context), box_: Widget.Box) void { + pub fn prepare_resize_default(_: context, state: *State(Context), box_: Widget.Box) Widget.Box { var box = box_; box.h = if (box_.h == 0) state.menu.widgets.items.len else box_.h; - state.container.resize(box); + return box; } + + pub fn after_resize_default(_: context, _: *State(Context), _: Widget.Box) void {} }; } pub fn create(ctx_type: type, allocator: std.mem.Allocator, parent: Plane, opts: Options(ctx_type)) !*State(ctx_type) { const self = try allocator.create(State(ctx_type)); errdefer allocator.destroy(self); - const container = try WidgetList.createH(allocator, parent, @typeName(@This()), .dynamic); + const container = try WidgetList.createHStyled(allocator, parent, @typeName(@This()), .dynamic, Widget.Style.boxed); self.* = .{ .allocator = allocator, .menu = try WidgetList.createV(allocator, container.plane, @typeName(@This()), .dynamic), .container = container, .container_widget = container.widget(), + .frame_widget = null, .scrollbar = if (tui.config().show_scrollbars) if (opts.on_scroll) |on_scroll| (try scrollbar_v.create(allocator, parent, null, on_scroll)).dynamic_cast(scrollbar_v).? else null else @@ -72,7 +76,8 @@ pub fn create(ctx_type: type, allocator: std.mem.Allocator, parent: Plane, opts: self.menu.ctx = self; self.menu.on_render = State(ctx_type).on_render_menu; container.ctx = self; - container.on_resize = State(ctx_type).on_resize_container; + container.prepare_resize = State(ctx_type).prepare_resize; + container.after_resize = State(ctx_type).after_resize; try container.add(self.menu.widget()); if (self.scrollbar) |sb| try container.add(sb.widget()); return self; @@ -84,6 +89,7 @@ pub fn State(ctx_type: type) type { menu: *WidgetList, container: *WidgetList, container_widget: Widget, + frame_widget: ?Widget, scrollbar: ?*scrollbar_v, opts: options_type, selected: ?usize = null, @@ -146,9 +152,14 @@ pub fn State(ctx_type: type) type { self.render_idx = 0; } - fn on_resize_container(ctx: ?*anyopaque, _: *WidgetList, box: Widget.Box) void { + fn prepare_resize(ctx: ?*anyopaque, _: *WidgetList, box: Widget.Box) Widget.Box { const self: *Self = @ptrCast(@alignCast(ctx)); - self.opts.on_resize(self.*.opts.ctx, self, box); + return self.opts.prepare_resize(self.*.opts.ctx, self, box); + } + + fn after_resize(ctx: ?*anyopaque, _: *WidgetList, box: Widget.Box) void { + const self: *Self = @ptrCast(@alignCast(ctx)); + self.opts.after_resize(self.*.opts.ctx, self, box); } pub fn on_layout(self: **Self, button: *Button.State(*Self)) Widget.Layout { @@ -170,7 +181,7 @@ pub fn State(ctx_type: type) type { } pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool { - return self.menu.walk(walk_ctx, f, &self.container_widget); + return self.menu.walk(walk_ctx, f, if (self.frame_widget) |*frame| frame else &self.container_widget); } pub fn count(self: *Self) usize { diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 0f0c96f..7de2347 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -38,6 +38,52 @@ pub const Layout = union(enum) { } }; +pub const Style = struct { + padding: Margin = margins.@"0", + inner_padding: Margin = margins.@"0", + border: Border = borders.blank, + + pub const PaddingUnit = u16; + + pub const Margin = struct { + top: PaddingUnit, + bottom: PaddingUnit, + left: PaddingUnit, + right: PaddingUnit, + }; + + pub const Border = struct { + nw: []const u8, + n: []const u8, + ne: []const u8, + e: []const u8, + se: []const u8, + s: []const u8, + sw: []const u8, + w: []const u8, + }; + + pub const margins = struct { + const @"0": Margin = .{ .top = 0, .bottom = 0, .left = 0, .right = 0 }; + const @"1": Margin = .{ .top = 1, .bottom = 1, .left = 1, .right = 1 }; + const @"2": Margin = .{ .top = 2, .bottom = 2, .left = 2, .right = 2 }; + }; + + pub const borders = struct { + const blank: Border = .{ .nw = " ", .n = " ", .ne = " ", .e = " ", .se = " ", .s = " ", .sw = " ", .w = " " }; + const box: Border = .{ .nw = "┌", .n = "─", .ne = "┐", .e = "│", .se = "┘", .s = "─", .sw = "└", .w = "│" }; + }; + + pub const default_static: @This() = .{}; + pub const default = &default_static; + + pub const boxed_static: @This() = .{ + .padding = margins.@"1", + .border = borders.box, + }; + pub const boxed = &boxed_static; +}; + pub const VTable = struct { deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void, send: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) error{Exit}!bool, diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index c40ff2c..4acfc3b 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -26,25 +26,35 @@ widgets: ArrayList(WidgetState), layout_: Layout, layout_empty: bool = true, direction: Direction, -box: ?Widget.Box = null, +deco_box: Widget.Box, ctx: ?*anyopaque = null, on_render: *const fn (ctx: ?*anyopaque, theme: *const Widget.Theme) void = on_render_default, after_render: *const fn (ctx: ?*anyopaque, theme: *const Widget.Theme) void = on_render_default, -on_resize: *const fn (ctx: ?*anyopaque, self: *Self, pos_: Widget.Box) void = on_resize_default, +prepare_resize: *const fn (ctx: ?*anyopaque, self: *Self, box: Widget.Box) Widget.Box = prepare_resize_default, +after_resize: *const fn (ctx: ?*anyopaque, self: *Self, box: Widget.Box) void = after_resize_default, on_layout: *const fn (ctx: ?*anyopaque, self: *Self) Widget.Layout = on_layout_default, +style: *const Widget.Style, pub fn createH(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout) error{OutOfMemory}!*Self { + return createHStyled(allocator, parent, name, layout_, Widget.Style.default); +} + +pub fn createHStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: *const Widget.Style) error{OutOfMemory}!*Self { const self = try allocator.create(Self); errdefer allocator.destroy(self); - self.* = try init(allocator, parent, name, .horizontal, layout_, Box{}); + self.* = try init(allocator, parent, name, .horizontal, layout_, Box{}, style); self.plane.hide(); return self; } pub fn createV(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout) !*Self { + return createVStyled(allocator, parent, name, layout_, Widget.Style.default); +} + +pub fn createVStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: *const Widget.Style) !*Self { const self = try allocator.create(Self); errdefer allocator.destroy(self); - self.* = try init(allocator, parent, name, .vertical, layout_, Box{}); + self.* = try init(allocator, parent, name, .vertical, layout_, Box{}, style); self.plane.hide(); return self; } @@ -57,15 +67,22 @@ pub fn createBox(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: D return self; } -fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, layout_: Layout, box: Box) !Self { - return .{ - .plane = try Plane.init(&box.opts(name), parent), +fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, layout_: Layout, box_: Box, style: *const Widget.Style) !Self { + var self: Self = .{ + .plane = undefined, .parent = parent, .allocator = allocator, .widgets = ArrayList(WidgetState).init(allocator), .layout_ = layout_, .direction = dir, + .style = style, + .deco_box = undefined, }; + self.deco_box = self.from_client_box(box_); + self.plane = try Plane.init(&self.deco_box.opts(name), parent); + if (self.style.padding.top > 0 and self.deco_box.y < 10) + std.log.info("init deco box: {any}", .{self.deco_box}); + return self; } pub fn widget(self: *Self) Widget { @@ -153,6 +170,7 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { }; self.on_render(self.ctx, theme); + self.render_decoration(theme); var more = false; for (self.widgets.items) |*w| @@ -166,6 +184,40 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { fn on_render_default(_: ?*anyopaque, _: *const Widget.Theme) void {} +fn render_decoration(self: *Self, theme: *const Widget.Theme) void { + const style = theme.editor_gutter_modified; + const plane = &self.plane; + const box = self.deco_box; + const padding = self.style.padding; + const border = self.style.border; + + plane.set_style(style); + + if (padding.top > 0 and padding.left > 0) put_at_pos(plane, 0, 0, border.nw); + if (padding.top > 0 and padding.right > 0) put_at_pos(plane, 0, box.w - 1, border.ne); + if (padding.bottom > 0 and padding.left > 0 and box.h > 0) put_at_pos(plane, box.h - 1, 0, border.sw); + if (padding.bottom > 0 and padding.right > 0 and box.h > 0) put_at_pos(plane, box.h - 1, box.w - 1, border.se); + + { + const start: usize = if (padding.left > 0) 1 else 0; + const end: usize = if (padding.right > 0 and box.w > 0) box.w - 1 else box.w; + if (padding.top > 0) for (start..end) |x| put_at_pos(plane, 0, x, border.n); + if (padding.bottom > 0) for (start..end) |x| put_at_pos(plane, box.h - 1, x, border.s); + } + + { + const start: usize = if (padding.top > 0) 1 else 0; + const end: usize = if (padding.bottom > 0 and box.h > 0) box.h - 1 else box.h; + if (padding.left > 0) for (start..end) |y| put_at_pos(plane, y, 0, border.w); + if (padding.right > 0) for (start..end) |y| put_at_pos(plane, y, box.w - 1, border.e); + } +} + +inline fn put_at_pos(plane: *Plane, y: usize, x: usize, egc: []const u8) void { + plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return; + plane.putchar(egc); +} + pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool { if (try m.match(.{ "H", tp.more })) return false; @@ -176,6 +228,13 @@ pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool { return false; } +fn get_size_a_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.h, + .horizontal => pos.w, + }; +} + fn get_size_a(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.h, @@ -183,6 +242,13 @@ fn get_size_a(self: *Self, pos: *Widget.Box) *usize { }; } +fn get_size_b_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.w, + .horizontal => pos.h, + }; +} + fn get_size_b(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.w, @@ -190,6 +256,13 @@ fn get_size_b(self: *Self, pos: *Widget.Box) *usize { }; } +fn get_loc_a_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.y, + .horizontal => pos.x, + }; +} + fn get_loc_a(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.y, @@ -197,6 +270,13 @@ fn get_loc_a(self: *Self, pos: *Widget.Box) *usize { }; } +fn get_loc_b_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.x, + .horizontal => pos.y, + }; +} + fn get_loc_b(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.x, @@ -205,27 +285,66 @@ fn get_loc_b(self: *Self, pos: *Widget.Box) *usize { } fn refresh_layout(self: *Self) void { - return if (self.box) |box| self.handle_resize(box); + return self.handle_resize(self.to_client_box(self.deco_box)); } -pub fn handle_resize(self: *Self, pos: Widget.Box) void { - self.on_resize(self.ctx, self, pos); +pub fn handle_resize(self: *Self, box: Widget.Box) void { + if (self.style.padding.top > 0 and self.deco_box.y < 10) + std.log.info("handle_resize deco box: {any}", .{self.deco_box}); + const client_box_ = self.prepare_resize(self.ctx, self, self.to_client_box(box)); + self.deco_box = self.from_client_box(client_box_); + if (self.style.padding.top > 0 and self.deco_box.y < 10) + std.log.info("prepare_resize deco box: {any}", .{self.deco_box}); + self.do_resize(); + self.after_resize(self.ctx, self, self.to_client_box(self.deco_box)); } -fn on_resize_default(_: ?*anyopaque, self: *Self, pos: Widget.Box) void { - self.resize(pos); +pub inline fn to_client_box(self: *const Self, box_: Widget.Box) Widget.Box { + const padding = self.style.padding; + const total_y_padding = padding.top + padding.bottom; + const total_x_padding = padding.left + padding.right; + var box = box_; + box.y += padding.top; + box.h -= if (box.h > total_y_padding) total_y_padding else box.h; + box.x += padding.left; + box.w -= if (box.w > total_x_padding) total_x_padding else box.w; + return box; } +inline fn from_client_box(self: *const Self, box_: Widget.Box) Widget.Box { + const padding = self.style.padding; + const total_y_padding = padding.top + padding.bottom; + const total_x_padding = padding.left + padding.right; + const y = if (box_.y < padding.top) padding.top else box_.y; + const x = if (box_.x < padding.left) padding.top else box_.x; + var box = box_; + box.y = y - padding.top; + box.h += total_y_padding; + box.x = x - padding.left; + box.w += total_x_padding; + return box; +} + +fn prepare_resize_default(_: ?*anyopaque, _: *Self, box: Widget.Box) Widget.Box { + return box; +} + +fn after_resize_default(_: ?*anyopaque, _: *Self, _: Widget.Box) void {} + fn on_layout_default(_: ?*anyopaque, self: *Self) Widget.Layout { return self.layout_; } -pub fn resize(self: *Self, pos_: Widget.Box) void { - self.box = pos_; - var pos = pos_; - self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return; - self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return; - const total = self.get_size_a(&pos).*; +pub fn resize(self: *Self, box: Widget.Box) void { + return self.handle_resize(box); +} + +fn do_resize(self: *Self) void { + const client_box = self.to_client_box(self.deco_box); + const deco_box = self.deco_box; + self.plane.move_yx(@intCast(deco_box.y), @intCast(deco_box.x)) catch return; + self.plane.resize_simple(@intCast(deco_box.h), @intCast(deco_box.w)) catch return; + const total = self.get_size_a_const(&client_box); var avail = total; var statics: usize = 0; var dynamics: usize = 0; @@ -245,7 +364,7 @@ pub fn resize(self: *Self, pos_: Widget.Box) void { const dyn_size = avail / if (dynamics > 0) dynamics else 1; const rounded: usize = if (dyn_size * dynamics < avail) avail - dyn_size * dynamics else 0; - var cur_loc: usize = self.get_loc_a(&pos).*; + var cur_loc: usize = self.get_loc_a_const(&client_box); var first = true; for (self.widgets.items) |*w| { @@ -261,8 +380,8 @@ pub fn resize(self: *Self, pos_: Widget.Box) void { self.get_loc_a(&w_pos).* = cur_loc; cur_loc += size; - self.get_size_b(&w_pos).* = self.get_size_b(&pos).*; - self.get_loc_b(&w_pos).* = self.get_loc_b(&pos).*; + self.get_size_b(&w_pos).* = self.get_size_b_const(&client_box); + self.get_loc_b(&w_pos).* = self.get_loc_b_const(&client_box); w.widget.resize(w_pos); } } diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index d284a84..499c229 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -33,7 +33,7 @@ logger: log.Logger, query_pending: bool = false, need_reset: bool = false, need_select_first: bool = true, -longest: usize = 0, +longest: usize, commands: Commands = undefined, buffer_manager: ?*BufferManager, @@ -49,7 +49,7 @@ pub fn create(allocator: std.mem.Allocator) !tui.Mode { .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, .on_render = on_render_menu, - .on_resize = on_resize_menu, + .prepare_resize = prepare_resize_menu, }), .logger = log.logger(@typeName(Self)), .inputbox = (try self.menu.add_header(try InputBox.create(*Self, self.allocator, self.menu.menu.parent, .{ @@ -57,12 +57,13 @@ pub fn create(allocator: std.mem.Allocator) !tui.Mode { .label = inputbox_label, }))).dynamic_cast(InputBox.State(*Self)) orelse unreachable, .buffer_manager = tui.get_buffer_manager(), + .longest = inputbox_label.len, }; try self.commands.init(self); try tui.message_filters().add(MessageFilter.bind(self, receive_project_manager)); self.query_pending = true; try project_manager.request_recent_files(max_recent_files); - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = max_menu_width() + 2 }); + self.do_resize(); try mv.floating_views.add(self.modal.widget()); try mv.floating_views.add(self.menu.container_widget); var mode = try keybind.mode("overlay/palette", allocator, .{ @@ -85,7 +86,7 @@ pub fn deinit(self: *Self) void { } inline fn menu_width(self: *Self) usize { - return @max(@min(self.longest, max_menu_width()) + 2, inputbox_label.len + 2); + return @max(@min(self.longest, max_menu_width()) + 5, inputbox_label.len + 2); } inline fn menu_pos_x(self: *Self) usize { @@ -149,8 +150,19 @@ fn on_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: return false; } -fn on_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); +fn prepare_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) Widget.Box { + return self.prepare_resize(); +} + +fn prepare_resize(self: *Self) Widget.Box { + const w = self.menu_width(); + const x = self.menu_pos_x(); + const h = self.menu.menu.widgets.items.len; + return .{ .y = 0, .x = x, .w = w, .h = h }; +} + +fn do_resize(self: *Self) void { + self.menu.resize(self.prepare_resize()); } fn menu_action_open_file(menu: **Menu.State(*Self), button: *Button.State(*Menu.State(*Self))) void { @@ -207,7 +219,7 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void })) { if (self.need_reset) self.reset_results(); try self.add_item(file_name, file_type, file_icon, file_color, matches); - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); + self.do_resize(); if (self.need_select_first) { self.menu.select_down(); self.need_select_first = false; @@ -224,7 +236,7 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void })) { if (self.need_reset) self.reset_results(); try self.add_item(file_name, file_type, file_icon, file_color, null); - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); + self.do_resize(); if (self.need_select_first) { self.menu.select_down(); self.need_select_first = false; diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index 1e0e18a..d85b1c9 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -58,7 +58,8 @@ pub fn Create(options: type) type { .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, .on_render = if (@hasDecl(options, "on_render_menu")) options.on_render_menu else on_render_menu, - .on_resize = on_resize_menu, + .prepare_resize = prepare_resize_menu, + .after_resize = after_resize_menu, .on_scroll = EventHandler.bind(self, Self.on_scroll), .on_click4 = mouse_click_button4, .on_click5 = mouse_click_button5, @@ -146,19 +147,32 @@ pub fn Create(options: type) type { return false; } - fn on_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { - self.do_resize(); - // self.start_query(0) catch {}; + fn prepare_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) Widget.Box { + return self.prepare_resize(); } - fn do_resize(self: *Self) void { + fn prepare_resize(self: *Self) Widget.Box { const screen = tui.screen(); const w = @max(@min(self.longest, max_menu_width) + 2 + 1 + self.longest_hint, options.label.len + 2); const x = if (screen.w > w) (screen.w - w) / 2 else 0; self.view_rows = get_view_rows(screen); const h = @min(self.items + self.menu.header_count, self.view_rows + self.menu.header_count); - self.menu.container.resize(.{ .y = 0, .x = x, .w = w, .h = h }); + return .{ .y = 0, .x = x, .w = w, .h = h }; + } + + fn after_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { + return self.after_resize(); + } + + fn after_resize(self: *Self) void { self.update_scrollbar(); + // self.start_query(0) catch {}; + } + + fn do_resize(self: *Self) void { + const box = self.prepare_resize(); + self.menu.resize(self.menu.container.to_client_box(box)); + self.after_resize(); } fn get_view_rows(screen: Widget.Box) usize { From ae39016f03d5241354cc578fac2518226c9136d1 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 01:10:31 +0200 Subject: [PATCH 21/87] fix: stop rendering widget list contents that are outside of it's box --- src/tui/WidgetList.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index 4acfc3b..6a96f39 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -172,11 +172,17 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { self.on_render(self.ctx, theme); self.render_decoration(theme); + const client_box = self.to_client_box(self.deco_box); + var more = false; - for (self.widgets.items) |*w| + for (self.widgets.items) |*w| { + const widget_box = w.widget.box(); + if (client_box.y + client_box.h <= widget_box.y) break; + if (client_box.x + client_box.w <= widget_box.x) break; if (w.widget.render(theme)) { more = true; - }; + } + } self.after_render(self.ctx, theme); return more; From c12f384a4f66413e9ca127355e98b931dbe8537e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 01:12:15 +0200 Subject: [PATCH 22/87] feat: add thick_box border style --- src/tui/Widget.zig | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 7de2347..bf8fb8e 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -67,11 +67,15 @@ pub const Style = struct { const @"0": Margin = .{ .top = 0, .bottom = 0, .left = 0, .right = 0 }; const @"1": Margin = .{ .top = 1, .bottom = 1, .left = 1, .right = 1 }; const @"2": Margin = .{ .top = 2, .bottom = 2, .left = 2, .right = 2 }; + + const top_bottom_1: Margin = .{ .top = 1, .bottom = 1, .left = 0, .right = 0 }; + const left_right_1: Margin = .{ .top = 0, .bottom = 0, .left = 0, .right = 0 }; }; pub const borders = struct { const blank: Border = .{ .nw = " ", .n = " ", .ne = " ", .e = " ", .se = " ", .s = " ", .sw = " ", .w = " " }; const box: Border = .{ .nw = "┌", .n = "─", .ne = "┐", .e = "│", .se = "┘", .s = "─", .sw = "└", .w = "│" }; + const thick_box: Border = .{ .nw = "▛", .n = "▀", .ne = "▜", .e = "▐", .se = "▟", .s = "▄", .sw = "▙", .w = "▌" }; }; pub const default_static: @This() = .{}; @@ -82,6 +86,18 @@ pub const Style = struct { .border = borders.box, }; pub const boxed = &boxed_static; + + pub const bars_top_bottom_static: @This() = .{ + .padding = margins.top_bottom_1, + .border = borders.thick_box, + }; + pub const bars_top_bottom = &bars_top_bottom_static; + + pub const bars_left_right_static: @This() = .{ + .padding = margins.left_right_1, + .border = borders.box, + }; + pub const bars_left_right = &bars_left_right_static; }; pub const VTable = struct { From d132df2d787af33803e4cb7967b44a89f085a91b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 01:13:11 +0200 Subject: [PATCH 23/87] fix: use client_box correctly in filelist_view --- src/tui/filelist_view.zig | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tui/filelist_view.zig b/src/tui/filelist_view.zig index 6f4e824..cc1e289 100644 --- a/src/tui/filelist_view.zig +++ b/src/tui/filelist_view.zig @@ -33,6 +33,7 @@ view_rows: usize = 0, view_cols: usize = 0, entries: std.ArrayList(Entry) = undefined, selected: ?usize = null, +box: Widget.Box = .{}, const path_column_ratio = 4; @@ -86,9 +87,11 @@ fn scrollbar_style(sb: *scrollbar_v, theme: *const Widget.Theme) Widget.Theme.St pub fn handle_resize(self: *Self, pos: Widget.Box) void { self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return; self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return; - self.menu.container_widget.resize(pos); - self.view_rows = pos.h; - self.view_cols = pos.w; + self.box = pos; + self.menu.container.resize(self.box); + const client_box = self.menu.container.to_client_box(pos); + self.view_rows = client_box.h; + self.view_cols = client_box.w; self.update_scrollbar(); } @@ -107,7 +110,7 @@ pub fn add_item(self: *Self, entry_: Entry) !void { const writer = label.writer(); cbor.writeValue(writer, idx) catch return; self.menu.add_item_with_handler(label.items, handle_menu_action) catch return; - self.menu.container_widget.resize(Widget.Box.from(self.plane)); + self.menu.resize(self.box); self.update_scrollbar(); } From b5a506450b33835cf0212d1674773bdd29cc56a3 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 01:27:58 +0200 Subject: [PATCH 24/87] feat: add more boxed styles --- src/tui/Menu.zig | 2 +- src/tui/Widget.zig | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/tui/Menu.zig b/src/tui/Menu.zig index 813051c..601c4e3 100644 --- a/src/tui/Menu.zig +++ b/src/tui/Menu.zig @@ -60,7 +60,7 @@ pub fn Options(context: type) type { pub fn create(ctx_type: type, allocator: std.mem.Allocator, parent: Plane, opts: Options(ctx_type)) !*State(ctx_type) { const self = try allocator.create(State(ctx_type)); errdefer allocator.destroy(self); - const container = try WidgetList.createHStyled(allocator, parent, @typeName(@This()), .dynamic, Widget.Style.boxed); + const container = try WidgetList.createHStyled(allocator, parent, @typeName(@This()), .dynamic, Widget.Style.thick_boxed); self.* = .{ .allocator = allocator, .menu = try WidgetList.createV(allocator, container.plane, @typeName(@This()), .dynamic), diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index bf8fb8e..a862ca2 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -67,15 +67,21 @@ pub const Style = struct { const @"0": Margin = .{ .top = 0, .bottom = 0, .left = 0, .right = 0 }; const @"1": Margin = .{ .top = 1, .bottom = 1, .left = 1, .right = 1 }; const @"2": Margin = .{ .top = 2, .bottom = 2, .left = 2, .right = 2 }; + const @"3": Margin = .{ .top = 3, .bottom = 3, .left = 3, .right = 3 }; + const @"2/3": Margin = .{ .top = 2, .bottom = 2, .left = 3, .right = 3 }; + const @"2/4": Margin = .{ .top = 2, .bottom = 2, .left = 4, .right = 4 }; const top_bottom_1: Margin = .{ .top = 1, .bottom = 1, .left = 0, .right = 0 }; - const left_right_1: Margin = .{ .top = 0, .bottom = 0, .left = 0, .right = 0 }; + const top_bottom_2: Margin = .{ .top = 2, .bottom = 2, .left = 0, .right = 0 }; + const left_right_1: Margin = .{ .top = 0, .bottom = 0, .left = 1, .right = 1 }; + const left_right_2: Margin = .{ .top = 0, .bottom = 0, .left = 2, .right = 2 }; }; pub const borders = struct { const blank: Border = .{ .nw = " ", .n = " ", .ne = " ", .e = " ", .se = " ", .s = " ", .sw = " ", .w = " " }; const box: Border = .{ .nw = "┌", .n = "─", .ne = "┐", .e = "│", .se = "┘", .s = "─", .sw = "└", .w = "│" }; const thick_box: Border = .{ .nw = "▛", .n = "▀", .ne = "▜", .e = "▐", .se = "▟", .s = "▄", .sw = "▙", .w = "▌" }; + const thick_box_sextant: Border = .{ .nw = "🬕", .n = "🬂", .ne = "🬨", .e = "▐", .se = "🬷", .s = "🬭", .sw = "🬲", .w = "▌" }; }; pub const default_static: @This() = .{}; @@ -87,6 +93,12 @@ pub const Style = struct { }; pub const boxed = &boxed_static; + pub const thick_boxed_static: @This() = .{ + .padding = margins.@"2/4", + .border = borders.thick_box_sextant, + }; + pub const thick_boxed = &thick_boxed_static; + pub const bars_top_bottom_static: @This() = .{ .padding = margins.top_bottom_1, .border = borders.thick_box, From d2c4fb66bda1ccd9653dfc22fc5a4c1b793be6d7 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 01:28:21 +0200 Subject: [PATCH 25/87] fix: typo in WidgetList --- src/tui/WidgetList.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index 6a96f39..dceaf09 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -198,6 +198,7 @@ fn render_decoration(self: *Self, theme: *const Widget.Theme) void { const border = self.style.border; plane.set_style(style); + plane.fill(" "); if (padding.top > 0 and padding.left > 0) put_at_pos(plane, 0, 0, border.nw); if (padding.top > 0 and padding.right > 0) put_at_pos(plane, 0, box.w - 1, border.ne); @@ -322,7 +323,7 @@ inline fn from_client_box(self: *const Self, box_: Widget.Box) Widget.Box { const total_y_padding = padding.top + padding.bottom; const total_x_padding = padding.left + padding.right; const y = if (box_.y < padding.top) padding.top else box_.y; - const x = if (box_.x < padding.left) padding.top else box_.x; + const x = if (box_.x < padding.left) padding.left else box_.x; var box = box_; box.y = y - padding.top; box.h += total_y_padding; From 794f8be2be05a9720046f4602bc204a38cf8e466 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 01:46:19 +0200 Subject: [PATCH 26/87] feat: add more padding styles --- src/tui/Widget.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index a862ca2..0aff980 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -68,8 +68,9 @@ pub const Style = struct { const @"1": Margin = .{ .top = 1, .bottom = 1, .left = 1, .right = 1 }; const @"2": Margin = .{ .top = 2, .bottom = 2, .left = 2, .right = 2 }; const @"3": Margin = .{ .top = 3, .bottom = 3, .left = 3, .right = 3 }; - const @"2/3": Margin = .{ .top = 2, .bottom = 2, .left = 3, .right = 3 }; - const @"2/4": Margin = .{ .top = 2, .bottom = 2, .left = 4, .right = 4 }; + const @"1/2": Margin = .{ .top = 1, .bottom = 1, .left = 2, .right = 1 }; + const @"2/3": Margin = .{ .top = 2, .bottom = 2, .left = 3, .right = 2 }; + const @"2/4": Margin = .{ .top = 2, .bottom = 2, .left = 4, .right = 3 }; const top_bottom_1: Margin = .{ .top = 1, .bottom = 1, .left = 0, .right = 0 }; const top_bottom_2: Margin = .{ .top = 2, .bottom = 2, .left = 0, .right = 0 }; @@ -94,7 +95,7 @@ pub const Style = struct { pub const boxed = &boxed_static; pub const thick_boxed_static: @This() = .{ - .padding = margins.@"2/4", + .padding = margins.@"1/2", .border = borders.thick_box_sextant, }; pub const thick_boxed = &thick_boxed_static; From ea5843bc2c5a7b51568b9448ac67892ddb61f59d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 01:46:38 +0200 Subject: [PATCH 27/87] fix: home screen menu padding --- src/tui/home.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/home.zig b/src/tui/home.zig index e00a16a..ae8f267 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -145,7 +145,7 @@ fn add_menu_command(self: *Self, command_name: []const u8, description: []const _ = try writer.write(leader); try writer.print(" :{s}", .{hint}); const label = fis.getWritten(); - self.menu_w = @max(self.menu_w, label.len + 1); + self.menu_w = @max(self.menu_w, label.len + 1 + menu.container.style.padding.left + menu.container.style.padding.right); } var value = std.ArrayList(u8).init(self.allocator); From deee1afe13f71e61047bafd809b540b6cd1c9712 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 11:47:49 +0200 Subject: [PATCH 28/87] feat: move widget styles to separate module And add a few more border styles. --- src/tui/Widget.zig | 76 +------------------------------------- src/tui/WidgetStyle.zig | 81 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 75 deletions(-) create mode 100644 src/tui/WidgetStyle.zig diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 0aff980..653f2ea 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -10,6 +10,7 @@ pub const Box = @import("Box.zig"); pub const Theme = @import("theme"); pub const themes = @import("themes").themes; pub const scopes = @import("themes").scopes; +pub const Style = @import("WidgetStyle.zig"); ptr: *anyopaque, plane: *Plane, @@ -38,81 +39,6 @@ pub const Layout = union(enum) { } }; -pub const Style = struct { - padding: Margin = margins.@"0", - inner_padding: Margin = margins.@"0", - border: Border = borders.blank, - - pub const PaddingUnit = u16; - - pub const Margin = struct { - top: PaddingUnit, - bottom: PaddingUnit, - left: PaddingUnit, - right: PaddingUnit, - }; - - pub const Border = struct { - nw: []const u8, - n: []const u8, - ne: []const u8, - e: []const u8, - se: []const u8, - s: []const u8, - sw: []const u8, - w: []const u8, - }; - - pub const margins = struct { - const @"0": Margin = .{ .top = 0, .bottom = 0, .left = 0, .right = 0 }; - const @"1": Margin = .{ .top = 1, .bottom = 1, .left = 1, .right = 1 }; - const @"2": Margin = .{ .top = 2, .bottom = 2, .left = 2, .right = 2 }; - const @"3": Margin = .{ .top = 3, .bottom = 3, .left = 3, .right = 3 }; - const @"1/2": Margin = .{ .top = 1, .bottom = 1, .left = 2, .right = 1 }; - const @"2/3": Margin = .{ .top = 2, .bottom = 2, .left = 3, .right = 2 }; - const @"2/4": Margin = .{ .top = 2, .bottom = 2, .left = 4, .right = 3 }; - - const top_bottom_1: Margin = .{ .top = 1, .bottom = 1, .left = 0, .right = 0 }; - const top_bottom_2: Margin = .{ .top = 2, .bottom = 2, .left = 0, .right = 0 }; - const left_right_1: Margin = .{ .top = 0, .bottom = 0, .left = 1, .right = 1 }; - const left_right_2: Margin = .{ .top = 0, .bottom = 0, .left = 2, .right = 2 }; - }; - - pub const borders = struct { - const blank: Border = .{ .nw = " ", .n = " ", .ne = " ", .e = " ", .se = " ", .s = " ", .sw = " ", .w = " " }; - const box: Border = .{ .nw = "┌", .n = "─", .ne = "┐", .e = "│", .se = "┘", .s = "─", .sw = "└", .w = "│" }; - const thick_box: Border = .{ .nw = "▛", .n = "▀", .ne = "▜", .e = "▐", .se = "▟", .s = "▄", .sw = "▙", .w = "▌" }; - const thick_box_sextant: Border = .{ .nw = "🬕", .n = "🬂", .ne = "🬨", .e = "▐", .se = "🬷", .s = "🬭", .sw = "🬲", .w = "▌" }; - }; - - pub const default_static: @This() = .{}; - pub const default = &default_static; - - pub const boxed_static: @This() = .{ - .padding = margins.@"1", - .border = borders.box, - }; - pub const boxed = &boxed_static; - - pub const thick_boxed_static: @This() = .{ - .padding = margins.@"1/2", - .border = borders.thick_box_sextant, - }; - pub const thick_boxed = &thick_boxed_static; - - pub const bars_top_bottom_static: @This() = .{ - .padding = margins.top_bottom_1, - .border = borders.thick_box, - }; - pub const bars_top_bottom = &bars_top_bottom_static; - - pub const bars_left_right_static: @This() = .{ - .padding = margins.left_right_1, - .border = borders.box, - }; - pub const bars_left_right = &bars_left_right_static; -}; - pub const VTable = struct { deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void, send: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) error{Exit}!bool, diff --git a/src/tui/WidgetStyle.zig b/src/tui/WidgetStyle.zig new file mode 100644 index 0000000..5a94afa --- /dev/null +++ b/src/tui/WidgetStyle.zig @@ -0,0 +1,81 @@ +padding: Margin = Margin.@"0", +inner_padding: Margin = Margin.@"0", +border: Border = Border.blank, + +pub const Padding = struct { + pub const Unit = u16; +}; + +pub const Margin = struct { + const Unit = Padding.Unit; + + top: Unit, + bottom: Unit, + left: Unit, + right: Unit, + + const @"0": Margin = .{ .top = 0, .bottom = 0, .left = 0, .right = 0 }; + const @"1": Margin = .{ .top = 1, .bottom = 1, .left = 1, .right = 1 }; + const @"2": Margin = .{ .top = 2, .bottom = 2, .left = 2, .right = 2 }; + const @"3": Margin = .{ .top = 3, .bottom = 3, .left = 3, .right = 3 }; + const @"1/2": Margin = .{ .top = 1, .bottom = 1, .left = 2, .right = 2 }; + const @"2/1": Margin = .{ .top = 2, .bottom = 2, .left = 1, .right = 1 }; + const @"2/3": Margin = .{ .top = 2, .bottom = 2, .left = 3, .right = 3 }; + const @"2/4": Margin = .{ .top = 2, .bottom = 2, .left = 4, .right = 4 }; + + const @"top/bottom/1": Margin = .{ .top = 1, .bottom = 1, .left = 0, .right = 0 }; + const @"top/bottom/2": Margin = .{ .top = 2, .bottom = 2, .left = 0, .right = 0 }; + const @"left/right/1": Margin = .{ .top = 0, .bottom = 0, .left = 1, .right = 1 }; + const @"left/right/2": Margin = .{ .top = 0, .bottom = 0, .left = 2, .right = 2 }; +}; + +pub const Border = struct { + nw: []const u8, + n: []const u8, + ne: []const u8, + e: []const u8, + se: []const u8, + s: []const u8, + sw: []const u8, + w: []const u8, + + const blank: Border = .{ .nw = " ", .n = " ", .ne = " ", .e = " ", .se = " ", .s = " ", .sw = " ", .w = " " }; + const box: Border = .{ .nw = "┌", .n = "─", .ne = "┐", .e = "│", .se = "┘", .s = "─", .sw = "└", .w = "│" }; + const @"rounded box": Border = .{ .nw = "╭", .n = "─", .ne = "╮", .e = "│", .se = "╯", .s = "─", .sw = "╰", .w = "│" }; + const @"double box": Border = .{ .nw = "╔", .n = "═", .ne = "╗", .e = "║", .se = "╝", .s = "═", .sw = "╚", .w = "║" }; + const @"single/double box (top/bottom)": Border = .{ .nw = "╓", .n = "─", .ne = "╖", .e = "║", .se = "╜", .s = "─", .sw = "╙", .w = "║" }; + const @"single/double box (left/right)": Border = .{ .nw = "╒", .n = "═", .ne = "╕", .e = "│", .se = "╛", .s = "═", .sw = "╘", .w = "│" }; + const @"dotted box (braille)": Border = .{ .nw = "⡏", .n = "⠉", .ne = "⢹", .e = "⢸", .se = "⣸", .s = "⣀", .sw = "⣇", .w = "⡇" }; + const @"thick box (half)": Border = .{ .nw = "▛", .n = "▀", .ne = "▜", .e = "▐", .se = "▟", .s = "▄", .sw = "▙", .w = "▌" }; + const @"thick box (sextant)": Border = .{ .nw = "🬕", .n = "🬂", .ne = "🬨", .e = "▐", .se = "🬷", .s = "🬭", .sw = "🬲", .w = "▌" }; + const @"thick box (octant)": Border = .{ .nw = "𜵊", .n = "🮂", .ne = "𜶘", .e = "▐", .se = "𜷕", .s = "▂", .sw = "𜷀", .w = "▌" }; + const @"extra thick box": Border = .{ .nw = "█", .n = "▀", .ne = "█", .e = "█", .se = "█", .s = "▄", .sw = "█", .w = "█" }; + const @"round thick box": Border = .{ .nw = "█", .n = "▀", .ne = "█", .e = "█", .se = "█", .s = "▄", .sw = "█", .w = "█" }; +}; + +pub const default_static: @This() = .{}; +pub const default = &default_static; + +pub const boxed_static: @This() = .{ + .padding = Margin.@"1", + .border = Border.box, +}; +pub const boxed = &boxed_static; + +pub const thick_boxed_static: @This() = .{ + .padding = Margin.@"1/2", + .border = Border.@"thick box (octant)", +}; +pub const thick_boxed = &thick_boxed_static; + +pub const bars_top_bottom_static: @This() = .{ + .padding = Margin.top_bottom_1, + .border = Border.thick_box, +}; +pub const bars_top_bottom = &bars_top_bottom_static; + +pub const bars_left_right_static: @This() = .{ + .padding = Margin.left_right_1, + .border = Border.box, +}; +pub const bars_left_right = &bars_left_right_static; From 5b852fdb3dfb81e33f54b1b337a1490d067ec680 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 12:08:24 +0200 Subject: [PATCH 29/87] fix: prevent crash on invalid project directory --- src/project_manager.zig | 3 ++- src/tui/MessageFilter.zig | 1 + src/tui/mainview.zig | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/project_manager.zig b/src/project_manager.zig index 1c4d6e5..925294a 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -27,7 +27,7 @@ const OutOfMemoryError = error{OutOfMemory}; const FileSystemError = error{FileSystem}; const SetCwdError = if (builtin.os.tag == .windows) error{UnrecognizedVolume} else error{}; const CallError = tp.CallError; -const ProjectManagerError = (SpawnError || error{ProjectManagerFailed}); +const ProjectManagerError = (SpawnError || error{ ProjectManagerFailed, InvalidProjectDirectory }); pub fn get() SpawnError!Self { const pid = tp.env.get().proc(module_name); @@ -63,6 +63,7 @@ pub fn open(rel_project_directory: []const u8) (ProjectManagerError || FileSyste const project_directory = std.fs.cwd().realpath(rel_project_directory, &path_buf) catch "(none)"; const current_project = tp.env.get().str("project"); if (std.mem.eql(u8, current_project, project_directory)) return; + if (!root.is_directory(project_directory)) return error.InvalidProjectDirectory; var dir = try std.fs.openDirAbsolute(project_directory, .{}); try dir.setAsCwd(); dir.close(); diff --git a/src/tui/MessageFilter.zig b/src/tui/MessageFilter.zig index afc1a37..6676e12 100644 --- a/src/tui/MessageFilter.zig +++ b/src/tui/MessageFilter.zig @@ -14,6 +14,7 @@ pub const Error = (cbor.Error || cbor.JsonEncodeError || error{ ThespianSpawnFailed, NoProject, ProjectManagerFailed, + InvalidProjectDirectory, SendFailed, }); diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index c726c6f..197fbcf 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -322,6 +322,7 @@ const cmds = struct { if (!try ctx.args.match(.{tp.extract(&project_dir)})) return; try self.check_all_not_dirty(); + try project_manager.open(project_dir); for (self.editors.items) |editor| { editor.clear_diagnostics(); try editor.close_file(.{}); @@ -332,7 +333,6 @@ const cmds = struct { try self.toggle_panel_view(filelist_view, false); self.buffer_manager.deinit(); self.buffer_manager = Buffer.Manager.init(self.allocator); - try project_manager.open(project_dir); const project = tp.env.get().str("project"); tui.rdr().set_terminal_working_directory(project); if (self.top_bar) |bar| _ = try bar.msg(.{ "PRJ", "open" }); From 21f7b1497040ec212386e174471a340f532423a4 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 12:09:14 +0200 Subject: [PATCH 30/87] refactor: remove widget debug output --- src/tui/WidgetList.zig | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index dceaf09..96fb03c 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -80,8 +80,6 @@ fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, }; self.deco_box = self.from_client_box(box_); self.plane = try Plane.init(&self.deco_box.opts(name), parent); - if (self.style.padding.top > 0 and self.deco_box.y < 10) - std.log.info("init deco box: {any}", .{self.deco_box}); return self; } @@ -296,12 +294,8 @@ fn refresh_layout(self: *Self) void { } pub fn handle_resize(self: *Self, box: Widget.Box) void { - if (self.style.padding.top > 0 and self.deco_box.y < 10) - std.log.info("handle_resize deco box: {any}", .{self.deco_box}); const client_box_ = self.prepare_resize(self.ctx, self, self.to_client_box(box)); self.deco_box = self.from_client_box(client_box_); - if (self.style.padding.top > 0 and self.deco_box.y < 10) - std.log.info("prepare_resize deco box: {any}", .{self.deco_box}); self.do_resize(); self.after_resize(self.ctx, self, self.to_client_box(self.deco_box)); } From fbc49c3dab8c4362ed07f928c8235681c36006d4 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 12:54:00 +0200 Subject: [PATCH 31/87] fix: home menu length --- src/tui/home.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/home.zig b/src/tui/home.zig index ae8f267..198d68f 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -329,7 +329,7 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { fn position_menu(self: *Self, y: usize, x: usize) void { const box = Widget.Box.from(self.plane); - self.menu.resize(.{ .y = box.y + y, .x = box.x + x, .w = self.menu_w }); + self.menu.resize(.{ .y = box.y + y, .x = box.x + x, .w = self.menu_w, .h = self.menu_len }); } fn center(self: *Self, non_centered: usize, w: usize) usize { From bcfd17a0e2ab58a3b411d8d711e89b8277efbfdd Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 12:58:05 +0200 Subject: [PATCH 32/87] feat: select widget styles based on widget type --- src/tui/Menu.zig | 3 +- src/tui/WidgetList.zig | 54 ++++++++++++++-------------- src/tui/WidgetStyle.zig | 34 +++++++++++++++--- src/tui/filelist_view.zig | 5 ++- src/tui/home.zig | 11 ++++-- src/tui/mode/overlay/open_recent.zig | 2 ++ src/tui/mode/overlay/palette.zig | 9 +++-- 7 files changed, 81 insertions(+), 37 deletions(-) diff --git a/src/tui/Menu.zig b/src/tui/Menu.zig index 601c4e3..58b832b 100644 --- a/src/tui/Menu.zig +++ b/src/tui/Menu.zig @@ -14,6 +14,7 @@ pub const scroll_lines = 3; pub fn Options(context: type) type { return struct { ctx: Context, + style: Widget.Style.Type, on_click: *const fn (ctx: context, button: *Button.State(*State(Context))) void = do_nothing, on_click4: *const fn (menu: **State(Context), button: *Button.State(*State(Context))) void = do_nothing_click, @@ -60,7 +61,7 @@ pub fn Options(context: type) type { pub fn create(ctx_type: type, allocator: std.mem.Allocator, parent: Plane, opts: Options(ctx_type)) !*State(ctx_type) { const self = try allocator.create(State(ctx_type)); errdefer allocator.destroy(self); - const container = try WidgetList.createHStyled(allocator, parent, @typeName(@This()), .dynamic, Widget.Style.thick_boxed); + const container = try WidgetList.createHStyled(allocator, parent, @typeName(@This()), .dynamic, opts.style); self.* = .{ .allocator = allocator, .menu = try WidgetList.createV(allocator, container.plane, @typeName(@This()), .dynamic), diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index 96fb03c..012dcd1 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -33,13 +33,13 @@ after_render: *const fn (ctx: ?*anyopaque, theme: *const Widget.Theme) void = on prepare_resize: *const fn (ctx: ?*anyopaque, self: *Self, box: Widget.Box) Widget.Box = prepare_resize_default, after_resize: *const fn (ctx: ?*anyopaque, self: *Self, box: Widget.Box) void = after_resize_default, on_layout: *const fn (ctx: ?*anyopaque, self: *Self) Widget.Layout = on_layout_default, -style: *const Widget.Style, +style: Widget.Style.Type, pub fn createH(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout) error{OutOfMemory}!*Self { - return createHStyled(allocator, parent, name, layout_, Widget.Style.default); + return createHStyled(allocator, parent, name, layout_, .none); } -pub fn createHStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: *const Widget.Style) error{OutOfMemory}!*Self { +pub fn createHStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: Widget.Style.Type) error{OutOfMemory}!*Self { const self = try allocator.create(Self); errdefer allocator.destroy(self); self.* = try init(allocator, parent, name, .horizontal, layout_, Box{}, style); @@ -48,10 +48,10 @@ pub fn createHStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, la } pub fn createV(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout) !*Self { - return createVStyled(allocator, parent, name, layout_, Widget.Style.default); + return createVStyled(allocator, parent, name, layout_, .none); } -pub fn createVStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: *const Widget.Style) !*Self { +pub fn createVStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: Widget.Style.Type) !*Self { const self = try allocator.create(Self); errdefer allocator.destroy(self); self.* = try init(allocator, parent, name, .vertical, layout_, Box{}, style); @@ -67,7 +67,7 @@ pub fn createBox(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: D return self; } -fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, layout_: Layout, box_: Box, style: *const Widget.Style) !Self { +fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, layout_: Layout, box_: Box, style: Widget.Style.Type) !Self { var self: Self = .{ .plane = undefined, .parent = parent, @@ -78,7 +78,8 @@ fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, .style = style, .deco_box = undefined, }; - self.deco_box = self.from_client_box(box_); + const padding = Widget.Style.from_type(self.style).padding; + self.deco_box = self.from_client_box(box_, padding); self.plane = try Plane.init(&self.deco_box.opts(name), parent); return self; } @@ -162,15 +163,17 @@ pub fn update(self: *Self) void { } pub fn render(self: *Self, theme: *const Widget.Theme) bool { + const widget_style = Widget.Style.from_type(self.style); + const padding = widget_style.padding; for (self.widgets.items) |*w| if (!w.layout.eql(w.widget.layout())) { - self.refresh_layout(); + self.refresh_layout(padding); break; }; self.on_render(self.ctx, theme); - self.render_decoration(theme); + self.render_decoration(theme, widget_style); - const client_box = self.to_client_box(self.deco_box); + const client_box = self.to_client_box(self.deco_box, padding); var more = false; for (self.widgets.items) |*w| { @@ -188,12 +191,12 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { fn on_render_default(_: ?*anyopaque, _: *const Widget.Theme) void {} -fn render_decoration(self: *Self, theme: *const Widget.Theme) void { - const style = theme.editor_gutter_modified; +fn render_decoration(self: *Self, theme: *const Widget.Theme, widget_style: *const Widget.Style) void { + const style = Widget.Style.theme_style_from_type(self.style, theme); + const padding = widget_style.padding; + const border = widget_style.border; const plane = &self.plane; const box = self.deco_box; - const padding = self.style.padding; - const border = self.style.border; plane.set_style(style); plane.fill(" "); @@ -289,19 +292,19 @@ fn get_loc_b(self: *Self, pos: *Widget.Box) *usize { }; } -fn refresh_layout(self: *Self) void { - return self.handle_resize(self.to_client_box(self.deco_box)); +fn refresh_layout(self: *Self, padding: Widget.Style.Margin) void { + return self.handle_resize(self.to_client_box(self.deco_box, padding)); } pub fn handle_resize(self: *Self, box: Widget.Box) void { - const client_box_ = self.prepare_resize(self.ctx, self, self.to_client_box(box)); - self.deco_box = self.from_client_box(client_box_); - self.do_resize(); - self.after_resize(self.ctx, self, self.to_client_box(self.deco_box)); + const padding = Widget.Style.from_type(self.style).padding; + const client_box_ = self.prepare_resize(self.ctx, self, self.to_client_box(box, padding)); + self.deco_box = self.from_client_box(client_box_, padding); + self.do_resize(padding); + self.after_resize(self.ctx, self, self.to_client_box(self.deco_box, padding)); } -pub inline fn to_client_box(self: *const Self, box_: Widget.Box) Widget.Box { - const padding = self.style.padding; +pub inline fn to_client_box(_: *const Self, box_: Widget.Box, padding: Widget.Style.Margin) Widget.Box { const total_y_padding = padding.top + padding.bottom; const total_x_padding = padding.left + padding.right; var box = box_; @@ -312,8 +315,7 @@ pub inline fn to_client_box(self: *const Self, box_: Widget.Box) Widget.Box { return box; } -inline fn from_client_box(self: *const Self, box_: Widget.Box) Widget.Box { - const padding = self.style.padding; +inline fn from_client_box(_: *const Self, box_: Widget.Box, padding: Widget.Style.Margin) Widget.Box { const total_y_padding = padding.top + padding.bottom; const total_x_padding = padding.left + padding.right; const y = if (box_.y < padding.top) padding.top else box_.y; @@ -340,8 +342,8 @@ pub fn resize(self: *Self, box: Widget.Box) void { return self.handle_resize(box); } -fn do_resize(self: *Self) void { - const client_box = self.to_client_box(self.deco_box); +fn do_resize(self: *Self, padding: Widget.Style.Margin) void { + const client_box = self.to_client_box(self.deco_box, padding); const deco_box = self.deco_box; self.plane.move_yx(@intCast(deco_box.y), @intCast(deco_box.x)) catch return; self.plane.resize_simple(@intCast(deco_box.h), @intCast(deco_box.w)) catch return; diff --git a/src/tui/WidgetStyle.zig b/src/tui/WidgetStyle.zig index 5a94afa..818c913 100644 --- a/src/tui/WidgetStyle.zig +++ b/src/tui/WidgetStyle.zig @@ -1,7 +1,13 @@ padding: Margin = Margin.@"0", -inner_padding: Margin = Margin.@"0", border: Border = Border.blank, +pub const Type = enum { + none, + palette, + panel, + home, +}; + pub const Padding = struct { pub const Unit = u16; }; @@ -69,13 +75,33 @@ pub const thick_boxed_static: @This() = .{ pub const thick_boxed = &thick_boxed_static; pub const bars_top_bottom_static: @This() = .{ - .padding = Margin.top_bottom_1, - .border = Border.thick_box, + .padding = Margin.@"top/bottom/1", + .border = Border.@"thick box (octant)", }; pub const bars_top_bottom = &bars_top_bottom_static; pub const bars_left_right_static: @This() = .{ - .padding = Margin.left_right_1, + .padding = Margin.@"left/right/1", .border = Border.box, }; pub const bars_left_right = &bars_left_right_static; + +pub fn from_type(style_type: Type) *const @This() { + return switch (style_type) { + .none => default, + .palette => thick_boxed, + .panel => default, + .home => default, + }; +} + +const Widget = @import("Widget.zig"); + +pub fn theme_style_from_type(style_type: Type, theme: *const Widget.Theme) Widget.Theme.Style { + return switch (style_type) { + .none => theme.editor, + .palette => .{ .fg = theme.editor_widget_border.fg, .bg = theme.editor_widget.bg }, + .panel => .{ .fg = theme.editor_widget_border.fg, .bg = theme.editor.bg }, + .home => .{ .fg = theme.editor_widget_border.fg, .bg = theme.editor.bg }, + }; +} diff --git a/src/tui/filelist_view.zig b/src/tui/filelist_view.zig index cc1e289..2e65a0c 100644 --- a/src/tui/filelist_view.zig +++ b/src/tui/filelist_view.zig @@ -36,6 +36,7 @@ selected: ?usize = null, box: Widget.Box = .{}, const path_column_ratio = 4; +const widget_style_type: Widget.Style.Type = .panel; const Entry = struct { path: []const u8, @@ -57,6 +58,7 @@ pub fn create(allocator: Allocator, parent: Plane) !Widget { .entries = std.ArrayList(Entry).init(allocator), .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, + .style = widget_style_type, .on_render = handle_render_menu, .on_scroll = EventHandler.bind(self, Self.handle_scroll), .on_click4 = mouse_click_button4, @@ -85,11 +87,12 @@ fn scrollbar_style(sb: *scrollbar_v, theme: *const Widget.Theme) Widget.Theme.St } pub fn handle_resize(self: *Self, pos: Widget.Box) void { + const padding = Widget.Style.from_type(widget_style_type).padding; self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return; self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return; self.box = pos; self.menu.container.resize(self.box); - const client_box = self.menu.container.to_client_box(pos); + const client_box = self.menu.container.to_client_box(pos, padding); self.view_rows = client_box.h; self.view_cols = client_box.w; self.update_scrollbar(); diff --git a/src/tui/home.zig b/src/tui/home.zig index 198d68f..686c411 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -79,6 +79,8 @@ home_style_bufs: [][]const u8, const Self = @This(); +const widget_style_type: Widget.Style.Type = .home; + pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { const logger = log.logger("home"); const self = try allocator.create(Self); @@ -95,7 +97,11 @@ pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { .allocator = allocator, .parent = parent.plane.*, .plane = n, - .menu = try Menu.create(*Self, allocator, w.plane.*, .{ .ctx = self, .on_render = menu_on_render }), + .menu = try Menu.create(*Self, allocator, w.plane.*, .{ + .ctx = self, + .style = widget_style_type, + .on_render = menu_on_render, + }), .input_namespace = keybind.get_namespace(), .home_style = home_style, .home_style_bufs = home_style_bufs, @@ -145,7 +151,8 @@ fn add_menu_command(self: *Self, command_name: []const u8, description: []const _ = try writer.write(leader); try writer.print(" :{s}", .{hint}); const label = fis.getWritten(); - self.menu_w = @max(self.menu_w, label.len + 1 + menu.container.style.padding.left + menu.container.style.padding.right); + const padding = Widget.Style.from_type(widget_style_type).padding; + self.menu_w = @max(self.menu_w, label.len + 1 + padding.left + padding.right); } var value = std.ArrayList(u8).init(self.allocator); diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index 499c229..0582129 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -23,6 +23,7 @@ const ModalBackground = @import("../../ModalBackground.zig"); const Self = @This(); const max_recent_files: usize = 25; +const widget_style_type: Widget.Style.Type = .palette; allocator: std.mem.Allocator, f: usize = 0, @@ -48,6 +49,7 @@ pub fn create(allocator: std.mem.Allocator) !tui.Mode { .modal = try ModalBackground.create(*Self, allocator, tui.mainview_widget(), .{ .ctx = self }), .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, + .style = widget_style_type, .on_render = on_render_menu, .prepare_resize = prepare_resize_menu, }), diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index d85b1c9..caa79c1 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -20,6 +20,7 @@ const ModalBackground = @import("../../ModalBackground.zig"); pub const Menu = @import("../../Menu.zig"); const max_menu_width = 80; +const widget_style_type: Widget.Style.Type = .palette; pub fn Create(options: type) type { return struct { @@ -57,6 +58,7 @@ pub fn Create(options: type) type { }), .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, + .style = widget_style_type, .on_render = if (@hasDecl(options, "on_render_menu")) options.on_render_menu else on_render_menu, .prepare_resize = prepare_resize_menu, .after_resize = after_resize_menu, @@ -169,9 +171,9 @@ pub fn Create(options: type) type { // self.start_query(0) catch {}; } - fn do_resize(self: *Self) void { + fn do_resize(self: *Self, padding: Widget.Style.Margin) void { const box = self.prepare_resize(); - self.menu.resize(self.menu.container.to_client_box(box)); + self.menu.resize(self.menu.container.to_client_box(box, padding)); self.after_resize(); } @@ -253,7 +255,8 @@ pub fn Create(options: type) type { var i = n; while (i > 0) : (i -= 1) self.menu.select_down(); - self.do_resize(); + const padding = Widget.Style.from_type(widget_style_type).padding; + self.do_resize(padding); tui.refresh_hover(); self.selection_updated(); } From b46e6edbcae239cd265a9163158a01eb52757abb Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 14:02:52 +0200 Subject: [PATCH 33/87] fix: add padding to home menu length --- src/tui/home.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tui/home.zig b/src/tui/home.zig index 686c411..d4b615e 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -123,6 +123,8 @@ pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { self.max_desc_len = @max(self.max_desc_len, description.len + hint.len + 5); try self.add_menu_command(command_name, description, hint, self.menu); } + const padding = Widget.Style.from_type(widget_style_type).padding; + self.menu_len += padding.top + padding.bottom; self.position_menu(15, 9); return w; } From ea1ae2228ebd28b2b528f325547790497e17337c Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 14:03:07 +0200 Subject: [PATCH 34/87] fix: home menu rendering --- src/tui/home.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/home.zig b/src/tui/home.zig index d4b615e..ba47db3 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -332,7 +332,7 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { _ = self.plane.print("{s}", .{debug_warning_text}) catch return false; } - const more = self.menu.render(theme); + const more = self.menu.container.render(theme); return more or self.fire != null; } From d07f0f5f352f4b73e1c661342c2d432219d6ad0b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 14:03:42 +0200 Subject: [PATCH 35/87] feat: tweak home and palette styles --- src/tui/WidgetStyle.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tui/WidgetStyle.zig b/src/tui/WidgetStyle.zig index 818c913..557be81 100644 --- a/src/tui/WidgetStyle.zig +++ b/src/tui/WidgetStyle.zig @@ -82,16 +82,16 @@ pub const bars_top_bottom = &bars_top_bottom_static; pub const bars_left_right_static: @This() = .{ .padding = Margin.@"left/right/1", - .border = Border.box, + .border = Border.@"thick box (octant)", }; pub const bars_left_right = &bars_left_right_static; pub fn from_type(style_type: Type) *const @This() { return switch (style_type) { .none => default, - .palette => thick_boxed, + .palette => bars_top_bottom, .panel => default, - .home => default, + .home => bars_left_right, }; } From 38236bd93a0ec2f2c1cb2a17412e1911569c4c3a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 14:40:59 +0200 Subject: [PATCH 36/87] refactor: Buffer.Manager.get_buffer_for_file can be const --- src/buffer/Manager.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig index 9097d58..86422b8 100644 --- a/src/buffer/Manager.zig +++ b/src/buffer/Manager.zig @@ -80,7 +80,7 @@ pub fn extract_state(self: *Self, iter: *[]const u8) !void { } } -pub fn get_buffer_for_file(self: *Self, file_path: []const u8) ?*Buffer { +pub fn get_buffer_for_file(self: *const Self, file_path: []const u8) ?*Buffer { return self.buffers.get(file_path); } From f3296482d08a2de3d07975e9595dd4609bc5fb08 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 14:43:30 +0200 Subject: [PATCH 37/87] refactor: unify file icon rendering --- src/tui/mode/overlay/file_type_palette.zig | 7 +++---- src/tui/tui.zig | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tui/mode/overlay/file_type_palette.zig b/src/tui/mode/overlay/file_type_palette.zig index 90f1305..20e3983 100644 --- a/src/tui/mode/overlay/file_type_palette.zig +++ b/src/tui/mode/overlay/file_type_palette.zig @@ -94,10 +94,9 @@ pub fn Variant(comptime command: []const u8, comptime label_: []const u8, allow_ if (!(cbor.matchString(&iter, &description_) catch false)) @panic("invalid file_type description"); if (!(cbor.matchString(&iter, &icon) catch false)) @panic("invalid file_type icon"); if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid file_type color"); - if (tui.config().show_fileicons) { - tui.render_file_icon(&button.plane, icon, color); - _ = button.plane.print(" ", .{}) catch {}; - } + + tui.render_file_icon(&button.plane, icon, color); + button.plane.set_style(style_label); _ = button.plane.print("{s} ", .{description_}) catch {}; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 0969e7c..f68201c 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1437,6 +1437,7 @@ pub fn render_file_icon(self: *renderer.Plane, icon: []const u8, color: u24) voi _ = self.cell_load(&cell, icon) catch {}; _ = self.putc(&cell) catch {}; self.cursor_move_rel(0, 1) catch {}; + _ = self.print(" ", .{}) catch {}; } pub fn render_match_cell(self: *renderer.Plane, y: usize, x: usize, theme_: *const Widget.Theme) !void { From c50ab782ec122ae4d229d467776a892341cee2ae Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 14:44:03 +0200 Subject: [PATCH 38/87] refactor: share file item menu rendering --- src/tui/mode/overlay/buffer_palette.zig | 54 +-------------------- src/tui/mode/overlay/open_recent.zig | 64 ++++--------------------- src/tui/tui.zig | 58 ++++++++++++++++++++++ 3 files changed, 70 insertions(+), 106 deletions(-) diff --git a/src/tui/mode/overlay/buffer_palette.zig b/src/tui/mode/overlay/buffer_palette.zig index 794ca16..2d73a39 100644 --- a/src/tui/mode/overlay/buffer_palette.zig +++ b/src/tui/mode/overlay/buffer_palette.zig @@ -12,8 +12,6 @@ const Widget = @import("../../Widget.zig"); pub const label = "Switch buffers"; pub const name = " buffer"; pub const description = "buffer"; -const dirty_indicator = ""; -const hidden_indicator = "-"; pub const Entry = struct { label: []const u8, @@ -27,12 +25,7 @@ pub fn load_entries(palette: *Type) !usize { const buffers = try buffer_manager.list_most_recently_used(palette.allocator); defer palette.allocator.free(buffers); for (buffers) |buffer| { - const indicator = if (buffer.is_dirty()) - dirty_indicator - else if (buffer.is_hidden()) - hidden_indicator - else - ""; + const indicator = tui.get_buffer_state_indicator(buffer); (try palette.entries.addOne()).* = .{ .label = buffer.get_file_path(), .icon = buffer.file_type_icon orelse "", @@ -61,50 +54,7 @@ pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !v } pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget.Theme, selected: bool) bool { - const style_base = theme.editor_widget; - const style_label = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget; - const style_hint = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_label; - button.plane.set_base_style(style_base); - button.plane.erase(); - button.plane.home(); - button.plane.set_style(style_label); - if (button.active or button.hover or selected) { - button.plane.fill(" "); - button.plane.home(); - } - - button.plane.set_style(style_hint); - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s}", .{pointer}) catch {}; - - var iter = button.opts.label; - var file_path_: []const u8 = undefined; - var icon: []const u8 = undefined; - var color: u24 = undefined; - if (!(cbor.matchString(&iter, &file_path_) catch false)) @panic("invalid buffer file path"); - if (!(cbor.matchString(&iter, &icon) catch false)) @panic("invalid buffer file type icon"); - if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid buffer file type color"); - if (tui.config().show_fileicons) { - tui.render_file_icon(&button.plane, icon, color); - _ = button.plane.print(" ", .{}) catch {}; - } - button.plane.set_style(style_label); - _ = button.plane.print(" {s} ", .{file_path_}) catch {}; - - var indicator: []const u8 = undefined; - if (!(cbor.matchString(&iter, &indicator) catch false)) - indicator = ""; - button.plane.set_style(style_hint); - _ = button.plane.print_aligned_right(0, "{s} ", .{indicator}) catch {}; - - var index: usize = 0; - var len = cbor.decodeArrayHeader(&iter) catch return false; - while (len > 0) : (len -= 1) { - if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { - tui.render_match_cell(&button.plane, 0, index + 4, theme) catch break; - } else break; - } - return false; + return tui.render_file_item_cbor(&button.plane, button.opts.label, button.active, selected, button.hover, theme); } fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index 0582129..58675e4 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -88,7 +88,7 @@ pub fn deinit(self: *Self) void { } inline fn menu_width(self: *Self) usize { - return @max(@min(self.longest, max_menu_width()) + 5, inputbox_label.len + 2); + return @max(@min(self.longest + 1, max_menu_width()) + 5, inputbox_label.len + 2); } inline fn menu_pos_x(self: *Self) usize { @@ -102,54 +102,8 @@ inline fn max_menu_width() usize { return @max(15, width - (width / 5)); } -fn on_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool { - const style_base = theme.editor_widget; - const style_label = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget; - const style_keybind = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_base; - button.plane.set_base_style(style_base); - button.plane.erase(); - button.plane.home(); - button.plane.set_style(style_label); - if (button.active or button.hover or selected) { - button.plane.fill(" "); - button.plane.home(); - } - var file_path: []const u8 = undefined; - var file_type: []const u8 = undefined; - var file_icon: []const u8 = undefined; - var file_color: u24 = undefined; - var iter = button.opts.label; // label contains cbor, first the file name, then multiple match indexes - if (!(cbor.matchString(&iter, &file_path) catch false)) file_path = "#ERROR#"; - if (!(cbor.matchString(&iter, &file_type) catch false)) file_type = file_type_config.default.name; - if (!(cbor.matchString(&iter, &file_icon) catch false)) file_icon = file_type_config.default.icon; - if (!(cbor.matchInt(u24, &iter, &file_color) catch false)) file_icon = file_type_config.default.icon; - - button.plane.set_style(style_keybind); - const dirty = if (self.buffer_manager) |bm| if (bm.is_buffer_dirty(file_path)) "" else " " else " "; - const pointer = if (selected) "⏵" else dirty; - _ = button.plane.print("{s}", .{pointer}) catch {}; - - if (tui.config().show_fileicons) { - tui.render_file_icon(&button.plane, file_icon, file_color); - _ = button.plane.print(" ", .{}) catch {}; - } - - var buf: [std.fs.max_path_bytes]u8 = undefined; - var removed_prefix: usize = 0; - const max_len = max_menu_width() - 2; - button.plane.set_style(style_label); - _ = button.plane.print("{s} ", .{ - if (file_path.len > max_len) root.shorten_path(&buf, file_path, &removed_prefix, max_len) else file_path, - }) catch {}; - - var index: usize = 0; - var len = cbor.decodeArrayHeader(&iter) catch return false; - while (len > 0) : (len -= 1) { - if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { - tui.render_match_cell(&button.plane, 0, index + 4, theme) catch break; - } else break; - } - return false; +fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool { + return tui.render_file_item_cbor(&button.plane, button.opts.label, button.active, selected, button.hover, theme); } fn prepare_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) Widget.Box { @@ -178,19 +132,19 @@ fn menu_action_open_file(menu: **Menu.State(*Self), button: *Button.State(*Menu. fn add_item( self: *Self, file_name: []const u8, - file_type: []const u8, file_icon: []const u8, file_color: u24, + indicator: []const u8, matches: ?[]const u8, ) !void { var label = std.ArrayList(u8).init(self.allocator); defer label.deinit(); const writer = label.writer(); try cbor.writeValue(writer, file_name); - try cbor.writeValue(writer, file_type); try cbor.writeValue(writer, file_icon); try cbor.writeValue(writer, file_color); - if (matches) |cb| _ = try writer.write(cb); + try cbor.writeValue(writer, indicator); + if (matches) |cb| _ = try writer.write(cb) else try cbor.writeValue(writer, &[_]usize{}); try self.menu.add_item_with_handler(label.items, menu_action_open_file); } @@ -220,7 +174,8 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void tp.extract_cbor(&matches), })) { if (self.need_reset) self.reset_results(); - try self.add_item(file_name, file_type, file_icon, file_color, matches); + const indicator = if (self.buffer_manager) |bm| tui.get_file_state_indicator(bm, file_name) else ""; + try self.add_item(file_name, file_icon, file_color, indicator, matches); self.do_resize(); if (self.need_select_first) { self.menu.select_down(); @@ -237,7 +192,8 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void tp.extract(&file_color), })) { if (self.need_reset) self.reset_results(); - try self.add_item(file_name, file_type, file_icon, file_color, null); + const indicator = if (self.buffer_manager) |bm| tui.get_file_state_indicator(bm, file_name) else ""; + try self.add_item(file_name, file_icon, file_color, indicator, null); self.do_resize(); if (self.need_select_first) { self.menu.select_down(); diff --git a/src/tui/tui.zig b/src/tui/tui.zig index f68201c..e699767 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1428,7 +1428,19 @@ pub fn message(comptime fmt: anytype, args: anytype) void { tp.self_pid().send(.{ "message", std.fmt.bufPrint(&buf, fmt, args) catch @panic("too large") }) catch {}; } +const dirty_indicator = ""; +const hidden_indicator = "-"; + +pub fn get_file_state_indicator(buffer_manager: *const @import("Buffer").Manager, file_name: []const u8) []const u8 { + return if (buffer_manager.get_buffer_for_file(file_name)) |buffer| get_buffer_state_indicator(buffer) else ""; +} + +pub fn get_buffer_state_indicator(buffer: *const @import("Buffer")) []const u8 { + return if (buffer.is_dirty()) dirty_indicator else if (buffer.is_hidden()) hidden_indicator else ""; +} + pub fn render_file_icon(self: *renderer.Plane, icon: []const u8, color: u24) void { + if (!config().show_fileicons) return; var cell = self.cell_init(); _ = self.at_cursor_cell(&cell) catch return; if (!(color == 0xFFFFFF or color == 0x000000 or color == 0x000001)) { @@ -1448,6 +1460,52 @@ pub fn render_match_cell(self: *renderer.Plane, y: usize, x: usize, theme_: *con _ = self.putc(&cell) catch {}; } +pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, active: bool, selected: bool, hover: bool, theme_: *const Widget.Theme) bool { + const style_base = theme_.editor_widget; + const style_label = if (active) theme_.editor_cursor else if (hover or selected) theme_.editor_selection else theme_.editor_widget; + const style_hint = if (find_scope_style(theme_, "entity.name")) |sty| sty.style else style_label; + self.set_base_style(style_base); + self.erase(); + self.home(); + self.set_style(style_label); + if (active or hover or selected) { + self.fill(" "); + self.home(); + } + + self.set_style(style_hint); + const pointer = if (selected) "⏵" else " "; + _ = self.print("{s}", .{pointer}) catch {}; + + var iter = file_item_cbor; + var file_path_: []const u8 = undefined; + var icon: []const u8 = undefined; + var color: u24 = undefined; + if (!(cbor.matchString(&iter, &file_path_) catch false)) @panic("invalid buffer file path"); + if (!(cbor.matchString(&iter, &icon) catch false)) @panic("invalid buffer file type icon"); + if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid buffer file type color"); + + render_file_icon(self, icon, color); + + self.set_style(style_label); + _ = self.print("{s} ", .{file_path_}) catch {}; + + var indicator: []const u8 = undefined; + if (!(cbor.matchString(&iter, &indicator) catch false)) + indicator = ""; + self.set_style(style_hint); + _ = self.print_aligned_right(0, "{s} ", .{indicator}) catch {}; + + var index: usize = 0; + var len = cbor.decodeArrayHeader(&iter) catch return false; + while (len > 0) : (len -= 1) { + if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { + render_match_cell(self, 0, index + 4, theme_) catch break; + } else break; + } + return false; +} + 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| { From 4d2c7d8a8ced46ff49dd9d9fb21a2b45ae1261ac Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 17:33:58 +0200 Subject: [PATCH 39/87] refactor: unify list pointer rendering --- src/tui/filelist_view.zig | 4 ++-- src/tui/home.zig | 6 +++--- src/tui/mode/overlay/file_type_palette.zig | 3 +-- src/tui/mode/overlay/open_recent.zig | 2 +- src/tui/mode/overlay/palette.zig | 7 +++---- src/tui/mode/overlay/task_palette.zig | 3 +-- src/tui/tui.zig | 10 +++++++--- 7 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/tui/filelist_view.zig b/src/tui/filelist_view.zig index 2e65a0c..4108be0 100644 --- a/src/tui/filelist_view.zig +++ b/src/tui/filelist_view.zig @@ -166,8 +166,8 @@ fn handle_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), th button.plane.home(); } const entry = &self.entries.items[idx]; - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s} ", .{pointer}) catch {}; + button.plane.set_style(style_label); + tui.render_pointer(&button.plane, selected); var buf: [std.fs.max_path_bytes]u8 = undefined; var removed_prefix: usize = 0; const max_len = self.view_cols / path_column_ratio; diff --git a/src/tui/home.zig b/src/tui/home.zig index ba47db3..6fd3c3d 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -154,7 +154,7 @@ fn add_menu_command(self: *Self, command_name: []const u8, description: []const try writer.print(" :{s}", .{hint}); const label = fis.getWritten(); const padding = Widget.Style.from_type(widget_style_type).padding; - self.menu_w = @max(self.menu_w, label.len + 1 + padding.left + padding.right); + self.menu_w = @max(self.menu_w, label.len + 2 + padding.left + padding.right); } var value = std.ArrayList(u8).init(self.allocator); @@ -237,8 +237,8 @@ fn menu_on_render(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: } else { button.plane.set_style_bg_transparent(style_text); } - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s}{s}", .{ pointer, description }) catch {}; + tui.render_pointer(&button.plane, selected); + _ = button.plane.print("{s}", .{description}) catch {}; if (button.active or button.hover or selected) { button.plane.set_style(style_leader); } else { diff --git a/src/tui/mode/overlay/file_type_palette.zig b/src/tui/mode/overlay/file_type_palette.zig index 20e3983..7236b4c 100644 --- a/src/tui/mode/overlay/file_type_palette.zig +++ b/src/tui/mode/overlay/file_type_palette.zig @@ -84,8 +84,7 @@ pub fn Variant(comptime command: []const u8, comptime label_: []const u8, allow_ } button.plane.set_style(style_hint); - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s}", .{pointer}) catch {}; + tui.render_pointer(&button.plane, selected); var iter = button.opts.label; var description_: []const u8 = undefined; diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index 58675e4..8fe352a 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -88,7 +88,7 @@ pub fn deinit(self: *Self) void { } inline fn menu_width(self: *Self) usize { - return @max(@min(self.longest + 1, max_menu_width()) + 5, inputbox_label.len + 2); + return @max(@min(self.longest + 3, max_menu_width()) + 5, inputbox_label.len + 2); } inline fn menu_pos_x(self: *Self) usize { diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index caa79c1..845a365 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -133,8 +133,7 @@ pub fn Create(options: type) type { if (!(cbor.matchString(&iter, &hint) catch false)) hint = ""; button.plane.set_style(style_hint); - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s}", .{pointer}) catch {}; + tui.render_pointer(&button.plane, selected); button.plane.set_style(style_label); _ = button.plane.print("{s} ", .{label}) catch {}; button.plane.set_style(style_hint); @@ -143,7 +142,7 @@ pub fn Create(options: type) type { var len = cbor.decodeArrayHeader(&iter) catch return false; while (len > 0) : (len -= 1) { if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { - tui.render_match_cell(&button.plane, 0, index + 1, theme) catch break; + tui.render_match_cell(&button.plane, 0, index + 2, theme) catch break; } else break; } return false; @@ -155,7 +154,7 @@ pub fn Create(options: type) type { fn prepare_resize(self: *Self) Widget.Box { const screen = tui.screen(); - const w = @max(@min(self.longest, max_menu_width) + 2 + 1 + self.longest_hint, options.label.len + 2); + const w = @max(@min(self.longest + 3, max_menu_width) + 2 + self.longest_hint, options.label.len + 2); const x = if (screen.w > w) (screen.w - w) / 2 else 0; self.view_rows = get_view_rows(screen); const h = @min(self.items + self.menu.header_count, self.view_rows + self.menu.header_count); diff --git a/src/tui/mode/overlay/task_palette.zig b/src/tui/mode/overlay/task_palette.zig index 0baae7b..949ca5c 100644 --- a/src/tui/mode/overlay/task_palette.zig +++ b/src/tui/mode/overlay/task_palette.zig @@ -85,8 +85,7 @@ pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget. button.plane.fill(" "); button.plane.home(); button.plane.set_style(style_hint); - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s}", .{pointer}) catch {}; + tui.render_pointer(&button.plane, selected); button.plane.set_style(style_label); _ = button.plane.print("{s} ", .{entry.label}) catch {}; var index: usize = 0; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index e699767..439880d 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1460,6 +1460,11 @@ pub fn render_match_cell(self: *renderer.Plane, y: usize, x: usize, theme_: *con _ = self.putc(&cell) catch {}; } +pub fn render_pointer(self: *renderer.Plane, selected: bool) void { + const pointer = if (selected) "⏵ " else " "; + _ = self.print("{s}", .{pointer}) catch {}; +} + pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, active: bool, selected: bool, hover: bool, theme_: *const Widget.Theme) bool { const style_base = theme_.editor_widget; const style_label = if (active) theme_.editor_cursor else if (hover or selected) theme_.editor_selection else theme_.editor_widget; @@ -1474,8 +1479,7 @@ pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, } self.set_style(style_hint); - const pointer = if (selected) "⏵" else " "; - _ = self.print("{s}", .{pointer}) catch {}; + render_pointer(self, selected); var iter = file_item_cbor; var file_path_: []const u8 = undefined; @@ -1500,7 +1504,7 @@ pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, var len = cbor.decodeArrayHeader(&iter) catch return false; while (len > 0) : (len -= 1) { if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { - render_match_cell(self, 0, index + 4, theme_) catch break; + render_match_cell(self, 0, index + 5, theme_) catch break; } else break; } return false; From 92b1354d4d6f4fe527c0945079ef6a2fac46f7a8 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 17:35:04 +0200 Subject: [PATCH 40/87] feat: add support for input box icons --- src/tui/InputBox.zig | 15 +++++++++++---- src/tui/mode/overlay/palette.zig | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/tui/InputBox.zig b/src/tui/InputBox.zig index 1efd3d2..706eaa5 100644 --- a/src/tui/InputBox.zig +++ b/src/tui/InputBox.zig @@ -12,6 +12,8 @@ pub fn Options(context: type) type { label: []const u8 = "Enter text", pos: Widget.Box = .{ .y = 0, .x = 0, .w = 12, .h = 1 }, ctx: Context, + padding: u8 = 1, + icon: ?[]const u8 = null, on_click: *const fn (ctx: context, button: *State(Context)) void = do_nothing, on_render: *const fn (ctx: context, button: *State(Context), theme: *const Widget.Theme) bool = on_render_default, @@ -29,18 +31,21 @@ pub fn Options(context: type) type { self.plane.set_style(style_label); self.plane.fill(" "); self.plane.home(); + for (0..self.opts.padding) |_| _ = self.plane.putchar(" "); + if (self.opts.icon) |icon| + _ = self.plane.print("{s}", .{icon}) catch {}; if (self.text.items.len > 0) { - _ = self.plane.print(" {s} ", .{self.text.items}) catch {}; + _ = self.plane.print("{s} ", .{self.text.items}) catch {}; } else { - _ = self.plane.print(" {s} ", .{self.label.items}) catch {}; + _ = self.plane.print("{s} ", .{self.label.items}) catch {}; } if (self.cursor) |cursor| { const pos: c_int = @intCast(cursor); if (tui.config().enable_terminal_cursor) { - const y, const x = self.plane.rel_yx_to_abs(0, pos + 1); + const y, const x = self.plane.rel_yx_to_abs(0, pos + self.opts.padding + self.icon_width); tui.rdr().cursor_enable(y, x, tui.get_cursor_shape()) catch {}; } else { - self.plane.cursor_move_yx(0, pos + 1) catch return false; + self.plane.cursor_move_yx(0, pos + self.opts.padding + self.icon_width) catch return false; var cell = self.plane.cell_init(); _ = self.plane.at_cursor_cell(&cell) catch return false; cell.set_style(theme.editor_cursor); @@ -68,6 +73,7 @@ pub fn create(ctx_type: type, allocator: std.mem.Allocator, parent: Plane, opts: .opts = opts, .label = std.ArrayList(u8).init(allocator), .text = std.ArrayList(u8).init(allocator), + .icon_width = @intCast(if (opts.icon) |icon| n.egc_chunk_width(icon, 0, 1) else 0), }; try self.label.appendSlice(self.opts.label); self.opts.label = self.label.items; @@ -83,6 +89,7 @@ pub fn State(ctx_type: type) type { label: std.ArrayList(u8), opts: Options(ctx_type), text: std.ArrayList(u8), + icon_width: c_int, cursor: ?usize = 0, const Self = @This(); diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index 845a365..84871dd 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -70,6 +70,8 @@ pub fn Create(options: type) type { .inputbox = (try self.menu.add_header(try InputBox.create(*Self, self.allocator, self.menu.menu.parent, .{ .ctx = self, .label = options.label, + .padding = 2, + .icon = if (@hasDecl(options, "icon")) options.icon else null, }))).dynamic_cast(InputBox.State(*Self)) orelse unreachable, .view_rows = get_view_rows(tui.screen()), .entries = std.ArrayList(Entry).init(allocator), From 3f61e46dfe70e8a896c986270be3847bf1136240 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 17:35:22 +0200 Subject: [PATCH 41/87] feat: add icon in buffer palette --- src/tui/mode/overlay/buffer_palette.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tui/mode/overlay/buffer_palette.zig b/src/tui/mode/overlay/buffer_palette.zig index 2d73a39..ee58850 100644 --- a/src/tui/mode/overlay/buffer_palette.zig +++ b/src/tui/mode/overlay/buffer_palette.zig @@ -12,6 +12,7 @@ const Widget = @import("../../Widget.zig"); pub const label = "Switch buffers"; pub const name = " buffer"; pub const description = "buffer"; +pub const icon = "󰈞 "; pub const Entry = struct { label: []const u8, From 5294ace5dad2fd1a6cdc2534187b5ba4b8674cb2 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 17:35:42 +0200 Subject: [PATCH 42/87] feat: add icon in file type palette --- src/tui/mode/overlay/file_type_palette.zig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/tui/mode/overlay/file_type_palette.zig b/src/tui/mode/overlay/file_type_palette.zig index 7236b4c..9660df1 100644 --- a/src/tui/mode/overlay/file_type_palette.zig +++ b/src/tui/mode/overlay/file_type_palette.zig @@ -14,6 +14,7 @@ pub fn Variant(comptime command: []const u8, comptime label_: []const u8, allow_ pub const label = label_; pub const name = " file type"; pub const description = "file type"; + pub const icon = " "; pub const Entry = struct { label: []const u8, @@ -88,13 +89,13 @@ pub fn Variant(comptime command: []const u8, comptime label_: []const u8, allow_ var iter = button.opts.label; var description_: []const u8 = undefined; - var icon: []const u8 = undefined; + var icon_: []const u8 = undefined; var color: u24 = undefined; if (!(cbor.matchString(&iter, &description_) catch false)) @panic("invalid file_type description"); - if (!(cbor.matchString(&iter, &icon) catch false)) @panic("invalid file_type icon"); + if (!(cbor.matchString(&iter, &icon_) catch false)) @panic("invalid file_type icon"); if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid file_type color"); - tui.render_file_icon(&button.plane, icon, color); + tui.render_file_icon(&button.plane, icon_, color); button.plane.set_style(style_label); _ = button.plane.print("{s} ", .{description_}) catch {}; @@ -117,12 +118,12 @@ pub fn Variant(comptime command: []const u8, comptime label_: []const u8, allow_ fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { var description_: []const u8 = undefined; - var icon: []const u8 = undefined; + var icon_: []const u8 = undefined; var color: u24 = undefined; var name_: []const u8 = undefined; var iter = button.opts.label; if (!(cbor.matchString(&iter, &description_) catch false)) return; - if (!(cbor.matchString(&iter, &icon) catch false)) return; + if (!(cbor.matchString(&iter, &icon_) catch false)) return; if (!(cbor.matchInt(u24, &iter, &color) catch false)) return; if (!(cbor.matchString(&iter, &name_) catch false)) return; if (!allow_previous) if (previous_file_type) |prev| if (std.mem.eql(u8, prev, name_)) From 12f6b884df033886c8c29b683371871be695d8fd Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 17:36:15 +0200 Subject: [PATCH 43/87] feat: add icon in open recent palette --- src/tui/mode/overlay/open_recent.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index 8fe352a..f68fc9d 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -57,6 +57,8 @@ pub fn create(allocator: std.mem.Allocator) !tui.Mode { .inputbox = (try self.menu.add_header(try InputBox.create(*Self, self.allocator, self.menu.menu.parent, .{ .ctx = self, .label = inputbox_label, + .padding = 2, + .icon = "󰈞 ", }))).dynamic_cast(InputBox.State(*Self)) orelse unreachable, .buffer_manager = tui.get_buffer_manager(), .longest = inputbox_label.len, From 2414f3b00f08ff03474bb5acd1c0662a5743cc0c Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 17:45:48 +0200 Subject: [PATCH 44/87] feat: add string parameter to add_task command for cli usage --- src/tui/tui.zig | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 439880d..aa8e1d4 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -922,6 +922,10 @@ const cmds = struct { pub const select_task_meta: Meta = .{ .description = "Run task" }; pub fn add_task(self: *Self, ctx: Ctx) Result { + var task: []const u8 = undefined; + if (try ctx.args.match(.{tp.extract(&task)})) + return call_add_task(task); + return enter_mini_mode(self, struct { pub const Type = @import("mode/mini/buffer.zig").Create(@This()); pub const create = Type.create; @@ -929,17 +933,24 @@ const cmds = struct { return @import("mode/overlay/task_palette.zig").name; } pub fn select(self_: *Type) void { - project_manager.add_task(self_.input.items) catch |e| { - const logger = log.logger("tui"); - logger.err("add_task", e); - logger.deinit(); - }; + call_add_task(self_.input.items); command.executeName("exit_mini_mode", .{}) catch {}; command.executeName("select_task", .{}) catch {}; } }, ctx); } - pub const add_task_meta: Meta = .{ .description = "Add task" }; + pub const add_task_meta: Meta = .{ + .description = "Add new task", + .arguments = &.{.string}, + }; + + fn call_add_task(task: []const u8) void { + project_manager.add_task(task) catch |e| { + const logger = log.logger("tui"); + logger.err("add_task", e); + logger.deinit(); + }; + } pub fn delete_task(_: *Self, ctx: Ctx) Result { var task: []const u8 = undefined; From c640c3f04b6ee6c0a5ff7d036074f5e7af7e85dc Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 17:52:23 +0200 Subject: [PATCH 45/87] fix: task_palette should not pass palette entry text to add_task --- src/tui/mode/overlay/task_palette.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/mode/overlay/task_palette.zig b/src/tui/mode/overlay/task_palette.zig index 949ca5c..4217919 100644 --- a/src/tui/mode/overlay/task_palette.zig +++ b/src/tui/mode/overlay/task_palette.zig @@ -107,7 +107,7 @@ fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { buffer_name.writer().print("*{s}*", .{entry.label}) catch {}; if (entry.command) |cmd| { tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - tp.self_pid().send(.{ "cmd", cmd, .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + tp.self_pid().send(.{ "cmd", cmd, .{} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); } else { project_manager.add_task(entry.label) catch {}; tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); From cea8edecb9822ad5f22ca5dc43f0028076051257 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 18:03:17 +0200 Subject: [PATCH 46/87] feat: rename select_task to run_task and add a string parameter --- src/keybind/builtin/flow.json | 2 +- src/tui/mode/overlay/task_palette.zig | 8 ++------ src/tui/tui.zig | 25 +++++++++++++++++++------ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 80e7e8f..9b59e99 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -29,7 +29,7 @@ ["f10", "theme_next"], ["f11", "toggle_panel"], ["f12", "toggle_inputview"], - ["alt+!", "select_task"], + ["alt+!", "run_task"], ["ctrl+tab", "next_tab"], ["ctrl+shift+tab", "previous_tab"], ["ctrl+shift+e", "switch_buffers"], diff --git a/src/tui/mode/overlay/task_palette.zig b/src/tui/mode/overlay/task_palette.zig index 4217919..d7d0821 100644 --- a/src/tui/mode/overlay/task_palette.zig +++ b/src/tui/mode/overlay/task_palette.zig @@ -102,17 +102,13 @@ fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { var entry: Entry = undefined; var iter = button.opts.label; if (!(cbor.matchValue(&iter, cbor.extract(&entry)) catch false)) return; - var buffer_name = std.ArrayList(u8).init(menu.*.opts.ctx.allocator); - defer buffer_name.deinit(); - buffer_name.writer().print("*{s}*", .{entry.label}) catch {}; if (entry.command) |cmd| { tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); tp.self_pid().send(.{ "cmd", cmd, .{} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); } else { - project_manager.add_task(entry.label) catch {}; tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - tp.self_pid().send(.{ "cmd", "create_scratch_buffer", .{ buffer_name.items, "", "conf" } }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - tp.self_pid().send(.{ "cmd", "shell_execute_stream", .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + project_manager.add_task(entry.label) catch {}; + tp.self_pid().send(.{ "cmd", "run_task", .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); } } diff --git a/src/tui/tui.zig b/src/tui/tui.zig index aa8e1d4..c06fdb8 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -916,11 +916,6 @@ const cmds = struct { } pub const switch_buffers_meta: Meta = .{ .description = "Switch buffers" }; - pub fn select_task(self: *Self, _: Ctx) Result { - return self.enter_overlay_mode(@import("mode/overlay/task_palette.zig").Type); - } - pub const select_task_meta: Meta = .{ .description = "Run task" }; - pub fn add_task(self: *Self, ctx: Ctx) Result { var task: []const u8 = undefined; if (try ctx.args.match(.{tp.extract(&task)})) @@ -935,7 +930,7 @@ const cmds = struct { pub fn select(self_: *Type) void { call_add_task(self_.input.items); command.executeName("exit_mini_mode", .{}) catch {}; - command.executeName("select_task", .{}) catch {}; + command.executeName("run_task", .{}) catch {}; } }, ctx); } @@ -952,6 +947,24 @@ const cmds = struct { }; } + pub fn run_task(self: *Self, ctx: Ctx) Result { + var task: []const u8 = undefined; + if (try ctx.args.match(.{tp.extract(&task)})) { + var buffer_name = std.ArrayList(u8).init(self.allocator); + defer buffer_name.deinit(); + buffer_name.writer().print("*{s}*", .{task}) catch {}; + project_manager.add_task(task) catch {}; + tp.self_pid().send(.{ "cmd", "create_scratch_buffer", .{ buffer_name.items, "", "conf" } }) catch |e| self.logger.err("task", e); + tp.self_pid().send(.{ "cmd", "shell_execute_stream", .{task} }) catch |e| self.logger.err("task", e); + } else { + return self.enter_overlay_mode(@import("mode/overlay/task_palette.zig").Type); + } + } + pub const run_task_meta: Meta = .{ + .description = "Run a task", + .arguments = &.{.string}, + }; + pub fn delete_task(_: *Self, ctx: Ctx) Result { var task: []const u8 = undefined; if (!try ctx.args.match(.{tp.extract(&task)})) From 469e10d4d9483866144d032ddff195294243f3f3 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 19:13:37 +0200 Subject: [PATCH 47/87] feat: add support for icons to command module --- src/command.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/command.zig b/src/command.zig index 8577a9a..16ad9f6 100644 --- a/src/command.zig +++ b/src/command.zig @@ -33,6 +33,7 @@ const Vtable = struct { pub const Metadata = struct { description: []const u8 = &[_]u8{}, arguments: []const ArgumentType = &[_]ArgumentType{}, + icon: ?[]const u8 = null, }; pub const ArgumentType = enum { @@ -188,6 +189,11 @@ pub fn get_arguments(id: ID) ?[]const ArgumentType { return (commands.items[id] orelse return null).meta.arguments; } +pub fn get_icon(id: ID) ?[]const u8 { + if (id >= commands.items.len) return null; + return (commands.items[id] orelse return null).meta.icon; +} + const suppressed_errors = std.StaticStringMap(void).initComptime(.{ .{ "enable_fast_scroll", void }, .{ "disable_fast_scroll", void }, From 5f77a48afe6a8a192a2bf4e143889f5ac77f6a8d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 19:16:32 +0200 Subject: [PATCH 48/87] feat: add description and icon for palette_menu_delete_item command --- src/tui/mode/overlay/palette.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index 84871dd..722f27d 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -475,7 +475,10 @@ pub fn Create(options: type) type { } } } - pub const palette_menu_delete_item_meta: Meta = .{}; + pub const palette_menu_delete_item_meta: Meta = .{ + .description = "Delete item", + .icon = "󰗨", + }; pub fn palette_menu_activate(self: *Self, _: Ctx) Result { self.menu.activate_selected(); From 5ce458e636b811132183fb8b080a8773debd5742 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 19:17:34 +0200 Subject: [PATCH 49/87] feat: add icon to add_task command meta --- src/tui/tui.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index c06fdb8..60c373c 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -937,6 +937,7 @@ const cmds = struct { pub const add_task_meta: Meta = .{ .description = "Add new task", .arguments = &.{.string}, + .icon = "", }; fn call_add_task(task: []const u8) void { From 7bd9c972e77c236581c7fc6207c123374b6ef5ea Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 19:17:52 +0200 Subject: [PATCH 50/87] feat: add flow mode keybindings for add_task --- src/keybind/builtin/flow.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 9b59e99..cf0a20e 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -30,6 +30,7 @@ ["f11", "toggle_panel"], ["f12", "toggle_inputview"], ["alt+!", "run_task"], + ["ctrl+1", "add_task"], ["ctrl+tab", "next_tab"], ["ctrl+shift+tab", "previous_tab"], ["ctrl+shift+e", "switch_buffers"], @@ -283,6 +284,7 @@ }, "overlay/palette": { "press": [ + ["alt+!", "add_task"], ["ctrl+j", "toggle_panel"], ["ctrl+q", "quit"], ["ctrl+w", "close_file"], From cef495cb5356ec054b8d8d4f7d1f36ede032c57f Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 19:18:28 +0200 Subject: [PATCH 51/87] feat: make add_task directly run the new task if called interactively --- src/tui/tui.zig | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 60c373c..416dc50 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -928,9 +928,8 @@ const cmds = struct { return @import("mode/overlay/task_palette.zig").name; } pub fn select(self_: *Type) void { - call_add_task(self_.input.items); + tp.self_pid().send(.{ "cmd", "run_task", .{self_.input.items} }) catch {}; command.executeName("exit_mini_mode", .{}) catch {}; - command.executeName("run_task", .{}) catch {}; } }, ctx); } @@ -954,7 +953,7 @@ const cmds = struct { var buffer_name = std.ArrayList(u8).init(self.allocator); defer buffer_name.deinit(); buffer_name.writer().print("*{s}*", .{task}) catch {}; - project_manager.add_task(task) catch {}; + call_add_task(task); tp.self_pid().send(.{ "cmd", "create_scratch_buffer", .{ buffer_name.items, "", "conf" } }) catch |e| self.logger.err("task", e); tp.self_pid().send(.{ "cmd", "shell_execute_stream", .{task} }) catch |e| self.logger.err("task", e); } else { From 8107a0d2b8cbb6e9002e64c08ac8d1de6829b45f Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 19:19:45 +0200 Subject: [PATCH 52/87] feat: display command hints in task_palette --- src/tui/mode/overlay/task_palette.zig | 28 +++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/tui/mode/overlay/task_palette.zig b/src/tui/mode/overlay/task_palette.zig index d7d0821..0a91870 100644 --- a/src/tui/mode/overlay/task_palette.zig +++ b/src/tui/mode/overlay/task_palette.zig @@ -60,7 +60,7 @@ pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !v palette.items += 1; } -pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget.Theme, selected: bool) bool { +pub fn on_render_menu(palette: *Type, button: *Type.ButtonState, theme: *const Widget.Theme, selected: bool) bool { var entry: Entry = undefined; var iter = button.opts.label; // label contains cbor entry object and matches if (!(cbor.matchValue(&iter, cbor.extract(&entry)) catch false)) @@ -84,10 +84,30 @@ pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget. button.plane.set_style(style_label); button.plane.fill(" "); button.plane.home(); + button.plane.set_style(style_hint); tui.render_pointer(&button.plane, selected); + button.plane.set_style(style_label); - _ = button.plane.print("{s} ", .{entry.label}) catch {}; + if (entry.command) |command_name| blk: { + button.plane.set_style(style_hint); + var label_: std.ArrayListUnmanaged(u8) = .empty; + defer label_.deinit(palette.allocator); + + const id = command.get_id(command_name) orelse break :blk; + if (command.get_icon(id)) |icon| + label_.writer(palette.allocator).print("{s} ", .{icon}) catch {}; + if (command.get_description(id)) |desc| + label_.writer(palette.allocator).print("{s}", .{desc}) catch {}; + _ = button.plane.print("{s} ", .{label_.items}) catch {}; + + const hints = if (tui.input_mode()) |m| m.keybind_hints else @panic("no keybind hints"); + if (hints.get(command_name)) |hint| + _ = button.plane.print_aligned_right(0, "{s} ", .{hint}) catch {}; + } else { + _ = button.plane.print("{s} ", .{entry.label}) catch {}; + } + var index: usize = 0; var len = cbor.decodeArrayHeader(&iter) catch return false; while (len > 0) : (len -= 1) { @@ -102,9 +122,9 @@ fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { var entry: Entry = undefined; var iter = button.opts.label; if (!(cbor.matchValue(&iter, cbor.extract(&entry)) catch false)) return; - if (entry.command) |cmd| { + if (entry.command) |command_name| { tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - tp.self_pid().send(.{ "cmd", cmd, .{} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + tp.self_pid().send(.{ "cmd", command_name, .{} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); } else { tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); project_manager.add_task(entry.label) catch {}; From d872e2e7345506b449370b464a2b069c81cc1308 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 19:20:04 +0200 Subject: [PATCH 53/87] feat: add palette_menu_delete_item hint to task_palette --- src/tui/mode/overlay/task_palette.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/tui/mode/overlay/task_palette.zig b/src/tui/mode/overlay/task_palette.zig index 0a91870..8d7368f 100644 --- a/src/tui/mode/overlay/task_palette.zig +++ b/src/tui/mode/overlay/task_palette.zig @@ -33,10 +33,8 @@ pub fn load_entries(palette: *Type) !usize { (try palette.entries.addOne()).* = .{ .label = try palette.allocator.dupe(u8, task) }; } else return error.InvalidTaskMessageField; } - (try palette.entries.addOne()).* = .{ - .label = try palette.allocator.dupe(u8, " Add new task"), - .command = "add_task", - }; + (try palette.entries.addOne()).* = .{ .label = "", .command = "add_task" }; + (try palette.entries.addOne()).* = .{ .label = "", .command = "palette_menu_delete_item" }; return if (palette.entries.items.len == 0) label.len else blk: { var longest: usize = 0; for (palette.entries.items) |item| longest = @max(longest, item.label.len); From 4f912cebebdb40ea81f98381572bdecd5ebd2362 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 22:32:31 +0200 Subject: [PATCH 54/87] feat: add basic widget style switching support --- src/tui/WidgetStyle.zig | 87 ++++++++++++++++++++++++++++++++++------- 1 file changed, 73 insertions(+), 14 deletions(-) diff --git a/src/tui/WidgetStyle.zig b/src/tui/WidgetStyle.zig index 557be81..8b28e8a 100644 --- a/src/tui/WidgetStyle.zig +++ b/src/tui/WidgetStyle.zig @@ -59,39 +59,98 @@ pub const Border = struct { const @"round thick box": Border = .{ .nw = "█", .n = "▀", .ne = "█", .e = "█", .se = "█", .s = "▄", .sw = "█", .w = "█" }; }; -pub const default_static: @This() = .{}; -pub const default = &default_static; +const compact: @This() = .{}; -pub const boxed_static: @This() = .{ +const boxed: @This() = .{ .padding = Margin.@"1", .border = Border.box, }; -pub const boxed = &boxed_static; -pub const thick_boxed_static: @This() = .{ +const thick_boxed: @This() = .{ .padding = Margin.@"1/2", .border = Border.@"thick box (octant)", }; -pub const thick_boxed = &thick_boxed_static; -pub const bars_top_bottom_static: @This() = .{ +const bars_top_bottom: @This() = .{ .padding = Margin.@"top/bottom/1", .border = Border.@"thick box (octant)", }; -pub const bars_top_bottom = &bars_top_bottom_static; -pub const bars_left_right_static: @This() = .{ +const bars_left_right: @This() = .{ .padding = Margin.@"left/right/1", .border = Border.@"thick box (octant)", }; -pub const bars_left_right = &bars_left_right_static; pub fn from_type(style_type: Type) *const @This() { return switch (style_type) { - .none => default, - .palette => bars_top_bottom, - .panel => default, - .home => bars_left_right, + .none => none_style, + .palette => palette_style, + .panel => panel_style, + .home => home_style, + }; +} + +pub const Styles = enum { + compact, + boxed, + thick_boxed, + bars_top_bottom, + bars_left_right, +}; + +pub fn from_tag(tag: Styles) *const @This() { + return switch (tag) { + .compact => &compact, + .boxed => &boxed, + .thick_boxed => &thick_boxed, + .bars_top_bottom => &bars_top_bottom, + .bars_left_right => &bars_left_right, + }; +} + +pub fn next_tag(tag: Styles) Styles { + const new_value = @intFromEnum(tag) + 1; + return if (new_value > @intFromEnum(Styles.bars_left_right)) .compact else @enumFromInt(new_value); +} + +pub fn set_type_style(style_type: Type, tag: Styles) void { + const ref = type_style(style_type); + ref.* = from_tag(tag); +} + +pub fn set_next_style(style_type: Type) void { + const tag_ref = type_tag(style_type); + const new_tag = next_tag(tag_ref.*); + const style_ref = type_style(style_type); + tag_ref.* = new_tag; + style_ref.* = from_tag(new_tag); +} + +var none_style: *const @This() = &compact; +var palette_style: *const @This() = &bars_top_bottom; +var panel_style: *const @This() = &compact; +var home_style: *const @This() = &bars_left_right; + +fn type_style(style_type: Type) **const @This() { + return switch (style_type) { + .none => &none_style, + .palette => &palette_style, + .panel => &panel_style, + .home => &home_style, + }; +} + +var none_tag: Styles = .compact; +var palette_tag: Styles = .bars_top_bottom; +var panel_tag: Styles = .compact; +var home_tag: Styles = .bars_left_right; + +fn type_tag(style_type: Type) *Styles { + return switch (style_type) { + .none => &none_tag, + .palette => &palette_tag, + .panel => &panel_tag, + .home => &home_tag, }; } From 17b3f152d5c24a834ac17b0e70992a6f3077d0dc Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 22:33:17 +0200 Subject: [PATCH 55/87] feat: add style switching command (alt+f9) to home screen --- src/keybind/builtin/flow.json | 1 + src/tui/home.zig | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index cf0a20e..cbecedd 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -254,6 +254,7 @@ "inherit": "project", "on_match_failure": "ignore", "press": [ + ["alt+f9", "home_next_widget_style"], ["ctrl+e", "find_file"], ["f", "find_file"], ["e", "find_file"], diff --git a/src/tui/home.zig b/src/tui/home.zig index 6fd3c3d..1c45c00 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -70,6 +70,8 @@ fire: ?Fire = null, commands: Commands = undefined, menu: *Menu.State(*Self), menu_w: usize = 0, +menu_label_max: usize = 0, +menu_count: usize = 0, menu_len: usize = 0, max_desc_len: usize = 0, input_namespace: []const u8, @@ -109,7 +111,6 @@ pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { try self.commands.init(self); var it = std.mem.splitAny(u8, self.home_style.menu_commands, "\n "); while (it.next()) |command_name| { - self.menu_len += 1; const id = command.get_id(command_name) orelse { logger.print("{s} is not defined", .{command_name}); continue; @@ -118,13 +119,14 @@ pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { logger.print("{s} has no description", .{command_name}); continue; }; + self.menu_count += 1; var hints = std.mem.splitScalar(u8, keybind_mode.keybind_hints.get(command_name) orelse "", ','); const hint = hints.first(); self.max_desc_len = @max(self.max_desc_len, description.len + hint.len + 5); try self.add_menu_command(command_name, description, hint, self.menu); } const padding = Widget.Style.from_type(widget_style_type).padding; - self.menu_len += padding.top + padding.bottom; + self.menu_len = self.menu_count + padding.top + padding.bottom; self.position_menu(15, 9); return w; } @@ -154,7 +156,8 @@ fn add_menu_command(self: *Self, command_name: []const u8, description: []const try writer.print(" :{s}", .{hint}); const label = fis.getWritten(); const padding = Widget.Style.from_type(widget_style_type).padding; - self.menu_w = @max(self.menu_w, label.len + 2 + padding.left + padding.right); + self.menu_label_max = @max(self.menu_label_max, label.len); + self.menu_w = self.menu_label_max + 2 + padding.left + padding.right; } var value = std.ArrayList(u8).init(self.allocator); @@ -397,6 +400,15 @@ const cmds = struct { } pub const home_menu_activate_meta: Meta = .{}; + pub fn home_next_widget_style(self: *Self, _: Ctx) Result { + Widget.Style.set_next_style(widget_style_type); + const padding = Widget.Style.from_type(widget_style_type).padding; + self.menu_len = self.menu_count + padding.top + padding.bottom; + self.menu_w = self.menu_label_max + 2 + padding.left + padding.right; + tui.need_render(); + } + pub const home_next_widget_style_meta: Meta = .{}; + pub fn home_sheeran(self: *Self, _: Ctx) Result { self.fire = if (self.fire) |*fire| ret: { fire.deinit(); From af9b097077e6c18882b9fd8acc499e0a7c444644 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 22:33:51 +0200 Subject: [PATCH 56/87] fix: rename run_task command on home screen --- src/tui/home.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tui/home.zig b/src/tui/home.zig index 1c45c00..b1ffcdf 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -30,7 +30,7 @@ const style = struct { \\open_recent_project \\find_in_files \\open_command_palette - \\select_task + \\run_task \\add_task \\open_config \\open_gui_config @@ -48,7 +48,7 @@ const style = struct { \\open_recent_project \\find_in_files \\open_command_palette - \\select_task + \\run_task \\add_task \\open_config \\open_keybind_config From c67c0b0c94c6533dbc03df816073c3d0be78e627 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 22:35:58 +0200 Subject: [PATCH 57/87] feat: add style switching command to palette (alt+f9) --- src/keybind/builtin/flow.json | 1 + src/tui/mode/overlay/palette.zig | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index cbecedd..aafa86e 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -285,6 +285,7 @@ }, "overlay/palette": { "press": [ + ["alt+f9", "overlay_next_widget_style"], ["alt+!", "add_task"], ["ctrl+j", "toggle_panel"], ["ctrl+q", "quit"], diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index 722f27d..d5c2472 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -532,6 +532,14 @@ pub fn Create(options: type) type { } pub const overlay_toggle_inputview_meta: Meta = .{}; + pub fn overlay_next_widget_style(self: *Self, _: Ctx) Result { + Widget.Style.set_next_style(widget_style_type); + const padding = Widget.Style.from_type(widget_style_type).padding; + self.do_resize(padding); + tui.need_render(); + } + pub const overlay_next_widget_style_meta: Meta = .{}; + pub fn mini_mode_paste(self: *Self, ctx: Ctx) Result { return overlay_insert_bytes(self, ctx); } From e95b2321841a700a40b586417977fa4998b1edf3 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 22:50:07 +0200 Subject: [PATCH 58/87] feat: add more widget box styles --- src/tui/WidgetStyle.zig | 70 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/src/tui/WidgetStyle.zig b/src/tui/WidgetStyle.zig index 8b28e8a..6af0bad 100644 --- a/src/tui/WidgetStyle.zig +++ b/src/tui/WidgetStyle.zig @@ -61,16 +61,51 @@ pub const Border = struct { const compact: @This() = .{}; +const spacious: @This() = .{ + .padding = Margin.@"1", + .border = Border.blank, +}; + const boxed: @This() = .{ .padding = Margin.@"1", .border = Border.box, }; +const rounded_boxed: @This() = .{ + .padding = Margin.@"1", + .border = Border.@"rounded box", +}; + +const double_boxed: @This() = .{ + .padding = Margin.@"1", + .border = Border.@"double box", +}; + +const single_double_top_bottom_boxed: @This() = .{ + .padding = Margin.@"1", + .border = Border.@"single/double box (top/bottom)", +}; + +const single_double_left_right_boxed: @This() = .{ + .padding = Margin.@"1", + .border = Border.@"single/double box (left/right)", +}; + +const dotted_boxed: @This() = .{ + .padding = Margin.@"1", + .border = Border.@"dotted box (braille)", +}; + const thick_boxed: @This() = .{ .padding = Margin.@"1/2", .border = Border.@"thick box (octant)", }; +const extra_thick_boxed: @This() = .{ + .padding = Margin.@"1/2", + .border = Border.@"extra thick box", +}; + const bars_top_bottom: @This() = .{ .padding = Margin.@"top/bottom/1", .border = Border.@"thick box (octant)", @@ -92,8 +127,15 @@ pub fn from_type(style_type: Type) *const @This() { pub const Styles = enum { compact, + spacious, boxed, + double_boxed, + rounded_boxed, + single_double_top_bottom_boxed, + single_double_left_right_boxed, + dotted_boxed, thick_boxed, + extra_thick_boxed, bars_top_bottom, bars_left_right, }; @@ -101,8 +143,15 @@ pub const Styles = enum { pub fn from_tag(tag: Styles) *const @This() { return switch (tag) { .compact => &compact, + .spacious => &spacious, .boxed => &boxed, + .double_boxed => &double_boxed, + .rounded_boxed => &rounded_boxed, + .single_double_top_bottom_boxed => &single_double_top_bottom_boxed, + .single_double_left_right_boxed => &single_double_left_right_boxed, + .dotted_boxed => &dotted_boxed, .thick_boxed => &thick_boxed, + .extra_thick_boxed => &extra_thick_boxed, .bars_top_bottom => &bars_top_bottom, .bars_left_right => &bars_left_right, }; @@ -126,10 +175,10 @@ pub fn set_next_style(style_type: Type) void { style_ref.* = from_tag(new_tag); } -var none_style: *const @This() = &compact; -var palette_style: *const @This() = &bars_top_bottom; -var panel_style: *const @This() = &compact; -var home_style: *const @This() = &bars_left_right; +var none_style: *const @This() = from_tag(none_tag_default); +var palette_style: *const @This() = from_tag(palette_tag_default); +var panel_style: *const @This() = from_tag(panel_tag_default); +var home_style: *const @This() = from_tag(home_tag_default); fn type_style(style_type: Type) **const @This() { return switch (style_type) { @@ -140,10 +189,15 @@ fn type_style(style_type: Type) **const @This() { }; } -var none_tag: Styles = .compact; -var palette_tag: Styles = .bars_top_bottom; -var panel_tag: Styles = .compact; -var home_tag: Styles = .bars_left_right; +const none_tag_default: Styles = .compact; +const palette_tag_default: Styles = .compact; +const panel_tag_default: Styles = .compact; +const home_tag_default: Styles = .compact; + +var none_tag: Styles = none_tag_default; +var palette_tag: Styles = palette_tag_default; +var panel_tag: Styles = panel_tag_default; +var home_tag: Styles = home_tag_default; fn type_tag(style_type: Type) *Styles { return switch (style_type) { From a27c212461596aad2797c9aa97747590fefd4cbd Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 13 Aug 2025 22:53:26 +0200 Subject: [PATCH 59/87] feat: add widget style switching command to open_recent palette --- src/tui/mode/overlay/open_recent.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index f68fc9d..6126315 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -370,6 +370,13 @@ const cmds = struct { } pub const overlay_toggle_inputview_meta: Meta = .{}; + pub fn overlay_next_widget_style(self: *Self, _: Ctx) Result { + Widget.Style.set_next_style(widget_style_type); + self.do_resize(); + tui.need_render(); + } + pub const overlay_next_widget_style_meta: Meta = .{}; + pub fn mini_mode_paste(self: *Self, ctx: Ctx) Result { return overlay_insert_bytes(self, ctx); } From 0c19cbd82da60fbef13377d895da3cb4f23f9930 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 14 Aug 2025 15:55:09 +0200 Subject: [PATCH 60/87] feat: persist widget style changes --- src/config.zig | 27 +++++++++ src/tui/Menu.zig | 2 +- src/tui/Widget.zig | 2 + src/tui/WidgetList.zig | 23 +++---- src/tui/WidgetStyle.zig | 89 ++-------------------------- src/tui/filelist_view.zig | 6 +- src/tui/home.zig | 13 ++-- src/tui/mode/overlay/open_recent.zig | 7 ++- src/tui/mode/overlay/palette.zig | 11 ++-- src/tui/tui.zig | 36 +++++++++++ 10 files changed, 103 insertions(+), 113 deletions(-) diff --git a/src/config.zig b/src/config.zig index f2a65e6..94ec3e9 100644 --- a/src/config.zig +++ b/src/config.zig @@ -36,6 +36,11 @@ show_fileicons: bool = true, start_debugger_on_crash: bool = false, +widget_style: WidgetStyle = .compact, +palette_style: WidgetStyle = .bars_top_bottom, +panel_style: WidgetStyle = .compact, +home_style: WidgetStyle = .bars_left_right, + include_files: []const u8 = "", pub const DigitStyle = enum { @@ -56,3 +61,25 @@ pub const IndentMode = enum { spaces, tabs, }; + +pub const WidgetType = enum { + none, + palette, + panel, + home, +}; + +pub const WidgetStyle = enum { + compact, + spacious, + boxed, + double_boxed, + rounded_boxed, + single_double_top_bottom_boxed, + single_double_left_right_boxed, + dotted_boxed, + thick_boxed, + extra_thick_boxed, + bars_top_bottom, + bars_left_right, +}; diff --git a/src/tui/Menu.zig b/src/tui/Menu.zig index 58b832b..7d51ab2 100644 --- a/src/tui/Menu.zig +++ b/src/tui/Menu.zig @@ -14,7 +14,7 @@ pub const scroll_lines = 3; pub fn Options(context: type) type { return struct { ctx: Context, - style: Widget.Style.Type, + style: Widget.Type, on_click: *const fn (ctx: context, button: *Button.State(*State(Context))) void = do_nothing, on_click4: *const fn (menu: **State(Context), button: *Button.State(*State(Context))) void = do_nothing_click, diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 653f2ea..2195259 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -10,6 +10,8 @@ pub const Box = @import("Box.zig"); pub const Theme = @import("theme"); pub const themes = @import("themes").themes; pub const scopes = @import("themes").scopes; +pub const Type = @import("config").WidgetType; +pub const StyleTag = @import("config").WidgetStyle; pub const Style = @import("WidgetStyle.zig"); ptr: *anyopaque, diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index 012dcd1..b799c04 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -6,6 +6,7 @@ const tp = @import("thespian"); const Plane = @import("renderer").Plane; +const tui = @import("tui.zig"); const Widget = @import("Widget.zig"); const Box = @import("Box.zig"); @@ -33,16 +34,16 @@ after_render: *const fn (ctx: ?*anyopaque, theme: *const Widget.Theme) void = on prepare_resize: *const fn (ctx: ?*anyopaque, self: *Self, box: Widget.Box) Widget.Box = prepare_resize_default, after_resize: *const fn (ctx: ?*anyopaque, self: *Self, box: Widget.Box) void = after_resize_default, on_layout: *const fn (ctx: ?*anyopaque, self: *Self) Widget.Layout = on_layout_default, -style: Widget.Style.Type, +widget_type: Widget.Type, pub fn createH(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout) error{OutOfMemory}!*Self { return createHStyled(allocator, parent, name, layout_, .none); } -pub fn createHStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: Widget.Style.Type) error{OutOfMemory}!*Self { +pub fn createHStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, widget_type: Widget.Type) error{OutOfMemory}!*Self { const self = try allocator.create(Self); errdefer allocator.destroy(self); - self.* = try init(allocator, parent, name, .horizontal, layout_, Box{}, style); + self.* = try init(allocator, parent, name, .horizontal, layout_, Box{}, widget_type); self.plane.hide(); return self; } @@ -51,10 +52,10 @@ pub fn createV(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: return createVStyled(allocator, parent, name, layout_, .none); } -pub fn createVStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: Widget.Style.Type) !*Self { +pub fn createVStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, widget_type: Widget.Type) !*Self { const self = try allocator.create(Self); errdefer allocator.destroy(self); - self.* = try init(allocator, parent, name, .vertical, layout_, Box{}, style); + self.* = try init(allocator, parent, name, .vertical, layout_, Box{}, widget_type); self.plane.hide(); return self; } @@ -67,7 +68,7 @@ pub fn createBox(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: D return self; } -fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, layout_: Layout, box_: Box, style: Widget.Style.Type) !Self { +fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, layout_: Layout, box_: Box, widget_type: Widget.Type) !Self { var self: Self = .{ .plane = undefined, .parent = parent, @@ -75,10 +76,10 @@ fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, .widgets = ArrayList(WidgetState).init(allocator), .layout_ = layout_, .direction = dir, - .style = style, + .widget_type = widget_type, .deco_box = undefined, }; - const padding = Widget.Style.from_type(self.style).padding; + const padding = tui.get_widget_style(self.widget_type).padding; self.deco_box = self.from_client_box(box_, padding); self.plane = try Plane.init(&self.deco_box.opts(name), parent); return self; @@ -163,7 +164,7 @@ pub fn update(self: *Self) void { } pub fn render(self: *Self, theme: *const Widget.Theme) bool { - const widget_style = Widget.Style.from_type(self.style); + const widget_style = tui.get_widget_style(self.widget_type); const padding = widget_style.padding; for (self.widgets.items) |*w| if (!w.layout.eql(w.widget.layout())) { self.refresh_layout(padding); @@ -192,7 +193,7 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { fn on_render_default(_: ?*anyopaque, _: *const Widget.Theme) void {} fn render_decoration(self: *Self, theme: *const Widget.Theme, widget_style: *const Widget.Style) void { - const style = Widget.Style.theme_style_from_type(self.style, theme); + const style = Widget.Style.theme_style_from_type(self.widget_type, theme); const padding = widget_style.padding; const border = widget_style.border; const plane = &self.plane; @@ -297,7 +298,7 @@ fn refresh_layout(self: *Self, padding: Widget.Style.Margin) void { } pub fn handle_resize(self: *Self, box: Widget.Box) void { - const padding = Widget.Style.from_type(self.style).padding; + const padding = tui.get_widget_style(self.widget_type).padding; const client_box_ = self.prepare_resize(self.ctx, self, self.to_client_box(box, padding)); self.deco_box = self.from_client_box(client_box_, padding); self.do_resize(padding); diff --git a/src/tui/WidgetStyle.zig b/src/tui/WidgetStyle.zig index 6af0bad..4f91d71 100644 --- a/src/tui/WidgetStyle.zig +++ b/src/tui/WidgetStyle.zig @@ -1,12 +1,8 @@ padding: Margin = Margin.@"0", border: Border = Border.blank, -pub const Type = enum { - none, - palette, - panel, - home, -}; +pub const WidgetType = @import("config").WidgetType; +pub const WidgetStyle = @import("config").WidgetStyle; pub const Padding = struct { pub const Unit = u16; @@ -116,31 +112,7 @@ const bars_left_right: @This() = .{ .border = Border.@"thick box (octant)", }; -pub fn from_type(style_type: Type) *const @This() { - return switch (style_type) { - .none => none_style, - .palette => palette_style, - .panel => panel_style, - .home => home_style, - }; -} - -pub const Styles = enum { - compact, - spacious, - boxed, - double_boxed, - rounded_boxed, - single_double_top_bottom_boxed, - single_double_left_right_boxed, - dotted_boxed, - thick_boxed, - extra_thick_boxed, - bars_top_bottom, - bars_left_right, -}; - -pub fn from_tag(tag: Styles) *const @This() { +pub fn from_tag(tag: WidgetStyle) *const @This() { return switch (tag) { .compact => &compact, .spacious => &spacious, @@ -157,60 +129,9 @@ pub fn from_tag(tag: Styles) *const @This() { }; } -pub fn next_tag(tag: Styles) Styles { - const new_value = @intFromEnum(tag) + 1; - return if (new_value > @intFromEnum(Styles.bars_left_right)) .compact else @enumFromInt(new_value); -} +const Theme = @import("Widget.zig").Theme; -pub fn set_type_style(style_type: Type, tag: Styles) void { - const ref = type_style(style_type); - ref.* = from_tag(tag); -} - -pub fn set_next_style(style_type: Type) void { - const tag_ref = type_tag(style_type); - const new_tag = next_tag(tag_ref.*); - const style_ref = type_style(style_type); - tag_ref.* = new_tag; - style_ref.* = from_tag(new_tag); -} - -var none_style: *const @This() = from_tag(none_tag_default); -var palette_style: *const @This() = from_tag(palette_tag_default); -var panel_style: *const @This() = from_tag(panel_tag_default); -var home_style: *const @This() = from_tag(home_tag_default); - -fn type_style(style_type: Type) **const @This() { - return switch (style_type) { - .none => &none_style, - .palette => &palette_style, - .panel => &panel_style, - .home => &home_style, - }; -} - -const none_tag_default: Styles = .compact; -const palette_tag_default: Styles = .compact; -const panel_tag_default: Styles = .compact; -const home_tag_default: Styles = .compact; - -var none_tag: Styles = none_tag_default; -var palette_tag: Styles = palette_tag_default; -var panel_tag: Styles = panel_tag_default; -var home_tag: Styles = home_tag_default; - -fn type_tag(style_type: Type) *Styles { - return switch (style_type) { - .none => &none_tag, - .palette => &palette_tag, - .panel => &panel_tag, - .home => &home_tag, - }; -} - -const Widget = @import("Widget.zig"); - -pub fn theme_style_from_type(style_type: Type, theme: *const Widget.Theme) Widget.Theme.Style { +pub fn theme_style_from_type(style_type: WidgetType, theme: *const Theme) Theme.Style { return switch (style_type) { .none => theme.editor, .palette => .{ .fg = theme.editor_widget_border.fg, .bg = theme.editor_widget.bg }, diff --git a/src/tui/filelist_view.zig b/src/tui/filelist_view.zig index 4108be0..7544dce 100644 --- a/src/tui/filelist_view.zig +++ b/src/tui/filelist_view.zig @@ -36,7 +36,7 @@ selected: ?usize = null, box: Widget.Box = .{}, const path_column_ratio = 4; -const widget_style_type: Widget.Style.Type = .panel; +const widget_type: Widget.Type = .panel; const Entry = struct { path: []const u8, @@ -58,7 +58,7 @@ pub fn create(allocator: Allocator, parent: Plane) !Widget { .entries = std.ArrayList(Entry).init(allocator), .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, - .style = widget_style_type, + .style = widget_type, .on_render = handle_render_menu, .on_scroll = EventHandler.bind(self, Self.handle_scroll), .on_click4 = mouse_click_button4, @@ -87,7 +87,7 @@ fn scrollbar_style(sb: *scrollbar_v, theme: *const Widget.Theme) Widget.Theme.St } pub fn handle_resize(self: *Self, pos: Widget.Box) void { - const padding = Widget.Style.from_type(widget_style_type).padding; + const padding = tui.get_widget_style(widget_type).padding; self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return; self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return; self.box = pos; diff --git a/src/tui/home.zig b/src/tui/home.zig index b1ffcdf..fb785e3 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -81,7 +81,7 @@ home_style_bufs: [][]const u8, const Self = @This(); -const widget_style_type: Widget.Style.Type = .home; +const widget_type: Widget.Type = .home; pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { const logger = log.logger("home"); @@ -101,7 +101,7 @@ pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { .plane = n, .menu = try Menu.create(*Self, allocator, w.plane.*, .{ .ctx = self, - .style = widget_style_type, + .style = widget_type, .on_render = menu_on_render, }), .input_namespace = keybind.get_namespace(), @@ -125,7 +125,7 @@ pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { self.max_desc_len = @max(self.max_desc_len, description.len + hint.len + 5); try self.add_menu_command(command_name, description, hint, self.menu); } - const padding = Widget.Style.from_type(widget_style_type).padding; + const padding = tui.get_widget_style(widget_type).padding; self.menu_len = self.menu_count + padding.top + padding.bottom; self.position_menu(15, 9); return w; @@ -155,7 +155,7 @@ fn add_menu_command(self: *Self, command_name: []const u8, description: []const _ = try writer.write(leader); try writer.print(" :{s}", .{hint}); const label = fis.getWritten(); - const padding = Widget.Style.from_type(widget_style_type).padding; + const padding = tui.get_widget_style(widget_type).padding; self.menu_label_max = @max(self.menu_label_max, label.len); self.menu_w = self.menu_label_max + 2 + padding.left + padding.right; } @@ -401,11 +401,12 @@ const cmds = struct { pub const home_menu_activate_meta: Meta = .{}; pub fn home_next_widget_style(self: *Self, _: Ctx) Result { - Widget.Style.set_next_style(widget_style_type); - const padding = Widget.Style.from_type(widget_style_type).padding; + tui.set_next_style(widget_type); + const padding = tui.get_widget_style(widget_type).padding; self.menu_len = self.menu_count + padding.top + padding.bottom; self.menu_w = self.menu_label_max + 2 + padding.left + padding.right; tui.need_render(); + try tui.save_config(); } pub const home_next_widget_style_meta: Meta = .{}; diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index 6126315..cd934da 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -23,7 +23,7 @@ const ModalBackground = @import("../../ModalBackground.zig"); const Self = @This(); const max_recent_files: usize = 25; -const widget_style_type: Widget.Style.Type = .palette; +const widget_type: Widget.Type = .palette; allocator: std.mem.Allocator, f: usize = 0, @@ -49,7 +49,7 @@ pub fn create(allocator: std.mem.Allocator) !tui.Mode { .modal = try ModalBackground.create(*Self, allocator, tui.mainview_widget(), .{ .ctx = self }), .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, - .style = widget_style_type, + .style = widget_type, .on_render = on_render_menu, .prepare_resize = prepare_resize_menu, }), @@ -371,9 +371,10 @@ const cmds = struct { pub const overlay_toggle_inputview_meta: Meta = .{}; pub fn overlay_next_widget_style(self: *Self, _: Ctx) Result { - Widget.Style.set_next_style(widget_style_type); + tui.set_next_style(widget_type); self.do_resize(); tui.need_render(); + try tui.save_config(); } pub const overlay_next_widget_style_meta: Meta = .{}; diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index d5c2472..cfadc52 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -20,7 +20,7 @@ const ModalBackground = @import("../../ModalBackground.zig"); pub const Menu = @import("../../Menu.zig"); const max_menu_width = 80; -const widget_style_type: Widget.Style.Type = .palette; +const widget_type: Widget.Type = .palette; pub fn Create(options: type) type { return struct { @@ -58,7 +58,7 @@ pub fn Create(options: type) type { }), .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, - .style = widget_style_type, + .style = widget_type, .on_render = if (@hasDecl(options, "on_render_menu")) options.on_render_menu else on_render_menu, .prepare_resize = prepare_resize_menu, .after_resize = after_resize_menu, @@ -256,7 +256,7 @@ pub fn Create(options: type) type { var i = n; while (i > 0) : (i -= 1) self.menu.select_down(); - const padding = Widget.Style.from_type(widget_style_type).padding; + const padding = tui.get_widget_style(widget_type).padding; self.do_resize(padding); tui.refresh_hover(); self.selection_updated(); @@ -533,10 +533,11 @@ pub fn Create(options: type) type { pub const overlay_toggle_inputview_meta: Meta = .{}; pub fn overlay_next_widget_style(self: *Self, _: Ctx) Result { - Widget.Style.set_next_style(widget_style_type); - const padding = Widget.Style.from_type(widget_style_type).padding; + tui.set_next_style(widget_type); + const padding = tui.get_widget_style(widget_type).padding; self.do_resize(padding); tui.need_render(); + try tui.save_config(); } pub const overlay_next_widget_style_meta: Meta = .{}; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 416dc50..f2b2549 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1562,3 +1562,39 @@ fn load_theme_file_internal(allocator: std.mem.Allocator, theme_name: []const u8 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 ConfigWidgetStyle = @import("config").WidgetStyle; +pub const WidgetStyle = @import("WidgetStyle.zig"); + +pub fn get_widget_style(widget_type: WidgetType) *const WidgetStyle { + const config_ = config(); + return switch (widget_type) { + .none => WidgetStyle.from_tag(config_.widget_style), + .palette => WidgetStyle.from_tag(config_.palette_style), + .panel => WidgetStyle.from_tag(config_.panel_style), + .home => WidgetStyle.from_tag(config_.home_style), + }; +} + +pub fn set_next_style(widget_type: WidgetType) void { + const ref = widget_type_config_variable(widget_type); + ref.* = next_widget_style(ref.*); + const self = current(); + self.logger.print("{s} style {s}", .{ @tagName(widget_type), @tagName(ref.*) }); +} + +fn next_widget_style(tag: ConfigWidgetStyle) ConfigWidgetStyle { + const new_value = @intFromEnum(tag) + 1; + return if (new_value > @intFromEnum(ConfigWidgetStyle.bars_left_right)) .compact else @enumFromInt(new_value); +} + +fn widget_type_config_variable(widget_type: WidgetType) *ConfigWidgetStyle { + const config_ = config_mut(); + return switch (widget_type) { + .none => &config_.widget_style, + .palette => &config_.palette_style, + .panel => &config_.panel_style, + .home => &config_.home_style, + }; +} From 4beedaf1aa0c7e6ed8a95cf0a954e6056ec6eaae Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 14 Aug 2025 16:03:44 +0200 Subject: [PATCH 61/87] refactor: make tui.next_widget_style tag order independant --- src/tui/tui.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index f2b2549..15d8c58 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1585,8 +1585,9 @@ pub fn set_next_style(widget_type: WidgetType) void { } fn next_widget_style(tag: ConfigWidgetStyle) ConfigWidgetStyle { + const max_tag = comptime std.meta.tags(ConfigWidgetStyle).len; const new_value = @intFromEnum(tag) + 1; - return if (new_value > @intFromEnum(ConfigWidgetStyle.bars_left_right)) .compact else @enumFromInt(new_value); + return if (new_value >= max_tag) @enumFromInt(0) else @enumFromInt(new_value); } fn widget_type_config_variable(widget_type: WidgetType) *ConfigWidgetStyle { From e8c780b3b7e438293a270b393e0bfec2565e1dc6 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 14 Aug 2025 16:04:25 +0200 Subject: [PATCH 62/87] refactor: re-order widget style tags --- src/config.zig | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/config.zig b/src/config.zig index 94ec3e9..25ff88a 100644 --- a/src/config.zig +++ b/src/config.zig @@ -70,16 +70,16 @@ pub const WidgetType = enum { }; pub const WidgetStyle = enum { - compact, - spacious, - boxed, - double_boxed, - rounded_boxed, - single_double_top_bottom_boxed, - single_double_left_right_boxed, - dotted_boxed, - thick_boxed, - extra_thick_boxed, bars_top_bottom, bars_left_right, + thick_boxed, + extra_thick_boxed, + dotted_boxed, + rounded_boxed, + double_boxed, + single_double_top_bottom_boxed, + single_double_left_right_boxed, + boxed, + spacious, + compact, }; From aaa3e5b079357217214e161d4a5edee13b1d6feb Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 14 Aug 2025 16:11:12 +0200 Subject: [PATCH 63/87] feat: tweak home screen widget style --- src/config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.zig b/src/config.zig index 25ff88a..1414403 100644 --- a/src/config.zig +++ b/src/config.zig @@ -39,7 +39,7 @@ start_debugger_on_crash: bool = false, widget_style: WidgetStyle = .compact, palette_style: WidgetStyle = .bars_top_bottom, panel_style: WidgetStyle = .compact, -home_style: WidgetStyle = .bars_left_right, +home_style: WidgetStyle = .bars_top_bottom, include_files: []const u8 = "", From c143eb6a59eb98cd1242b1dac0a37815ae98cb2d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 14 Aug 2025 16:29:46 +0200 Subject: [PATCH 64/87] fix: also hide inputbox icons if show_fileicons config option is off --- src/tui/InputBox.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tui/InputBox.zig b/src/tui/InputBox.zig index 706eaa5..4d63aea 100644 --- a/src/tui/InputBox.zig +++ b/src/tui/InputBox.zig @@ -32,8 +32,9 @@ pub fn Options(context: type) type { self.plane.fill(" "); self.plane.home(); for (0..self.opts.padding) |_| _ = self.plane.putchar(" "); - if (self.opts.icon) |icon| + if (self.icon_width > 0) if (self.opts.icon) |icon| { _ = self.plane.print("{s}", .{icon}) catch {}; + }; if (self.text.items.len > 0) { _ = self.plane.print("{s} ", .{self.text.items}) catch {}; } else { @@ -73,7 +74,7 @@ pub fn create(ctx_type: type, allocator: std.mem.Allocator, parent: Plane, opts: .opts = opts, .label = std.ArrayList(u8).init(allocator), .text = std.ArrayList(u8).init(allocator), - .icon_width = @intCast(if (opts.icon) |icon| n.egc_chunk_width(icon, 0, 1) else 0), + .icon_width = @intCast(if (tui.config().show_fileicons) if (opts.icon) |icon| n.egc_chunk_width(icon, 0, 1) else 0 else 0), }; try self.label.appendSlice(self.opts.label); self.opts.label = self.label.items; From 62b8493b93dd9e32321cde9d82a61e36d15f4c38 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 14 Aug 2025 16:30:46 +0200 Subject: [PATCH 65/87] fix: correct match offsets when show_fileicons is off --- src/tui/mode/overlay/file_type_palette.zig | 4 ++-- src/tui/tui.zig | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/tui/mode/overlay/file_type_palette.zig b/src/tui/mode/overlay/file_type_palette.zig index 9660df1..1815f00 100644 --- a/src/tui/mode/overlay/file_type_palette.zig +++ b/src/tui/mode/overlay/file_type_palette.zig @@ -95,7 +95,7 @@ pub fn Variant(comptime command: []const u8, comptime label_: []const u8, allow_ if (!(cbor.matchString(&iter, &icon_) catch false)) @panic("invalid file_type icon"); if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid file_type color"); - tui.render_file_icon(&button.plane, icon_, color); + const icon_width = tui.render_file_icon(&button.plane, icon_, color); button.plane.set_style(style_label); _ = button.plane.print("{s} ", .{description_}) catch {}; @@ -110,7 +110,7 @@ pub fn Variant(comptime command: []const u8, comptime label_: []const u8, allow_ var len = cbor.decodeArrayHeader(&iter) catch return false; while (len > 0) : (len -= 1) { if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { - tui.render_match_cell(&button.plane, 0, index + 4, theme) catch break; + tui.render_match_cell(&button.plane, 0, index + 2 + icon_width, theme) catch break; } else break; } return false; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 15d8c58..c84880c 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1463,8 +1463,8 @@ pub fn get_buffer_state_indicator(buffer: *const @import("Buffer")) []const u8 { return if (buffer.is_dirty()) dirty_indicator else if (buffer.is_hidden()) hidden_indicator else ""; } -pub fn render_file_icon(self: *renderer.Plane, icon: []const u8, color: u24) void { - if (!config().show_fileicons) return; +pub fn render_file_icon(self: *renderer.Plane, icon: []const u8, color: u24) usize { + if (!config().show_fileicons) return 0; var cell = self.cell_init(); _ = self.at_cursor_cell(&cell) catch return; if (!(color == 0xFFFFFF or color == 0x000000 or color == 0x000001)) { @@ -1474,6 +1474,7 @@ pub fn render_file_icon(self: *renderer.Plane, icon: []const u8, color: u24) voi _ = self.putc(&cell) catch {}; self.cursor_move_rel(0, 1) catch {}; _ = self.print(" ", .{}) catch {}; + return 3; } pub fn render_match_cell(self: *renderer.Plane, y: usize, x: usize, theme_: *const Widget.Theme) !void { @@ -1513,7 +1514,7 @@ pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, if (!(cbor.matchString(&iter, &icon) catch false)) @panic("invalid buffer file type icon"); if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid buffer file type color"); - render_file_icon(self, icon, color); + const icon_width = render_file_icon(self, icon, color); self.set_style(style_label); _ = self.print("{s} ", .{file_path_}) catch {}; @@ -1528,7 +1529,7 @@ pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, var len = cbor.decodeArrayHeader(&iter) catch return false; while (len > 0) : (len -= 1) { if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { - render_match_cell(self, 0, index + 5, theme_) catch break; + render_match_cell(self, 0, index + 2 + icon_width, theme_) catch break; } else break; } return false; From 6b04f4db08c18fac921e37fb09d02025093c8aae Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 15 Aug 2025 11:22:43 +0200 Subject: [PATCH 66/87] fix: entering the same minimode twice causes an empty keybind set --- src/tui/tui.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index c84880c..ed5640e 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1036,10 +1036,10 @@ const cmds = struct { fn enter_mini_mode(self: *Self, comptime mode: anytype, ctx: Ctx) !void { command.executeName("disable_fast_scroll", .{}) catch {}; command.executeName("disable_jump_mode", .{}) catch {}; - const input_mode_, const mini_mode_ = try mode.create(self.allocator, ctx); if (self.mini_mode_) |_| try exit_mini_mode(self, .{}); if (self.input_mode_outer_) |_| try exit_overlay_mode(self, .{}); if (self.input_mode_outer_ != null) @panic("exit_overlay_mode failed"); + const input_mode_, const mini_mode_ = try mode.create(self.allocator, ctx); self.input_mode_outer_ = self.input_mode_; self.input_mode_ = input_mode_; self.mini_mode_ = mini_mode_; From b043dfe34f8ad708e79bd7466bcae179a0a906e0 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 15 Aug 2025 11:27:08 +0200 Subject: [PATCH 67/87] feat: reduce mode indicator clutter --- src/tui/status/linenumstate.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tui/status/linenumstate.zig b/src/tui/status/linenumstate.zig index 3c29e48..9c6813c 100644 --- a/src/tui/status/linenumstate.zig +++ b/src/tui/status/linenumstate.zig @@ -90,11 +90,11 @@ fn format(self: *Self) void { const writer = fbs.writer(); const eol_mode = switch (self.eol_mode) { .lf => "", - .crlf => " [␍␊]", + .crlf => " ␍␊", }; const indent_mode = switch (self.indent_mode) { .spaces, .auto => "", - .tabs => " [⭾]", + .tabs => " ⭾ ", }; std.fmt.format(writer, "{s}{s} Ln ", .{ eol_mode, indent_mode }) catch {}; self.format_count(writer, self.line + 1, self.padding orelse 0) catch {}; From 3b3104e876ce13ca01edac2d4867207f029ae015 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 15 Aug 2025 12:31:54 +0200 Subject: [PATCH 68/87] feat: add flow mode keybind for find_in_files to overlay/palette mode --- src/keybind/builtin/flow.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index aafa86e..ed576da 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -290,6 +290,7 @@ ["ctrl+j", "toggle_panel"], ["ctrl+q", "quit"], ["ctrl+w", "close_file"], + ["ctrl+shift+f", "find_in_files"], ["ctrl+p", "palette_menu_up"], ["ctrl+n", "palette_menu_down"], ["ctrl+e", "palette_menu_down"], From 70efcc8693ebaf361574b906487a09322487a0d5 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 15 Aug 2025 12:55:59 +0200 Subject: [PATCH 69/87] feat: add no_store argument to set_theme command for cli use --- src/tui/tui.zig | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index ed5640e..7b95860 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -714,17 +714,22 @@ fn refresh_input_mode(self: *Self) command.Result { if (self.input_mode_) |*m| m.run_init(); } -fn set_theme_by_name(self: *Self, name: []const u8) !void { +fn set_theme_by_name(self: *Self, name: []const u8, action: enum { none, store }) !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(); + switch (action) { + .none => {}, + .store => { + self.config_.theme = self.theme_.name; + try save_config(); + }, + } } const cmds = struct { @@ -790,21 +795,24 @@ const cmds = struct { pub fn set_theme(self: *Self, ctx: Ctx) Result { var name: []const u8 = undefined; - if (!try ctx.args.match(.{tp.extract(&name)})) - return tp.exit_error(error.InvalidSetThemeArgument, null); - return self.set_theme_by_name(name); + if (try ctx.args.match(.{tp.extract(&name)})) + return self.set_theme_by_name(name, .store); + if (try ctx.args.match(.{ tp.extract(&name), "no_store" })) + return self.set_theme_by_name(name, .none); + + return tp.exit_error(error.InvalidSetThemeArgument, null); } pub const set_theme_meta: Meta = .{ .arguments = &.{.string} }; pub fn theme_next(self: *Self, _: Ctx) Result { const name = get_next_theme_by_name(self.theme_.name); - return self.set_theme_by_name(name); + return self.set_theme_by_name(name, .store); } pub const theme_next_meta: Meta = .{ .description = "Next color theme" }; pub fn theme_prev(self: *Self, _: Ctx) Result { const name = get_prev_theme_by_name(self.theme_.name); - return self.set_theme_by_name(name); + return self.set_theme_by_name(name, .store); } pub const theme_prev_meta: Meta = .{ .description = "Previous color theme" }; From 961090140ad5e7337299301be04a7f5d4ae034a7 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 15 Aug 2025 23:26:13 +0200 Subject: [PATCH 70/87] refactor: split render_file_item_cbor into two functions for better reuse --- src/tui/tui.zig | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 7b95860..7ccdfe4 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1498,7 +1498,18 @@ pub fn render_pointer(self: *renderer.Plane, selected: bool) void { _ = self.print("{s}", .{pointer}) catch {}; } -pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, active: bool, selected: bool, hover: bool, theme_: *const Widget.Theme) bool { +pub fn render_file_item( + self: *renderer.Plane, + file_path_: []const u8, + icon: []const u8, + color: u24, + indicator: []const u8, + matches_cbor: []const u8, + active: bool, + selected: bool, + hover: bool, + theme_: *const Widget.Theme, +) bool { const style_base = theme_.editor_widget; const style_label = if (active) theme_.editor_cursor else if (hover or selected) theme_.editor_selection else theme_.editor_widget; const style_hint = if (find_scope_style(theme_, "entity.name")) |sty| sty.style else style_label; @@ -1514,25 +1525,15 @@ pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, self.set_style(style_hint); render_pointer(self, selected); - var iter = file_item_cbor; - var file_path_: []const u8 = undefined; - var icon: []const u8 = undefined; - var color: u24 = undefined; - if (!(cbor.matchString(&iter, &file_path_) catch false)) @panic("invalid buffer file path"); - if (!(cbor.matchString(&iter, &icon) catch false)) @panic("invalid buffer file type icon"); - if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid buffer file type color"); - const icon_width = render_file_icon(self, icon, color); self.set_style(style_label); _ = self.print("{s} ", .{file_path_}) catch {}; - var indicator: []const u8 = undefined; - if (!(cbor.matchString(&iter, &indicator) catch false)) - indicator = ""; self.set_style(style_hint); _ = self.print_aligned_right(0, "{s} ", .{indicator}) catch {}; + var iter = matches_cbor; var index: usize = 0; var len = cbor.decodeArrayHeader(&iter) catch return false; while (len > 0) : (len -= 1) { @@ -1543,6 +1544,23 @@ pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, return false; } +pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, active: bool, selected: bool, hover: bool, theme_: *const Widget.Theme) bool { + var iter = file_item_cbor; + var file_path_: []const u8 = undefined; + var icon: []const u8 = undefined; + var color: u24 = undefined; + var indicator: []const u8 = undefined; + var matches_cbor: []const u8 = undefined; + + if (!(cbor.matchString(&iter, &file_path_) catch false)) @panic("invalid buffer file path"); + if (!(cbor.matchString(&iter, &icon) catch false)) @panic("invalid buffer file type icon"); + if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid buffer file type color"); + if (!(cbor.matchString(&iter, &indicator) catch false)) indicator = ""; + + if (!(cbor.matchValue(&iter, cbor.extract_cbor(&matches_cbor)) catch false)) @panic("invalid matches cbor"); + return render_file_item(self, file_path_, icon, color, indicator, matches_cbor, active, selected, hover, theme_); +} + 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| { From 057a9d60cd09fdeb8c8aa369a48fc19ba444ad69 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 15 Aug 2025 23:30:54 +0200 Subject: [PATCH 71/87] feat: add completion palette --- src/keybind/builtin/flow.json | 1 + src/tui/editor.zig | 12 +- src/tui/mainview.zig | 7 +- src/tui/mode/overlay/completion_palette.zig | 175 ++++++++++++++++++++ 4 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 src/tui/mode/overlay/completion_palette.zig diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index ed576da..30ddc9c 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -165,6 +165,7 @@ ["shift+f11", "toggle_highlight_columns"], ["ctrl+f11", "toggle_inspector_view"], ["f12", "goto_definition"], + ["ctrl+.", "completion"], ["f34", "toggle_whitespace_mode"], ["escape", "cancel"], ["enter", "smart_insert_line"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 03b5900..d90d6cc 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -351,6 +351,9 @@ pub const Editor = struct { diag_hints: usize = 0, completions: std.ArrayListUnmanaged(u8) = .empty, + completion_row: usize = 0, + completion_col: usize = 0, + completion_is_complete: bool = true, enable_auto_save: bool, enable_format_on_save: bool, @@ -5678,10 +5681,13 @@ pub const Editor = struct { } pub fn add_completion(self: *Self, row: usize, col: usize, is_incomplete: bool, msg: tp.message) Result { + if (!(row == self.completion_row and col == self.completion_col)) { + self.completions.clearRetainingCapacity(); + self.completion_row = row; + self.completion_col = col; + } try self.completions.appendSlice(self.allocator, msg.buf); - _ = row; - _ = col; - _ = is_incomplete; + self.completion_is_complete = is_incomplete; } pub fn select(self: *Self, ctx: Context) Result { diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 197fbcf..288963a 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -827,8 +827,11 @@ const cmds = struct { tp.more, })) return error.InvalidAddDiagnosticArgument; file_path = project_manager.normalize_file_path(file_path); - if (self.get_active_editor()) |editor| if (std.mem.eql(u8, file_path, editor.file_path orelse "")) - try editor.add_completion(row, col, is_incomplete, ctx.args); + if (self.get_active_editor()) |editor| { + if (std.mem.eql(u8, file_path, editor.file_path orelse "")) + try editor.add_completion(row, col, is_incomplete, ctx.args); + try tui.open_overlay(@import("mode/overlay/completion_palette.zig").Type); + } } pub const add_completion_meta: Meta = .{ .arguments = &.{ diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig new file mode 100644 index 0000000..eac3856 --- /dev/null +++ b/src/tui/mode/overlay/completion_palette.zig @@ -0,0 +1,175 @@ +const std = @import("std"); +const cbor = @import("cbor"); +const tp = @import("thespian"); +const root = @import("root"); +const command = @import("command"); + +const tui = @import("../../tui.zig"); +pub const Type = @import("palette.zig").Create(@This()); +const module_name = @typeName(@This()); +const Widget = @import("../../Widget.zig"); + +pub const label = "Select completion"; +pub const name = "completion"; +pub const description = "completions"; +pub const icon = "󱎸 "; + +pub const Entry = struct { + label: []const u8, + sort_text: []const u8, + cbor: []const u8, +}; + +pub fn load_entries(palette: *Type) !usize { + const editor = tui.get_active_editor() orelse return error.NotFound; + var iter: []const u8 = editor.completions.items; + while (iter.len > 0) { + var cbor_item: []const u8 = undefined; + if (!try cbor.matchValue(&iter, cbor.extract_cbor(&cbor_item))) return error.BadCompletion; + (try palette.entries.addOne()).* = .{ .cbor = cbor_item, .label = undefined, .sort_text = undefined }; + } + + var max_label_len: usize = 0; + for (palette.entries.items) |*item| { + const label_, const sort_text, _ = get_values(item.cbor); + item.label = label_; + item.sort_text = sort_text; + max_label_len = @max(max_label_len, item.label.len); + } + + const less_fn = struct { + fn less_fn(_: void, lhs: Entry, rhs: Entry) bool { + const lhs_str = if (lhs.sort_text.len > 0) lhs.sort_text else lhs.label; + const rhs_str = if (rhs.sort_text.len > 0) rhs.sort_text else rhs.label; + return std.mem.order(u8, lhs_str, rhs_str) == .lt; + } + }.less_fn; + std.mem.sort(Entry, palette.entries.items, {}, less_fn); + + return if (max_label_len > label.len + 3) 0 else label.len + 3 - max_label_len; +} + +pub fn clear_entries(palette: *Type) void { + palette.entries.clearRetainingCapacity(); +} + +pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { + var value = std.ArrayList(u8).init(palette.allocator); + defer value.deinit(); + const writer = value.writer(); + try writer.writeAll(entry.cbor); + try cbor.writeValue(writer, matches orelse &[_]usize{}); + try palette.menu.add_item_with_handler(value.items, select); + palette.items += 1; +} + +pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget.Theme, selected: bool) bool { + var item_cbor: []const u8 = undefined; + var matches_cbor: []const u8 = undefined; + + var iter = button.opts.label; + if (!(cbor.matchValue(&iter, cbor.extract_cbor(&item_cbor)) catch false)) return false; + if (!(cbor.matchValue(&iter, cbor.extract_cbor(&matches_cbor)) catch false)) return false; + + const label_, _, const kind = get_values(item_cbor); + const icon_: []const u8 = kind_icon(@enumFromInt(kind)); + const color: u24 = 0x0; + const indicator: []const u8 = &.{}; + + return tui.render_file_item(&button.plane, label_, icon_, color, indicator, matches_cbor, button.active, selected, button.hover, theme); +} + +fn get_values(item_cbor: []const u8) struct { []const u8, []const u8, u8 } { + var label_: []const u8 = ""; + var sort_text: []const u8 = ""; + var kind: u8 = 0; + _ = cbor.match(item_cbor, .{ + cbor.any, // file_path + cbor.any, // row + cbor.any, // col + cbor.any, // is_incomplete + cbor.extract(&label_), // label + cbor.any, // label_detail + cbor.any, // label_description + cbor.extract(&kind), // kind + cbor.any, // detail + cbor.any, // documentation + cbor.any, // documentation_kind + cbor.extract(&sort_text), // sortText + cbor.any, // insertTextFormat + cbor.any, // textEdit_newText + cbor.any, // insert.begin.row + cbor.any, // insert.begin.col + cbor.any, // insert.end.row + cbor.any, // insert.end.col + cbor.any, // replace.begin.row + cbor.any, // replace.begin.col + cbor.any, // replace.end.row + cbor.any, // replace.end.col + }) catch false; + return .{ label_, sort_text, kind }; +} + +fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { + const label_, _, _ = get_values(button.opts.label); + tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + tp.self_pid().send(.{ "cmd", "insert_chars", .{label_} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); +} + +const CompletionItemKind = enum(u8) { + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +}; + +fn kind_icon(kind: CompletionItemKind) []const u8 { + return switch (kind) { + .Text => "󰊄", + .Method => "", + .Function => "󰊕", + .Constructor => "", + .Field => "", + .Variable => "", + .Class => "", + .Interface => "", + .Module => "", + .Property => "", + .Unit => "󱔁", + .Value => "󱔁", + .Enum => "", + .Keyword => "", + .Snippet => "", + .Color => "", + .File => "", + .Reference => "※", + .Folder => "🗀", + .EnumMember => "", + .Constant => "", + .Struct => "", + .Event => "", + .Operator => "", + .TypeParameter => "", + }; +} From 523085ba70e8e943ee925ca2eb17bb85dcdc52d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Sat, 16 Aug 2025 17:36:36 +0200 Subject: [PATCH 72/87] file_types: invoke OmniSharp instead of omnisharp The former is the canonical executable name, and also what Helix uses. Seems reasonable to standardize on that. --- src/syntax/src/file_types.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/syntax/src/file_types.zig b/src/syntax/src/file_types.zig index efb8ca2..e75ad4a 100644 --- a/src/syntax/src/file_types.zig +++ b/src/syntax/src/file_types.zig @@ -41,7 +41,7 @@ pub const @"c-sharp" = .{ .icon = "󰌛", .extensions = .{"cs"}, .comment = "//", - .language_server = .{ "omnisharp", "-lsp" }, + .language_server = .{ "OmniSharp", "-lsp" }, .formatter = .{ "csharpier", "format" }, }; From 0a60f378578a64192c28a6dd30dab84bec31ddfc Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 16 Aug 2025 22:47:33 +0200 Subject: [PATCH 73/87] build: update libvaxis --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 57fc456..53801fd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -27,8 +27,8 @@ .hash = "fuzzig-0.1.1-AAAAALNIAQBmbHr-MPalGuR393Vem2pTQXI7_LXeNJgX", }, .vaxis = .{ - .url = "git+https://github.com/neurocyte/libvaxis?ref=main#846ddb8bf483e8a7eb25628d6c34ba7e781155b6", - .hash = "vaxis-0.5.1-BWNV_AsQCQDvfb-li1CZEOBG_YsteinP9qI-PpV47-jf", + .url = "git+https://github.com/neurocyte/libvaxis?ref=main#7d8015ee82bd547e92b433fc698ce86a0d87b18b", + .hash = "vaxis-0.5.1-BWNV_CIVCQC3Bk1-nguTPlKtXmm7AyPOZd95Ut2KYLVD", }, .zeit = .{ .url = "https://github.com/rockorager/zeit/archive/8fd203f85f597f16e0a525c1f1ca1e0bffded809.tar.gz", From 7207b0435e26c794a2d5978a14c1b7c348953dcd Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 17 Aug 2025 22:31:15 +0200 Subject: [PATCH 74/87] feat: add support for specifying positions in bytes in goto_line_and_column --- src/tui/editor.zig | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index d90d6cc..499fe18 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -55,6 +55,8 @@ pub const whitespace = struct { }; }; +pub const PosType = enum { column, byte }; + pub const Match = struct { begin: Cursor = Cursor{}, end: Cursor = Cursor{}, @@ -5410,11 +5412,18 @@ pub const Editor = struct { var column: usize = 0; var have_sel: bool = false; var sel: Selection = .{}; + var pos_type: PosType = .column; if (try ctx.args.match(.{ tp.extract(&line), tp.extract(&column), })) { // self.logger.print("goto: l:{d} c:{d}", .{ line, column }); + } else if (try ctx.args.match(.{ + tp.extract(&line), + tp.extract(&column), + tp.extract(&pos_type), + })) { + // self.logger.print("goto: l:{d} c:{d}", .{ line, column }); } else if (try ctx.args.match(.{ tp.extract(&line), tp.extract(&column), @@ -5425,9 +5434,29 @@ pub const Editor = struct { })) { // self.logger.print("goto: l:{d} c:{d} {any}", .{ line, column, sel }); have_sel = true; + } else if (try ctx.args.match(.{ + tp.extract(&line), + tp.extract(&column), + tp.extract(&sel.begin.row), + tp.extract(&sel.begin.col), + tp.extract(&sel.end.row), + tp.extract(&sel.end.col), + tp.extract(&pos_type), + })) { + // self.logger.print("goto: l:{d} c:{d} {any} {}", .{ line, column, sel, pos_type }); + have_sel = true; } else return error.InvalidGotoLineAndColumnArgument; self.cancel_all_selections(); const root = self.buf_root() catch return; + if (pos_type == .byte) { + column = root.pos_to_width(line - 1, column - 1, self.metrics) catch return; + column += 1; + if (have_sel) { + sel.begin.col = root.pos_to_width(sel.begin.row, sel.begin.col, self.metrics) catch return; + sel.end.col = root.pos_to_width(sel.end.row, sel.end.col, self.metrics) catch return; + } + // self.logger.print("goto_byte_pos: l:{d} c:{d} {any} {}", .{ line, column, sel, pos_type }); + } const primary = self.get_primary(); try primary.cursor.move_to( root, From 4188e25df99accc39239e44c37adb430aa68d192 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 17 Aug 2025 22:32:44 +0200 Subject: [PATCH 75/87] feat: add support for specifying byte positions in filelist_view --- src/tui/filelist_view.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tui/filelist_view.zig b/src/tui/filelist_view.zig index 7544dce..26efe59 100644 --- a/src/tui/filelist_view.zig +++ b/src/tui/filelist_view.zig @@ -46,6 +46,7 @@ const Entry = struct { end_pos: usize, lines: []const u8, severity: editor.Diagnostic.Severity = .Information, + pos_type: editor.PosType, }; pub fn create(allocator: Allocator, parent: Plane) !Widget { @@ -250,6 +251,7 @@ fn handle_menu_action(menu: **Menu.State(*Self), button: *Button.State(*Menu.Sta if (entry.begin_pos == 0) 0 else entry.begin_pos + 1, entry.end_line, entry.end_pos + 1, + entry.pos_type, }, } }) catch |e| self.logger.err("navigate", e); } From 06a31ea5fd2dcafa4402c6801b0e16983a9b6267 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 17 Aug 2025 22:33:24 +0200 Subject: [PATCH 76/87] fix: mark find in files results as byte positions --- src/tui/mainview.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 288963a..6166b29 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -1423,6 +1423,7 @@ fn add_find_in_files_result( .end_pos = @max(1, end_pos) - 1, .lines = lines, .severity = severity, + .pos_type = .byte, }) catch |e| return tp.exit_error(e, @errorReturnTrace()); } From cdd1e09069ef7ebfe14a71eeae40ec39d2468008 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 18 Aug 2025 14:42:34 +0200 Subject: [PATCH 77/87] feat: add support for loading palettes with arguments --- src/tui/mode/overlay/palette.zig | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index cfadc52..927e3a0 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -47,6 +47,10 @@ pub fn Create(options: type) type { pub const ButtonState = Button.State(*Menu.State(*Self)); pub fn create(allocator: std.mem.Allocator) !tui.Mode { + return create_with_args(allocator, .{}); + } + + pub fn create_with_args(allocator: std.mem.Allocator, ctx: command.Context) !tui.Mode { const mv = tui.mainview() orelse return error.NotFound; const self = try allocator.create(Self); errdefer allocator.destroy(self); @@ -77,7 +81,10 @@ pub fn Create(options: type) type { .entries = std.ArrayList(Entry).init(allocator), }; if (self.menu.scrollbar) |scrollbar| scrollbar.style_factory = scrollbar_style; - self.longest_hint = try options.load_entries(self); + self.longest_hint = if (@hasDecl(options, "load_entries_with_args")) + try options.load_entries_with_args(self, ctx) + else + try options.load_entries(self); if (@hasDecl(options, "restore_state")) options.restore_state(self) catch {}; try self.commands.init(self); @@ -467,11 +474,15 @@ pub fn Create(options: type) type { const button = self.menu.get_selected() orelse return; const refresh = options.delete_item(self.menu, button); if (refresh) { - options.clear_entries(self); - self.longest_hint = try options.load_entries(self); - if (self.entries.items.len > 0) - self.initial_selected = self.menu.selected; - try self.start_query(0); + if (@hasDecl(options, "load_entries")) { + options.clear_entries(self); + self.longest_hint = try options.load_entries(self); + if (self.entries.items.len > 0) + self.initial_selected = self.menu.selected; + try self.start_query(0); + } else { + return palette_menu_cancel(self, .{}); + } } } } From 7d7a45f539ace91254bf84dd0a37b1bb3b07c5f9 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 18 Aug 2025 14:46:13 +0200 Subject: [PATCH 78/87] refactor: make project_manager.request_recent_projects fully async --- src/project_manager.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/project_manager.zig b/src/project_manager.zig index 925294a..fc73c9e 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -98,9 +98,9 @@ pub fn request_recent_files(max: usize) (ProjectManagerError || ProjectError)!vo return send(.{ "request_recent_files", project, max }); } -pub fn request_recent_projects(allocator: std.mem.Allocator) (ProjectError || CallError)!tp.message { +pub fn request_recent_projects() (ProjectManagerError || ProjectError)!void { const project = tp.env.get().str("project"); - return (try get()).pid.call(allocator, request_timeout, .{ "request_recent_projects", project }); + return send(.{ "request_recent_projects", project }); } pub fn query_recent_files(max: usize, query: []const u8) (ProjectManagerError || ProjectError)!void { @@ -462,6 +462,9 @@ const Process = struct { self.sort_projects_by_last_used(&recent_projects); var message = std.ArrayList(u8).init(self.allocator); const writer = message.writer(); + try cbor.writeArrayHeader(writer, 3); + try cbor.writeValue(writer, "PRJ"); + try cbor.writeValue(writer, "recent_projects"); try cbor.writeArrayHeader(writer, recent_projects.items.len); for (recent_projects.items) |project| { try cbor.writeArrayHeader(writer, 2); @@ -469,6 +472,7 @@ const Process = struct { try cbor.writeValue(writer, if (self.projects.get(project.name)) |_| true else false); } from.send_raw(.{ .buf = message.items }) catch return error.ClientFailed; + self.logger.print("{d} projects found", .{recent_projects.items.len}); } fn query_recent_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize, query: []const u8) (ProjectError || Project.ClientError)!void { From d53a24a1d2338d12914202438312a0be011a74d5 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 18 Aug 2025 14:47:01 +0200 Subject: [PATCH 79/87] fix: make open_recent_projects run async --- src/tui/mode/overlay/open_recent_project.zig | 10 ++++++---- src/tui/tui.zig | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/tui/mode/overlay/open_recent_project.zig b/src/tui/mode/overlay/open_recent_project.zig index d9fd219..aef1729 100644 --- a/src/tui/mode/overlay/open_recent_project.zig +++ b/src/tui/mode/overlay/open_recent_project.zig @@ -27,10 +27,12 @@ pub fn deinit(palette: *Type) void { palette.allocator.free(entry.label); } -pub fn load_entries(palette: *Type) !usize { - const rsp = try project_manager.request_recent_projects(palette.allocator); - defer palette.allocator.free(rsp.buf); - var iter: []const u8 = rsp.buf; +pub fn load_entries_with_args(palette: *Type, ctx: command.Context) !usize { + var items_cbor: []const u8 = undefined; + if (!(cbor.match(ctx.args.buf, .{ "PRJ", "recent_projects", tp.extract_cbor(&items_cbor) }) catch false)) + return error.InvalidRecentProjects; + + var iter: []const u8 = items_cbor; var len = try cbor.decodeArrayHeader(&iter); while (len > 0) : (len -= 1) { var name_: []const u8 = undefined; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 7ccdfe4..3bc7c39 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -413,6 +413,9 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { return; } + if (try m.match(.{ "PRJ", "recent_projects", tp.more })) // async recent projects request + return self.enter_overlay_mode_with_args(@import("mode/overlay/open_recent_project.zig").Type, .{ .args = m }); + if (try m.match(.{ "PRJ", tp.more })) // drop late project manager query responses return; @@ -684,6 +687,17 @@ fn enter_overlay_mode(self: *Self, mode: type) command.Result { refresh_hover(); } +fn enter_overlay_mode_with_args(self: *Self, mode: type, ctx: command.Context) command.Result { + command.executeName("disable_fast_scroll", .{}) catch {}; + command.executeName("disable_jump_mode", .{}) catch {}; + if (self.mini_mode_) |_| try cmds.exit_mini_mode(self, .{}); + if (self.input_mode_outer_) |_| try cmds.exit_overlay_mode(self, .{}); + self.input_mode_outer_ = self.input_mode_; + self.input_mode_ = try mode.create_with_args(self.allocator, ctx); + if (self.input_mode_) |*m| m.run_init(); + refresh_hover(); +} + fn get_input_mode(self: *Self, mode_name: []const u8) !Mode { return keybind.mode(mode_name, self.allocator, .{}); } @@ -914,8 +928,8 @@ const cmds = struct { } pub const open_recent_meta: Meta = .{ .description = "Open recent" }; - pub fn open_recent_project(self: *Self, _: Ctx) Result { - return self.enter_overlay_mode(@import("mode/overlay/open_recent_project.zig").Type); + pub fn open_recent_project(_: *Self, _: Ctx) Result { + try project_manager.request_recent_projects(); } pub const open_recent_project_meta: Meta = .{ .description = "Open project" }; From 9d127e4cc3df0fa13df7c0a71645a64711df2cfb Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 18 Aug 2025 15:14:50 +0200 Subject: [PATCH 80/87] fix: add support for kitty mouse leave events This prevents reporting kitty mouse leave events as spurious mouse clicks. --- build.zig.zon | 4 ++-- src/renderer/vaxis/renderer.zig | 3 +++ src/tui/tui.zig | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 53801fd..cb21721 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -27,8 +27,8 @@ .hash = "fuzzig-0.1.1-AAAAALNIAQBmbHr-MPalGuR393Vem2pTQXI7_LXeNJgX", }, .vaxis = .{ - .url = "git+https://github.com/neurocyte/libvaxis?ref=main#7d8015ee82bd547e92b433fc698ce86a0d87b18b", - .hash = "vaxis-0.5.1-BWNV_CIVCQC3Bk1-nguTPlKtXmm7AyPOZd95Ut2KYLVD", + .url = "git+https://github.com/neurocyte/libvaxis?ref=main#beb82aa0a9d77f63462f8ca8250c2b9eecf057a2", + .hash = "vaxis-0.5.1-BWNV_FMWCQDGtkYFKz_85wSkBRO1kynUZaPO1-RCSrQM", }, .zeit = .{ .url = "https://github.com/rockorager/zeit/archive/8fd203f85f597f16e0a525c1f1ca1e0bffded809.tar.gz", diff --git a/src/renderer/vaxis/renderer.zig b/src/renderer/vaxis/renderer.zig index 3141e34..e388db0 100644 --- a/src/renderer/vaxis/renderer.zig +++ b/src/renderer/vaxis/renderer.zig @@ -323,6 +323,9 @@ pub fn process_renderer_event(self: *Self, msg: []const u8) Error!void { })), }; }, + .mouse_leave => { + if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"mouse_leave"})); + }, .focus_in => { if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"focus_in"})); }, diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 3bc7c39..75c7b06 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -389,6 +389,9 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { return; } + if (try m.match(.{"mouse_leave"})) + return; + if (try m.match(.{"focus_in"})) return; From 9bc25620cb6ec7c4742b1fa1f10c76d5efd81631 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 18 Aug 2025 21:07:49 +0200 Subject: [PATCH 81/87] fix: over eager whitespace collapsing in smart_insert_line --- src/tui/editor.zig | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 499fe18..393cd0e 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -4480,7 +4480,9 @@ pub const Editor = struct { }; } - fn cursel_smart_insert_line(self: *Self, root: Buffer.Root, cursel: *CurSel, b_allocator: std.mem.Allocator) !Buffer.Root { + const WSCollapseMode = enum { leave_ws, collapse_ws }; + + fn cursel_smart_insert_line(self: *Self, root: Buffer.Root, cursel: *CurSel, b_allocator: std.mem.Allocator, mode: WSCollapseMode) !Buffer.Root { const row = cursel.cursor.row; const leading_ws = @min(find_first_non_ws(root, row, self.metrics), cursel.cursor.col); var sfa = std.heap.stackFallback(512, self.allocator); @@ -4491,7 +4493,8 @@ pub const Editor = struct { _ = try writer.write("\n"); try self.generate_leading_ws(&writer, leading_ws); var root_ = try self.insert(root, cursel, stream.items, b_allocator); - root_ = self.collapse_trailing_ws_line(root_, row, b_allocator); + if (mode == .collapse_ws) + root_ = self.collapse_trailing_ws_line(root_, row, b_allocator); const leading_ws_ = find_first_non_ws(root_, cursel.cursor.row, self.metrics); if (leading_ws_ > leading_ws and leading_ws_ > cursel.cursor.col) { const sel = try cursel.enable_selection(root_, self.metrics); @@ -4522,11 +4525,11 @@ pub const Editor = struct { (std.mem.eql(u8, egc_right, "(") and std.mem.eql(u8, egc_left, ")")); }; - root = try self.cursel_smart_insert_line(root, cursel, b.allocator); + root = try self.cursel_smart_insert_line(root, cursel, b.allocator, .collapse_ws); if (smart_brace_indent) { const cursor = cursel.cursor; - root = try self.cursel_smart_insert_line(root, cursel, b.allocator); + root = try self.cursel_smart_insert_line(root, cursel, b.allocator, .leave_ws); cursel.cursor = cursor; if (indent_extra) root = try self.indent_cursel(root, cursel, b.allocator); From 9c926883f880d6298980f7e681d393185f56d9d5 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 19 Aug 2025 11:57:40 +0200 Subject: [PATCH 82/87] refactor: send LSP client messages to project_manager instead of log --- src/LSP.zig | 83 +++++++++++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/src/LSP.zig b/src/LSP.zig index 58012a3..fde015b 100644 --- a/src/LSP.zig +++ b/src/LSP.zig @@ -3,7 +3,6 @@ const tp = @import("thespian"); const cbor = @import("cbor"); const root = @import("root"); const tracy = @import("tracy"); -const log = @import("log"); allocator: std.mem.Allocator, pid: tp.pid, @@ -188,10 +187,8 @@ const Process = struct { } else if (try cbor.match(cmd.buf, .{ tp.extract(&tag), tp.more })) { // } else { - const logger = log.logger("LSP"); - defer logger.deinit(); var buf: [1024]u8 = undefined; - logger.print_err("create", "invalid command: {d} {s}", .{ cmd.buf.len, cmd.to_json(&buf) catch "{command too large}" }); + send_msg(tp.self_pid().clone(), tag, .err, "invalid command: {d} {s}", .{ cmd.buf.len, cmd.to_json(&buf) catch "{command too large}" }); return error.InvalidLspCommand; } const self = try allocator.create(Process); @@ -245,6 +242,20 @@ const Process = struct { } } + fn msg(self: *const Process, comptime fmt: anytype, args: anytype) void { + send_msg(self.parent, self.tag, .msg, fmt, args); + } + + fn err_msg(self: *const Process, comptime fmt: anytype, args: anytype) void { + send_msg(self.parent, self.tag, .err, fmt, args); + } + + fn send_msg(proc: tp.pid, tag: []const u8, type_: enum { msg, err }, comptime fmt: anytype, args: anytype) void { + var buf: [@import("log").max_log_message]u8 = undefined; + const output = std.fmt.bufPrint(&buf, fmt, args) catch "MESSAGE TOO LARGE"; + proc.send(.{ "lsp", type_, tag, output }) catch {}; + } + fn start(self: *Process) tp.result { const frame = tracy.initZone(@src(), .{ .name = module_name ++ " start" }); defer frame.deinit(); @@ -440,18 +451,14 @@ const Process = struct { } fn handle_not_found(self: *Process) error{ExitNormal}!void { - const logger = log.logger("LSP"); - defer logger.deinit(); - logger.print_err("init", "'{s}' executable not found", .{self.tag}); + self.err_msg("'{s}' executable not found", .{self.tag}); self.write_log("### '{s}' executable not found ###\n", .{self.tag}); self.parent.send(.{ sp_tag, self.tag, "not found" }) catch {}; return error.ExitNormal; } fn handle_terminated(self: *Process, err: []const u8, code: u32) error{ExitNormal}!void { - const logger = log.logger("LSP"); - defer logger.deinit(); - logger.print("terminated: {s} {d}", .{ err, code }); + self.msg("terminated: {s} {d}", .{ err, code }); self.write_log("### subprocess terminated {s} {d} ###\n", .{ err, code }); self.parent.send(.{ sp_tag, self.tag, "done" }) catch {}; return error.ExitNormal; @@ -463,9 +470,9 @@ const Process = struct { const id = self.next_id; self.next_id += 1; - var msg = std.ArrayList(u8).init(self.allocator); - defer msg.deinit(); - const msg_writer = msg.writer(); + var request = std.ArrayList(u8).init(self.allocator); + defer request.deinit(); + const msg_writer = request.writer(); try cbor.writeMapHeader(msg_writer, 4); try cbor.writeValue(msg_writer, "jsonrpc"); try cbor.writeValue(msg_writer, "2.0"); @@ -476,7 +483,7 @@ const Process = struct { try cbor.writeValue(msg_writer, "params"); _ = try msg_writer.write(params_cb); - const json = try cbor.toJsonAlloc(self.allocator, msg.items); + const json = try cbor.toJsonAlloc(self.allocator, request.items); defer self.allocator.free(json); var output = std.ArrayList(u8).init(self.allocator); defer output.deinit(); @@ -499,9 +506,9 @@ const Process = struct { fn send_response(self: *Process, cbor_id: []const u8, result_cb: []const u8) (error{Closed} || SendError || cbor.Error || cbor.JsonEncodeError)!void { const sp = if (self.sp) |*sp| sp else return error.Closed; - var msg = std.ArrayList(u8).init(self.allocator); - defer msg.deinit(); - const msg_writer = msg.writer(); + var response = std.ArrayList(u8).init(self.allocator); + defer response.deinit(); + const msg_writer = response.writer(); try cbor.writeMapHeader(msg_writer, 3); try cbor.writeValue(msg_writer, "jsonrpc"); try cbor.writeValue(msg_writer, "2.0"); @@ -510,7 +517,7 @@ const Process = struct { try cbor.writeValue(msg_writer, "result"); _ = try msg_writer.write(result_cb); - const json = try cbor.toJsonAlloc(self.allocator, msg.items); + const json = try cbor.toJsonAlloc(self.allocator, response.items); defer self.allocator.free(json); var output = std.ArrayList(u8).init(self.allocator); defer output.deinit(); @@ -528,9 +535,9 @@ const Process = struct { fn send_error_response(self: *Process, cbor_id: []const u8, error_code: ErrorCode, message: []const u8) (error{Closed} || SendError || cbor.Error || cbor.JsonEncodeError)!void { const sp = if (self.sp) |*sp| sp else return error.Closed; - var msg = std.ArrayList(u8).init(self.allocator); - defer msg.deinit(); - const msg_writer = msg.writer(); + var response = std.ArrayList(u8).init(self.allocator); + defer response.deinit(); + const msg_writer = response.writer(); try cbor.writeMapHeader(msg_writer, 3); try cbor.writeValue(msg_writer, "jsonrpc"); try cbor.writeValue(msg_writer, "2.0"); @@ -543,7 +550,7 @@ const Process = struct { try cbor.writeValue(msg_writer, "message"); try cbor.writeValue(msg_writer, message); - const json = try cbor.toJsonAlloc(self.allocator, msg.items); + const json = try cbor.toJsonAlloc(self.allocator, response.items); defer self.allocator.free(json); var output = std.ArrayList(u8).init(self.allocator); defer output.deinit(); @@ -563,9 +570,9 @@ const Process = struct { const have_params = !(cbor.match(params_cb, cbor.null_) catch false); - var msg = std.ArrayList(u8).init(self.allocator); - defer msg.deinit(); - const msg_writer = msg.writer(); + var notification = std.ArrayList(u8).init(self.allocator); + defer notification.deinit(); + const msg_writer = notification.writer(); try cbor.writeMapHeader(msg_writer, 3); try cbor.writeValue(msg_writer, "jsonrpc"); try cbor.writeValue(msg_writer, "2.0"); @@ -578,7 +585,7 @@ const Process = struct { try cbor.writeMapHeader(msg_writer, 0); } - const json = try cbor.toJsonAlloc(self.allocator, msg.items); + const json = try cbor.toJsonAlloc(self.allocator, notification.items); defer self.allocator.free(json); var output = std.ArrayList(u8).init(self.allocator); defer output.deinit(); @@ -617,9 +624,9 @@ const Process = struct { const json = if (params) |p| try cbor.toJsonPrettyAlloc(self.allocator, p) else null; defer if (json) |p| self.allocator.free(p); self.write_log("### RECV req: {s}\nmethod: {s}\n{s}\n###\n", .{ json_id, method, json orelse "no params" }); - var msg = std.ArrayList(u8).init(self.allocator); - defer msg.deinit(); - const writer = msg.writer(); + var request = std.ArrayList(u8).init(self.allocator); + defer request.deinit(); + const writer = request.writer(); try cbor.writeArrayHeader(writer, 7); try cbor.writeValue(writer, sp_tag); try cbor.writeValue(writer, self.project); @@ -628,7 +635,7 @@ const Process = struct { try cbor.writeValue(writer, method); try writer.writeAll(cbor_id); if (params) |p| _ = try writer.write(p) else try cbor.writeValue(writer, null); - self.parent.send_raw(.{ .buf = msg.items }) catch return error.SendFailed; + self.parent.send_raw(.{ .buf = request.items }) catch return error.SendFailed; } fn receive_lsp_response(self: *Process, cbor_id: []const u8, result: ?[]const u8, err: ?[]const u8) Error!void { @@ -640,9 +647,9 @@ const Process = struct { defer if (json_err) |p| self.allocator.free(p); self.write_log("### RECV rsp: {s} {s}\n{s}\n###\n", .{ json_id, if (json_err) |_| "error" else "response", json_err orelse json orelse "no result" }); const from = self.requests.get(cbor_id) orelse return; - var msg = std.ArrayList(u8).init(self.allocator); - defer msg.deinit(); - const writer = msg.writer(); + var response = std.ArrayList(u8).init(self.allocator); + defer response.deinit(); + const writer = response.writer(); try cbor.writeArrayHeader(writer, 4); try cbor.writeValue(writer, sp_tag); try cbor.writeValue(writer, self.tag); @@ -653,16 +660,16 @@ const Process = struct { try cbor.writeValue(writer, "result"); _ = try writer.write(result_); } - from.send_raw(.{ .buf = msg.items }) catch return error.SendFailed; + from.send_raw(.{ .buf = response.items }) catch return error.SendFailed; } fn receive_lsp_notification(self: *Process, method: []const u8, params: ?[]const u8) Error!void { const json = if (params) |p| try cbor.toJsonPrettyAlloc(self.allocator, p) else null; defer if (json) |p| self.allocator.free(p); self.write_log("### RECV notify:\nmethod: {s}\n{s}\n###\n", .{ method, json orelse "no params" }); - var msg = std.ArrayList(u8).init(self.allocator); - defer msg.deinit(); - const writer = msg.writer(); + var notification = std.ArrayList(u8).init(self.allocator); + defer notification.deinit(); + const writer = notification.writer(); try cbor.writeArrayHeader(writer, 6); try cbor.writeValue(writer, sp_tag); try cbor.writeValue(writer, self.project); @@ -670,7 +677,7 @@ const Process = struct { try cbor.writeValue(writer, "notify"); try cbor.writeValue(writer, method); if (params) |p| _ = try writer.write(p) else try cbor.writeValue(writer, null); - self.parent.send_raw(.{ .buf = msg.items }) catch return error.SendFailed; + self.parent.send_raw(.{ .buf = notification.items }) catch return error.SendFailed; } fn write_log(self: *Process, comptime format: []const u8, args: anytype) void { From d0e175a5d7e1cd5b16c29aafb9e13b23bb11b5e4 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 19 Aug 2025 11:59:17 +0200 Subject: [PATCH 83/87] fix: use OS path separator when creating LSP log file --- src/LSP.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LSP.zig b/src/LSP.zig index fde015b..a3480df 100644 --- a/src/LSP.zig +++ b/src/LSP.zig @@ -266,7 +266,7 @@ const Process = struct { var log_file_path = std.ArrayList(u8).init(self.allocator); defer log_file_path.deinit(); const state_dir = root.get_state_dir() catch |e| return tp.exit_error(e, @errorReturnTrace()); - log_file_path.writer().print("{s}/lsp-{s}.log", .{ state_dir, self.tag }) catch |e| return tp.exit_error(e, @errorReturnTrace()); + log_file_path.writer().print("{s}{c}lsp-{s}.log", .{ state_dir, std.fs.path.sep, self.tag }) catch |e| return tp.exit_error(e, @errorReturnTrace()); self.log_file = std.fs.createFileAbsolute(log_file_path.items, .{ .truncate = true }) catch |e| return tp.exit_error(e, @errorReturnTrace()); } From 72423471f1bbddc2786ee0d6acb3b4ab575a357a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 19 Aug 2025 11:59:52 +0200 Subject: [PATCH 84/87] feat: store LSP log file name in LSP client --- src/LSP.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/LSP.zig b/src/LSP.zig index a3480df..185e95d 100644 --- a/src/LSP.zig +++ b/src/LSP.zig @@ -171,6 +171,7 @@ const Process = struct { project: [:0]const u8, sp_tag: [:0]const u8, log_file: ?std.fs.File = null, + log_file_path: ?[]const u8 = null, next_id: i32 = 0, requests: std.StringHashMap(tp.pid), state: enum { init, running } = .init, @@ -224,6 +225,7 @@ const Process = struct { self.close() catch {}; self.write_log("### terminated LSP process ###\n", .{}); if (self.log_file) |file| file.close(); + if (self.log_file_path) |file_path| self.allocator.free(file_path); } fn close(self: *Process) error{CloseFailed}!void { @@ -268,6 +270,7 @@ const Process = struct { const state_dir = root.get_state_dir() catch |e| return tp.exit_error(e, @errorReturnTrace()); log_file_path.writer().print("{s}{c}lsp-{s}.log", .{ state_dir, std.fs.path.sep, self.tag }) catch |e| return tp.exit_error(e, @errorReturnTrace()); self.log_file = std.fs.createFileAbsolute(log_file_path.items, .{ .truncate = true }) catch |e| return tp.exit_error(e, @errorReturnTrace()); + self.log_file_path = log_file_path.toOwnedSlice() catch null; } fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result { From 69ea49549565dffefb28f749ab6fe52c362f925e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 20 Aug 2025 21:00:24 +0200 Subject: [PATCH 85/87] feat: add config option lsp_output "quiet" to reduce LSP log verbosity Set the option to "verbose" to re-enable logging of LSP show/logMessage requests. --- src/Project.zig | 13 +++++++++++-- src/config.zig | 2 ++ src/project_manager.zig | 5 +++-- src/tui/tui.zig | 2 ++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Project.zig b/src/Project.zig index 6613215..85cfd0f 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -1530,7 +1530,16 @@ fn read_position(position: []const u8) !Position { return .{ .line = line.?, .character = character.? }; } -pub fn show_message(self: *Self, _: tp.pid_ref, params_cb: []const u8) !void { +pub fn show_message(self: *Self, params_cb: []const u8) !void { + return self.show_or_log_message(.show, params_cb); +} + +pub fn log_message(self: *Self, params_cb: []const u8) !void { + return self.show_or_log_message(.log, params_cb); +} + +fn show_or_log_message(self: *Self, operation: enum { show, log }, params_cb: []const u8) !void { + if (!tp.env.get().is("lsp_verbose")) return; var type_: i32 = 0; var message: ?[]const u8 = null; var iter = params_cb; @@ -1550,7 +1559,7 @@ pub fn show_message(self: *Self, _: tp.pid_ref, params_cb: []const u8) !void { if (type_ <= 2) self.logger_lsp.err_msg("lsp", msg) else - self.logger_lsp.print("{s}", .{msg}); + self.logger_lsp.print("{s}: {s}", .{ @tagName(operation), msg }); } pub fn register_capability(self: *Self, from: tp.pid_ref, cbor_id: []const u8, params_cb: []const u8) ClientError!void { diff --git a/src/config.zig b/src/config.zig index 1414403..bb12d9a 100644 --- a/src/config.zig +++ b/src/config.zig @@ -41,6 +41,8 @@ palette_style: WidgetStyle = .bars_top_bottom, panel_style: WidgetStyle = .compact, home_style: WidgetStyle = .bars_top_bottom, +lsp_output: enum { quiet, verbose } = .quiet, + include_files: []const u8 = "", pub const DigitStyle = enum { diff --git a/src/project_manager.zig b/src/project_manager.zig index fc73c9e..e288fe9 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -608,10 +608,11 @@ const Process = struct { return if (std.mem.eql(u8, method, "textDocument/publishDiagnostics")) project.publish_diagnostics(self.parent.ref(), params_cb) else if (std.mem.eql(u8, method, "window/showMessage")) - project.show_message(self.parent.ref(), params_cb) + project.show_message(params_cb) else if (std.mem.eql(u8, method, "window/logMessage")) - project.show_message(self.parent.ref(), params_cb) + project.log_message(params_cb) else { + if (!tp.env.get().is("lsp_verbose")) return; const params = try cbor.toJsonAlloc(self.allocator, params_cb); defer self.allocator.free(params); self.logger.print("LSP notification: {s} -> {s}", .{ method, params }); diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 75c7b06..810a723 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -122,6 +122,8 @@ fn init(allocator: Allocator) InitError!*Self { const frame_time = std.time.us_per_s / conf.frame_rate; const frame_clock = try tp.metronome.init(frame_time); + tp.env.get().set("lsp_verbose", conf.lsp_output == .verbose); + var self = try allocator.create(Self); // don't destroy // if tui fails it is catastrophic anyway and we don't want to cause nock-on errors From 21bd1e58a86793491f168ef2d78a373aa8e06d2d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 20 Aug 2025 21:15:38 +0200 Subject: [PATCH 86/87] refactor: output LSP client messages to log Regular message only if lsp_output "verbose". --- src/Project.zig | 7 +++++++ src/project_manager.zig | 15 +++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Project.zig b/src/Project.zig index 85cfd0f..ad71f8c 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -1562,6 +1562,13 @@ fn show_or_log_message(self: *Self, operation: enum { show, log }, params_cb: [] self.logger_lsp.print("{s}: {s}", .{ @tagName(operation), msg }); } +pub fn show_notification(self: *Self, method: []const u8, params_cb: []const u8) !void { + if (!tp.env.get().is("lsp_verbose")) return; + const params = try cbor.toJsonAlloc(self.allocator, params_cb); + defer self.allocator.free(params); + self.logger_lsp.print("LSP notification: {s} -> {s}", .{ method, params }); +} + pub fn register_capability(self: *Self, from: tp.pid_ref, cbor_id: []const u8, params_cb: []const u8) ClientError!void { _ = params_cb; return LSP.send_response(self.allocator, from, cbor_id, null) catch error.ClientFailed; diff --git a/src/project_manager.zig b/src/project_manager.zig index e288fe9..44e2f31 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -333,6 +333,8 @@ const Process = struct { var n: usize = 0; var task: []const u8 = undefined; var context: usize = undefined; + var tag: []const u8 = undefined; + var message: []const u8 = undefined; var eol_mode: Buffer.EolModeTag = @intFromEnum(Buffer.EolMode.lf); @@ -405,6 +407,11 @@ const Process = struct { self.hover(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed; } else if (try cbor.match(m.buf, .{ "get_mru_position", tp.extract(&project_directory), tp.extract(&path) })) { self.get_mru_position(from, project_directory, path) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed; + } else if (try cbor.match(m.buf, .{ "lsp", "msg", tp.extract(&tag), tp.extract(&message) })) { + if (tp.env.get().is("lsp_verbose")) + self.logger.print("{s}: {s}", .{ tag, message }); + } else if (try cbor.match(m.buf, .{ "lsp", "err", tp.extract(&tag), tp.extract(&message) })) { + self.logger.print("{s} error: {s}", .{ tag, message }); } else if (try cbor.match(m.buf, .{"shutdown"})) { self.persist_projects(); from.send(.{ "project_manager", "shutdown" }) catch return error.ClientFailed; @@ -611,12 +618,8 @@ const Process = struct { project.show_message(params_cb) else if (std.mem.eql(u8, method, "window/logMessage")) project.log_message(params_cb) - else { - if (!tp.env.get().is("lsp_verbose")) return; - const params = try cbor.toJsonAlloc(self.allocator, params_cb); - defer self.allocator.free(params); - self.logger.print("LSP notification: {s} -> {s}", .{ method, params }); - }; + else + project.show_notification(method, params_cb); } fn dispatch_request(self: *Process, from: tp.pid_ref, project_directory: []const u8, language_server: []const u8, method: []const u8, cbor_id: []const u8, params_cb: []const u8) (ProjectError || Project.ClientError || cbor.Error || cbor.JsonEncodeError || UnsupportedError)!void { From a227eb925c1f6b820e08a8e2fa97e7e598f0eb13 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 21 Aug 2025 10:45:21 +0200 Subject: [PATCH 87/87] refactor: unvendor flow-syntax --- build.zig.zon | 5 +- src/syntax/.gitignore | 1 - src/syntax/LICENSE | 21 - src/syntax/README.md | 2 - src/syntax/build.zig | 154 ------- src/syntax/build.zig.zon | 22 - src/syntax/src/QueryCache.zig | 195 --------- src/syntax/src/file_type.zig | 207 ---------- src/syntax/src/file_types.zig | 609 ---------------------------- src/syntax/src/syntax.zig | 213 ---------- src/syntax/src/treez_dummy.zig | 133 ------ src/syntax/src/ts_bin_query_gen.zig | 140 ------- src/syntax/src/ts_serializer.zig | 297 -------------- 13 files changed, 4 insertions(+), 1995 deletions(-) delete mode 100644 src/syntax/.gitignore delete mode 100644 src/syntax/LICENSE delete mode 100644 src/syntax/README.md delete mode 100644 src/syntax/build.zig delete mode 100644 src/syntax/build.zig.zon delete mode 100644 src/syntax/src/QueryCache.zig delete mode 100644 src/syntax/src/file_type.zig delete mode 100644 src/syntax/src/file_types.zig delete mode 100644 src/syntax/src/syntax.zig delete mode 100644 src/syntax/src/treez_dummy.zig delete mode 100644 src/syntax/src/ts_bin_query_gen.zig delete mode 100644 src/syntax/src/ts_serializer.zig diff --git a/build.zig.zon b/build.zig.zon index cb21721..75d94c0 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,7 +5,10 @@ .fingerprint = 0x52c0d670590aa80f, .dependencies = .{ - .syntax = .{ .path = "src/syntax" }, + .syntax = .{ + .url = "git+https://github.com/neurocyte/flow-syntax?ref=zig-0.14#410d19e633f237cd1602175450bd7d3bb03a1898", + .hash = "flow_syntax-0.1.0-X8jOoT4OAQDibKKzYlJls3u5KczVh__cWYN7vTqCE1o3", + }, .flags = .{ .url = "https://github.com/n0s4/flags/archive/372501d1576b5723829bcba98e41361132c7b618.tar.gz", .hash = "flags-0.8.0-AAAAAJV0AACuGBBnpUnHqZzAhoGTp4ibFROBQQQZGRqx", diff --git a/src/syntax/.gitignore b/src/syntax/.gitignore deleted file mode 100644 index 5211f11..0000000 --- a/src/syntax/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/.zig-cache/ diff --git a/src/syntax/LICENSE b/src/syntax/LICENSE deleted file mode 100644 index 0c64a22..0000000 --- a/src/syntax/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 CJ van den Berg - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/syntax/README.md b/src/syntax/README.md deleted file mode 100644 index f1cc61a..0000000 --- a/src/syntax/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# flow-syntax -Syntax highlighting module used by [flow](https://github.com/neurocyte/flow), [zat](https://github.com/neurocyte/zat) and [zine](https://github.com/kristoff-it/zine) diff --git a/src/syntax/build.zig b/src/syntax/build.zig deleted file mode 100644 index 2b3a149..0000000 --- a/src/syntax/build.zig +++ /dev/null @@ -1,154 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const use_tree_sitter = b.option(bool, "use_tree_sitter", "Enable tree-sitter (default: yes)") orelse true; - const options = b.addOptions(); - options.addOption(bool, "use_tree_sitter", use_tree_sitter); - const options_mod = options.createModule(); - - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const tree_sitter_dep = b.dependency("tree_sitter", .{ - .target = target, - .optimize = optimize, - }); - - const tree_sitter_host_dep = b.dependency("tree_sitter", .{ - .target = b.graph.host, - .optimize = optimize, - }); - - const cbor_dep = b.dependency("cbor", .{ - .target = target, - .optimize = optimize, - }); - - const ts_bin_query_gen = b.addExecutable(.{ - .name = "ts_bin_query_gen", - .target = b.graph.host, - .root_source_file = b.path("src/ts_bin_query_gen.zig"), - }); - ts_bin_query_gen.linkLibC(); - ts_bin_query_gen.root_module.addImport("cbor", cbor_dep.module("cbor")); - ts_bin_query_gen.root_module.addImport("treez", tree_sitter_host_dep.module("treez")); - ts_bin_query_gen.root_module.addImport("build_options", options_mod); - - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "queries/cmake/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-agda/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-astro/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-bash/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-c-sharp/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-c/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-cpp/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-css/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-diff/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-dockerfile/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-elixir/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-git-rebase/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-gitcommit/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-gleam/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-go/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-fish/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-haskell/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-hare/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-html/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-hurl/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-java/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-javascript/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-jsdoc/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-json/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-julia/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-kdl/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-lua/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-mail/queries/mail/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-make/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-markdown/tree-sitter-markdown/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-markdown/tree-sitter-markdown-inline/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-nasm/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-nim/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-ninja/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-nix/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-nu/queries/nu/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-ocaml/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-odin/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-openscad/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-org/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-php/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-powershell/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-proto/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-python/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-purescript/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-regex/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-rpmspec/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-ruby/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-rust/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-ssh-config/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-scala/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-scheme/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-superhtml/tree-sitter-superhtml/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-sql/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-swift/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-toml/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-typescript/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-typst/queries/typst/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-uxntal/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-vim/queries/vim/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-xml/queries/dtd/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-xml/queries/xml/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-yaml/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-zig/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-ziggy/tree-sitter-ziggy/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-ziggy/tree-sitter-ziggy-schema/queries/highlights.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "nvim-treesitter/queries/verilog/highlights.scm"); - - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "queries/cmake/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-astro/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-cpp/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-elixir/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-gitcommit/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-hare/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-html/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-hurl/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-javascript/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-kdl/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-lua/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-markdown/tree-sitter-markdown-inline/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-markdown/tree-sitter-markdown/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-nasm/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-nix/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-nu/queries/nu/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-odin/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-openscad/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-php/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-purescript/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-purescript/vim_queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-rust/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-superhtml/tree-sitter-superhtml/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-swift/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-typst/queries/typst/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-uxntal/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-vim/queries/vim/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "tree-sitter-zig/queries/injections.scm"); - ts_queryfile(b, tree_sitter_dep, ts_bin_query_gen, "nvim-treesitter/queries/verilog/injections.scm"); - - const syntax_mod = b.addModule("syntax", .{ - .root_source_file = b.path("src/syntax.zig"), - .imports = &.{ - .{ .name = "build_options", .module = options_mod }, - .{ .name = "cbor", .module = cbor_dep.module("cbor") }, - .{ .name = "treez", .module = tree_sitter_dep.module("treez") }, - }, - }); - - if (use_tree_sitter) { - const ts_bin_query_gen_step = b.addRunArtifact(ts_bin_query_gen); - const output = ts_bin_query_gen_step.addOutputFileArg("bin_queries.cbor"); - syntax_mod.addAnonymousImport("syntax_bin_queries", .{ .root_source_file = output }); - } -} - -fn ts_queryfile(b: *std.Build, dep: *std.Build.Dependency, bin_gen: *std.Build.Step.Compile, comptime sub_path: []const u8) void { - const module = b.createModule(.{ .root_source_file = dep.path(sub_path) }); - bin_gen.root_module.addImport(sub_path, module); -} diff --git a/src/syntax/build.zig.zon b/src/syntax/build.zig.zon deleted file mode 100644 index 70ca5d8..0000000 --- a/src/syntax/build.zig.zon +++ /dev/null @@ -1,22 +0,0 @@ -.{ - .name = .flow_syntax, - .version = "0.1.0", - .fingerprint = 0x3ba2584ea1cec85f, - .minimum_zig_version = "0.14.1", - - .dependencies = .{ - .tree_sitter = .{ - .url = "https://github.com/neurocyte/tree-sitter/releases/download/master-1c3ad59bd98ee430b166054030dac4c46d641e39/source.tar.gz", - .hash = "N-V-__8AANMzUiemOR2eNnrtlMmAGHFqij6VYtDUiaFfn6Dw", - }, - .cbor = .{ - .url = "https://github.com/neurocyte/cbor/archive/1fccb83c70cd84e1dff57cc53f7db8fb99909a94.tar.gz", - .hash = "cbor-1.0.0-RcQE_HvqAACcrLH7t3IDZOshgY2xqJA_UX330MvwSepb", - }, - }, - .paths = .{ - "src", - "build.zig", - "build.zig.zon", - }, -} diff --git a/src/syntax/src/QueryCache.zig b/src/syntax/src/QueryCache.zig deleted file mode 100644 index 7b011d5..0000000 --- a/src/syntax/src/QueryCache.zig +++ /dev/null @@ -1,195 +0,0 @@ -const std = @import("std"); -const build_options = @import("build_options"); - -const treez = if (build_options.use_tree_sitter) - @import("treez") -else - @import("treez_dummy.zig"); - -const Self = @This(); - -pub const tss = @import("ts_serializer.zig"); -pub const FileType = @import("file_type.zig"); -const Query = treez.Query; - -allocator: std.mem.Allocator, -mutex: ?std.Thread.Mutex, -highlights: std.StringHashMapUnmanaged(*CacheEntry) = .{}, -injections: std.StringHashMapUnmanaged(*CacheEntry) = .{}, -errors: std.StringHashMapUnmanaged(*CacheEntry) = .{}, -ref_count: usize = 1, - -const CacheEntry = struct { - mutex: ?std.Thread.Mutex, - query: ?*Query, - query_arena: ?*std.heap.ArenaAllocator, - query_type: QueryType, - file_type_name: []const u8, - lang_fn: FileType.LangFn, - - fn destroy(self: *@This(), allocator: std.mem.Allocator) void { - if (self.query_arena) |a| { - a.deinit(); - allocator.destroy(a); - } else if (self.query) |q| - q.destroy(); - self.query_arena = null; - self.query = null; - } -}; - -pub const QueryType = enum { - highlights, - errors, - injections, -}; - -const QueryParseError = error{ - InvalidSyntax, - InvalidNodeType, - InvalidField, - InvalidCapture, - InvalidStructure, - InvalidLanguage, -}; - -const CacheError = error{ - NotFound, - OutOfMemory, -}; - -pub const Error = CacheError || QueryParseError || QuerySerializeError; - -pub fn create(allocator: std.mem.Allocator, opts: struct { lock: bool = false }) !*Self { - const self = try allocator.create(Self); - errdefer allocator.destroy(self); - self.* = .{ - .allocator = allocator, - .mutex = if (opts.lock) .{} else null, - }; - return self; -} - -pub fn deinit(self: *Self) void { - self.release_ref_unlocked_and_maybe_destroy(); -} - -fn add_ref_locked(self: *Self) void { - std.debug.assert(self.ref_count > 0); - self.ref_count += 1; -} - -fn release_ref_unlocked_and_maybe_destroy(self: *Self) void { - { - if (self.mutex) |*mtx| mtx.lock(); - defer if (self.mutex) |*mtx| mtx.unlock(); - self.ref_count -= 1; - if (self.ref_count > 0) return; - } - - release_cache_entry_hash_map(self.allocator, &self.highlights); - release_cache_entry_hash_map(self.allocator, &self.errors); - release_cache_entry_hash_map(self.allocator, &self.injections); - self.allocator.destroy(self); -} - -fn release_cache_entry_hash_map(allocator: std.mem.Allocator, hash_map: *std.StringHashMapUnmanaged(*CacheEntry)) void { - var iter = hash_map.iterator(); - while (iter.next()) |p| { - allocator.free(p.key_ptr.*); - p.value_ptr.*.destroy(allocator); - allocator.destroy(p.value_ptr.*); - } - hash_map.deinit(allocator); -} - -fn get_cache_entry(self: *Self, file_type: FileType, comptime query_type: QueryType) CacheError!*CacheEntry { - if (self.mutex) |*mtx| mtx.lock(); - defer if (self.mutex) |*mtx| mtx.unlock(); - - const hash = switch (query_type) { - .highlights => &self.highlights, - .errors => &self.errors, - .injections => &self.injections, - }; - - return if (hash.get(file_type.name)) |entry| entry else blk: { - const entry_ = try hash.getOrPut(self.allocator, try self.allocator.dupe(u8, file_type.name)); - - const q = try self.allocator.create(CacheEntry); - q.* = .{ - .query = null, - .query_arena = null, - .mutex = if (self.mutex) |_| .{} else null, - .lang_fn = file_type.lang_fn, - .file_type_name = file_type.name, - .query_type = query_type, - }; - entry_.value_ptr.* = q; - - break :blk q; - }; -} - -fn get_cached_query(self: *Self, entry: *CacheEntry) Error!?*Query { - if (entry.mutex) |*mtx| mtx.lock(); - defer if (entry.mutex) |*mtx| mtx.unlock(); - - return if (entry.query) |query| query else blk: { - const lang = entry.lang_fn() orelse std.debug.panic("tree-sitter parser function failed for language: {s}", .{entry.file_type_name}); - const queries = FileType.queries.get(entry.file_type_name) orelse return null; - const query_bin = switch (entry.query_type) { - .highlights => queries.highlights_bin, - .errors => queries.errors_bin, - .injections => queries.injections_bin orelse return null, - }; - const query, const arena = try deserialize_query(query_bin, lang, self.allocator); - entry.query = query; - entry.query_arena = arena; - break :blk entry.query.?; - }; -} - -fn pre_load_internal(self: *Self, file_type: *const FileType, comptime query_type: QueryType) Error!void { - _ = try self.get_cached_query(try self.get_cache_entry(file_type, query_type)); -} - -pub fn pre_load(self: *Self, lang_name: []const u8) Error!void { - const file_type = FileType.get_by_name(lang_name) orelse return; - _ = try self.pre_load_internal(file_type, .highlights); - _ = try self.pre_load_internal(file_type, .errors); - _ = try self.pre_load_internal(file_type, .injections); -} - -fn ReturnType(comptime query_type: QueryType) type { - return switch (query_type) { - .highlights => *Query, - .errors => *Query, - .injections => ?*Query, - }; -} - -pub fn get(self: *Self, file_type: FileType, comptime query_type: QueryType) Error!ReturnType(query_type) { - const query = try self.get_cached_query(try self.get_cache_entry(file_type, query_type)); - self.add_ref_locked(); - return switch (@typeInfo(ReturnType(query_type))) { - .optional => |_| query, - else => query.?, - }; -} - -pub fn release(self: *Self, query: *Query, comptime query_type: QueryType) void { - _ = query; - _ = query_type; - self.release_ref_unlocked_and_maybe_destroy(); -} - -pub const QuerySerializeError = (tss.SerializeError || tss.DeserializeError); - -fn deserialize_query(query_bin: []const u8, language: ?*const treez.Language, allocator: std.mem.Allocator) QuerySerializeError!struct { *Query, *std.heap.ArenaAllocator } { - var ts_query_out, const arena = try tss.fromCbor(query_bin, allocator); - ts_query_out.language = @intFromPtr(language); - - const query_out: *Query = @alignCast(@ptrCast(ts_query_out)); - return .{ query_out, arena }; -} diff --git a/src/syntax/src/file_type.zig b/src/syntax/src/file_type.zig deleted file mode 100644 index 88a1494..0000000 --- a/src/syntax/src/file_type.zig +++ /dev/null @@ -1,207 +0,0 @@ -const std = @import("std"); -const cbor = @import("cbor"); -const build_options = @import("build_options"); - -const treez = if (build_options.use_tree_sitter) - @import("treez") -else - @import("treez_dummy.zig"); - -pub const FileType = @This(); - -color: u24, -icon: []const u8, -name: []const u8, -description: []const u8, -lang_fn: LangFn, -extensions: []const []const u8, -first_line_matches: ?FirstLineMatch = null, -comment: []const u8, -formatter: ?[]const []const u8, -language_server: ?[]const []const u8, - -pub fn get_by_name_static(name: []const u8) ?FileType { - return FileType.static_file_types.get(name); -} - -pub fn get_all() []const FileType { - return FileType.static_file_types.values(); -} - -pub fn guess_static(file_path: ?[]const u8, content: []const u8) ?FileType { - if (guess_first_line_static(content)) |ft| return ft; - for (static_file_types.values()) |file_type| - if (file_path) |fp| if (match_file_type(file_type.extensions, fp)) - return file_type; - return null; -} - -fn guess_first_line_static(content: []const u8) ?FileType { - const first_line = if (std.mem.indexOf(u8, content, "\n")) |pos| content[0..pos] else content; - for (static_file_types.values()) |file_type| - if (file_type.first_line_matches) |match| - if (match_first_line(match.prefix, match.content, first_line)) - return file_type; - return null; -} - -pub fn match_first_line(match_prefix: ?[]const u8, match_content: ?[]const u8, first_line: []const u8) bool { - if (match_prefix == null and match_content == null) return false; - if (match_prefix) |prefix| - if (prefix.len > first_line.len or !std.mem.eql(u8, first_line[0..prefix.len], prefix)) - return false; - if (match_content) |content| - if (std.mem.indexOf(u8, first_line, content)) |_| {} else return false; - return true; -} - -pub fn match_file_type(extensions: []const []const u8, file_path: []const u8) bool { - const basename = std.fs.path.basename(file_path); - const extension = std.fs.path.extension(file_path); - return for (extensions) |ext| { - if (ext.len == basename.len and std.mem.eql(u8, ext, basename)) - return true; - if (extension.len > 0 and ext.len == extension.len - 1 and std.mem.eql(u8, ext, extension[1..])) - return true; - } else false; -} - -pub fn Parser(comptime lang: []const u8) LangFn { - return get_parser(lang); -} - -fn get_parser(comptime lang: []const u8) LangFn { - if (build_options.use_tree_sitter) { - const language_name = ft_func_name(lang); - return @extern(?LangFn, .{ .name = "tree_sitter_" ++ language_name }) orelse @compileError(std.fmt.comptimePrint("Cannot find extern tree_sitter_{s}", .{language_name})); - } else { - return treez.Language.LangFn; - } -} - -fn ft_func_name(comptime lang: []const u8) []const u8 { - var transform: [lang.len]u8 = undefined; - for (lang, 0..) |c, i| - transform[i] = if (c == '-') '_' else c; - const func_name = transform; - return &func_name; -} - -pub const LangFn = *const fn () callconv(.C) ?*const treez.Language; - -pub const FirstLineMatch = struct { - prefix: ?[]const u8 = null, - content: ?[]const u8 = null, -}; - -const static_file_type_list = load_file_types(@import("file_types.zig")); -const static_file_types = std.StaticStringMap(FileType).initComptime(static_file_type_list); - -fn vec(comptime args: anytype) []const []const u8 { - var cmd: []const []const u8 = &[_][]const u8{}; - inline for (args) |arg| { - cmd = cmd ++ [_][]const u8{arg}; - } - return cmd; -} - -const ListEntry = struct { []const u8, FileType }; - -fn load_file_types(comptime Namespace: type) []const ListEntry { - comptime switch (@typeInfo(Namespace)) { - .@"struct" => |info| { - var count = 0; - for (info.decls) |_| { - // @compileLog(decl.name, @TypeOf(@field(Namespace, decl.name))); - count += 1; - } - var construct_types: [count]ListEntry = undefined; - var i = 0; - for (info.decls) |decl| { - const lang = decl.name; - const args = @field(Namespace, lang); - construct_types[i] = .{ lang, .{ - .color = if (@hasField(@TypeOf(args), "color")) args.color else 0xffffff, - .icon = if (@hasField(@TypeOf(args), "icon")) args.icon else "󱀫", - .name = lang, - .description = args.description, - .lang_fn = if (@hasField(@TypeOf(args), "parser")) args.parser else get_parser(lang), - .extensions = vec(args.extensions), - .comment = args.comment, - .first_line_matches = if (@hasField(@TypeOf(args), "first_line_matches")) args.first_line_matches else null, - .formatter = if (@hasField(@TypeOf(args), "formatter")) vec(args.formatter) else null, - .language_server = if (@hasField(@TypeOf(args), "language_server")) vec(args.language_server) else null, - } }; - i += 1; - } - const types = construct_types; - return &types; - }, - else => @compileError("expected tuple or struct type"), - }; -} - -pub const FileTypeQueries = struct { - highlights_bin: []const u8, - errors_bin: []const u8, - injections_bin: ?[]const u8, -}; - -pub const queries = std.StaticStringMap(FileTypeQueries).initComptime(load_queries()); - -fn load_queries() []const struct { []const u8, FileTypeQueries } { - if (!build_options.use_tree_sitter) return &.{}; - @setEvalBranchQuota(32000); - const queries_cb = @embedFile("syntax_bin_queries"); - var iter: []const u8 = queries_cb; - var len = cbor.decodeMapHeader(&iter) catch |e| { - @compileLog("cbor.decodeMapHeader", e); - @compileError("invalid syntax_bin_queries"); - }; - var construct_types: [len]struct { []const u8, FileTypeQueries } = undefined; - var i = 0; - while (len > 0) : (len -= 1) { - var lang: []const u8 = undefined; - if (!try cbor.matchString(&iter, &lang)) - @compileError("invalid language name field"); - construct_types[i] = .{ lang, .{ - .highlights_bin = blk: { - var iter_: []const u8 = iter; - break :blk get_query_value_bin(&iter_, "highlights") orelse @compileError("missing highlights for " ++ lang); - }, - .errors_bin = blk: { - var iter_: []const u8 = iter; - break :blk get_query_value_bin(&iter_, "errors") orelse @compileError("missing errors query for " ++ lang); - }, - .injections_bin = blk: { - var iter_: []const u8 = iter; - break :blk get_query_value_bin(&iter_, "injections"); - }, - } }; - try cbor.skipValue(&iter); - i += 1; - } - const types = construct_types; - return &types; -} - -fn get_query_value_bin(iter: *[]const u8, comptime query: []const u8) ?[]const u8 { - var len = cbor.decodeMapHeader(iter) catch |e| { - @compileLog("cbor.decodeMapHeader", e); - @compileError("invalid query map in syntax_bin_queries"); - }; - while (len > 0) : (len -= 1) { - var query_name: []const u8 = undefined; - if (!try cbor.matchString(iter, &query_name)) - @compileError("invalid query name field"); - if (std.mem.eql(u8, query_name, query)) { - var query_value: []const u8 = undefined; - if (try cbor.matchValue(iter, cbor.extract(&query_value))) - return query_value; - @compileError("invalid query value field"); - } else { - try cbor.skipValue(iter); - } - } - return null; -} diff --git a/src/syntax/src/file_types.zig b/src/syntax/src/file_types.zig deleted file mode 100644 index e75ad4a..0000000 --- a/src/syntax/src/file_types.zig +++ /dev/null @@ -1,609 +0,0 @@ -const file_type = @import("file_type.zig"); -const FirstLineMatch = file_type.FirstLineMatch; - -pub const agda = .{ - .description = "Agda", - .extensions = .{"agda"}, - .comment = "--", -}; - -pub const astro = .{ - .description = "Astro", - .icon = "", - .extensions = .{"astro"}, - .comment = "//", - .language_server = .{ "astro-ls", "--stdio" }, -}; - -pub const bash = .{ - .description = "Bash", - .color = 0x3e474a, - .icon = "󱆃", - .extensions = .{ "sh", "bash", ".profile" }, - .comment = "#", - .first_line_matches = FirstLineMatch{ .prefix = "#!", .content = "sh" }, - .formatter = .{ "shfmt", "--indent", "4" }, - .language_server = .{ "bash-language-server", "start" }, -}; - -pub const c = .{ - .description = "C", - .icon = "", - .extensions = .{"c"}, - .comment = "//", - .formatter = .{"clang-format"}, - .language_server = .{"clangd"}, -}; - -pub const @"c-sharp" = .{ - .description = "C#", - .color = 0x68217a, - .icon = "󰌛", - .extensions = .{"cs"}, - .comment = "//", - .language_server = .{ "OmniSharp", "-lsp" }, - .formatter = .{ "csharpier", "format" }, -}; - -pub const conf = .{ - .description = "Config", - .color = 0x000000, - .icon = "", - .extensions = .{ "conf", "log", "config", ".gitconfig", "gui_config" }, - .highlights = fish.highlights, - .comment = "#", - .parser = fish.parser, -}; - -pub const cmake = .{ - .description = "CMake", - .color = 0x004078, - .icon = "", - .extensions = .{ "CMakeLists.txt", "cmake", "cmake.in" }, - .comment = "#", - .highlights = "queries/cmake/highlights.scm", - .injections = "queries/cmake/injections.scm", - .formatter = .{"cmake-format"}, - .language_server = .{"cmake-language-server"}, -}; - -pub const cpp = .{ - .description = "C++", - .color = 0x9c033a, - .icon = "", - .extensions = .{ "cc", "cpp", "cxx", "hpp", "hxx", "h", "ipp", "ixx" }, - .comment = "//", - .highlights_list = .{ - "tree-sitter-c/queries/highlights.scm", - "tree-sitter-cpp/queries/highlights.scm", - }, - .injections = "tree-sitter-cpp/queries/injections.scm", - .formatter = .{"clang-format"}, - .language_server = .{"clangd"}, -}; - -pub const css = .{ - .description = "CSS", - .color = 0x3d8fc6, - .icon = "󰌜", - .extensions = .{"css"}, - .comment = "//", - .language_server = .{ "vscode-css-language-server", "--stdio" }, -}; - -pub const diff = .{ - .description = "Diff", - .extensions = .{ "diff", "patch", "rej" }, - .comment = "#", -}; - -pub const dockerfile = .{ - .description = "Docker", - .color = 0x019bc6, - .icon = "", - .extensions = .{ "Dockerfile", "dockerfile", "docker", "Containerfile", "container" }, - .comment = "#", -}; - -pub const dtd = .{ - .description = "DTD", - .icon = "󰗀", - .extensions = .{"dtd"}, - .comment = "