From 561124e667d362083631ef7fd402fb07eb79dba6 Mon Sep 17 00:00:00 2001 From: JailBird Date: Tue, 24 Mar 2026 19:06:38 -0700 Subject: [PATCH 01/12] Add formatter for YAML and LSP for YAML and RPM .spec --- src/file_type_lsp.zig | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/file_type_lsp.zig b/src/file_type_lsp.zig index d7bf03d3..83f0bf3b 100644 --- a/src/file_type_lsp.zig +++ b/src/file_type_lsp.zig @@ -183,7 +183,9 @@ pub const python = .{ pub const regex = .{}; -pub const rpmspec = .{}; +pub const rpmspec = .{ + .language_server = .{ "python3", "-mrpm_spec_language_server", "--stdio" }, +}; pub const rst = .{ .language_server = .{"esbonio"}, @@ -233,7 +235,10 @@ pub const xml = .{ .formatter = .{ "xmllint", "--format", "-" }, }; -pub const yaml = .{}; +pub const yaml = .{ + .language_server = .{ "yaml-language-server", "--stdio" }, + .formatter = .{ "prettier", "--parser", "yaml" }, +}; pub const zig = .{ .language_server = .{"zls"}, From 84a45810a6ccdc9dd27c3dcec2fab797a35dd9a8 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 25 Mar 2026 12:28:18 +0100 Subject: [PATCH 02/12] fix(lsp): fallback to sorting completions by label for LSPs that don't return unique sortText --- src/tui/mode/overlay/completion_dropdown.zig | 5 +++-- src/tui/mode/overlay/completion_palette.zig | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/tui/mode/overlay/completion_dropdown.zig b/src/tui/mode/overlay/completion_dropdown.zig index ceb7a340..a9a20786 100644 --- a/src/tui/mode/overlay/completion_dropdown.zig +++ b/src/tui/mode/overlay/completion_dropdown.zig @@ -87,8 +87,9 @@ pub fn load_entries(self: *Type) !usize { 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; + const sort_text_equal = std.mem.eql(u8, lhs.sort_text, rhs.sort_text); + const lhs_str = if (!sort_text_equal and lhs.sort_text.len > 0) lhs.sort_text else lhs.label; + const rhs_str = if (!sort_text_equal and rhs.sort_text.len > 0) rhs.sort_text else rhs.label; return std.mem.order(u8, lhs_str, rhs_str) == .lt; } }.less_fn; diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index ae8f9d26..c6e6723a 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -69,8 +69,9 @@ pub fn load_entries(palette: *Type) !usize { 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; + const sort_text_equal = std.mem.eql(u8, lhs.sort_text, rhs.sort_text); + const lhs_str = if (!sort_text_equal and lhs.sort_text.len > 0) lhs.sort_text else lhs.label; + const rhs_str = if (!sort_text_equal and rhs.sort_text.len > 0) rhs.sort_text else rhs.label; return std.mem.order(u8, lhs_str, rhs_str) == .lt; } }.less_fn; From 7f07f544c95e8ed97b295d488b786c6149175aac Mon Sep 17 00:00:00 2001 From: Volodia Kraplich Date: Tue, 24 Mar 2026 18:06:02 +0200 Subject: [PATCH 03/12] feat(lsp): support toml --- src/file_type_lsp.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/file_type_lsp.zig b/src/file_type_lsp.zig index 83f0bf3b..5b216187 100644 --- a/src/file_type_lsp.zig +++ b/src/file_type_lsp.zig @@ -216,7 +216,10 @@ pub const verilog = .{ .formatter = .{ "verible-verilog-format", "-" }, }; -pub const toml = .{}; +pub const toml = .{ + .language_server = .{ "tombi", "lsp" }, + .formatter = .{ "tombi", "format" }, +}; pub const typescript = .{ .language_server = .{ "typescript-language-server", "--stdio" }, From e930effa0ca98b679cd84259626dd92a74c77095 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 26 Mar 2026 09:38:28 +0100 Subject: [PATCH 04/12] feat: add {{reflow_width}} expansion --- src/tui/expansion.zig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tui/expansion.zig b/src/tui/expansion.zig index c98ab393..427a78f1 100644 --- a/src/tui/expansion.zig +++ b/src/tui/expansion.zig @@ -9,6 +9,7 @@ /// {{selections*}} - All current selections expanded to multiple quoted arguments /// {{indent_mode}} - The current indent mode ("tabs" or "spaces") /// {{indent_size}} - The current indent size (in columns) +/// {{reflow_width}} - The current reflow width (in columns) /// {{blame_commit}} - The blame commit ID at the line number of the primary cursor pub fn expand(allocator: Allocator, arg: []const u8) Error![]const u8 { var result: std.Io.Writer.Allocating = .init(allocator); @@ -162,6 +163,15 @@ const functions = struct { return stream.toOwnedSlice(); } + /// {{reflow_width}} - The current reflow width (in columns) + pub fn reflow_width(allocator: Allocator) Error![]const u8 { + const mv = tui.mainview() orelse return &.{}; + const ed = mv.get_active_editor() orelse return &.{}; + var stream: std.Io.Writer.Allocating = .init(allocator); + try stream.writer.print("{d}", .{ed.reflow_width orelse tui.config().reflow_width}); + return stream.toOwnedSlice(); + } + /// {{blame_commit}} - The blame commit ID at the line number of the primary cursor pub fn blame_commit(allocator: Allocator) Error![]const u8 { const mv = tui.mainview() orelse return &.{}; From f61c716c27b1327c3846aa688673e32263d897c2 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 26 Mar 2026 09:39:06 +0100 Subject: [PATCH 05/12] feat: configure default markdown formatter to reflow at reflow_width --- src/file_type_lsp.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/file_type_lsp.zig b/src/file_type_lsp.zig index 5b216187..9064fc3c 100644 --- a/src/file_type_lsp.zig +++ b/src/file_type_lsp.zig @@ -116,7 +116,7 @@ pub const make = .{}; pub const markdown = .{ .language_server = .{ "marksman", "server" }, - .formatter = .{ "prettier", "--parser", "markdown" }, + .formatter = .{ "prettier", "--parser", "markdown", "--prose-wrap", "always", "--print-width", "{{reflow_width}}" }, }; pub const @"markdown-inline" = .{}; From a782bfb690aec321fc3c6a955953895a150333b1 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 28 Mar 2026 14:48:07 +0100 Subject: [PATCH 06/12] fix: don't filter super, hyper and meta modifiers for no reason This filter was introduced to ignore caps lock, scroll lock and num lock modifier bits. super, hyper and meta might actually be useful though. --- src/renderer/vaxis/renderer.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer/vaxis/renderer.zig b/src/renderer/vaxis/renderer.zig index 3ed5f280..384d0d7f 100644 --- a/src/renderer/vaxis/renderer.zig +++ b/src/renderer/vaxis/renderer.zig @@ -627,6 +627,9 @@ fn filter_mods(key_: vaxis.Key) vaxis.Key { .shift = key_.mods.shift, .alt = key_.mods.alt, .ctrl = key_.mods.ctrl, + .super = key_.mods.super, + .hyper = key_.mods.hyper, + .meta = key_.mods.meta, }; return key__; } From a8437d61391479c1f56ae2ced5238d6f6973788b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 31 Mar 2026 09:52:31 +0200 Subject: [PATCH 07/12] feat(themes): add Kanso theme (zen, ink, mist, pearl variants) --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 095e111d..48e0c7d0 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -22,8 +22,8 @@ .hash = "thespian-0.0.1-owFOjlgiBgC8w4XqkCOegxz5vMy6kNErcssWQWf2QHeE", }, .themes = .{ - .url = "https://github.com/neurocyte/flow-themes/releases/download/master-c6c7f18cfb2e3945cd0b71dab24271465074dbc3/flow-themes.tar.gz", - .hash = "N-V-__8AAOKzJACguNxU76WX9M7RIhOYGuLnlasJ1-GDdhqT", + .url = "https://github.com/neurocyte/flow-themes/releases/download/master-750400d02ea8cacaabc869cd4d34dcebf04a53c8/flow-themes.tar.gz", + .hash = "N-V-__8AAEWxJQAyUV_rvRIWHB8EhIBxpQXqCB68SpilIjEt", }, .fuzzig = .{ .url = "https://github.com/fjebaker/fuzzig/archive/4251fe4230d38e721514394a485db62ee1667ff3.tar.gz", From ba840b72e0d48e4ea5dc6ab24ffa5fd3b336cf38 Mon Sep 17 00:00:00 2001 From: Paul Graydon Date: Sat, 14 Feb 2026 23:09:57 +0100 Subject: [PATCH 08/12] feat: [vim] Add word textobject actions --- src/keybind/builtin/vim.json | 12 ++++ src/tui/editor.zig | 12 +++- src/tui/mode/vim.zig | 128 ++++++++++++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/src/keybind/builtin/vim.json b/src/keybind/builtin/vim.json index a844be61..587f148f 100644 --- a/src/keybind/builtin/vim.json +++ b/src/keybind/builtin/vim.json @@ -81,14 +81,23 @@ ["dgg", "cut_buffer_begin"], ["\"_dd", "delete_line"], + ["diw", "cut_inside_word"], + ["daw", "cut_around_word"], + ["cc", ["enter_mode", "insert"], ["cut_internal_vim"]], ["C", ["enter_mode", "insert"], ["cut_to_end_vim"]], ["D", "cut_to_end_vim"], ["cw", ["enter_mode", "insert"], ["cut_word_right_vim"]], ["cb", ["enter_mode", "insert"], ["cut_word_left_vim"]], + ["ciw", ["enter_mode", "insert"], ["cut_inside_word"]], + ["caw", ["enter_mode", "insert"], ["cut_around_word"]], + ["yy", ["copy_line_internal_vim"], ["cancel"]], + ["yiw", ["copy_inside_word"], ["cancel"]], + ["yaw", ["copy_around_word"], ["cancel"]], + ["", "move_scroll_half_page_up_vim"], ["", "move_scroll_half_page_down_vim"], @@ -159,6 +168,9 @@ ["B", "select_word_left"], ["e", "select_word_right_end_vim"], + ["iw", "select_inside_word"], + ["aw", "select_around_word"], + ["^", "smart_move_begin"], ["$", "select_end"], [":", "open_command_palette"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index bdac85ac..e65dd6d5 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -2628,7 +2628,15 @@ pub const Editor = struct { return cursor.test_at(root, is_whitespace, metrics); } - fn is_non_whitespace_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + pub fn is_whitespace_or_eol_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + return cursor.test_at(root, is_whitespace_or_eol, metrics); + } + + pub fn is_non_whitespace_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + return !cursor.test_at(root, is_whitespace, metrics); + } + + pub fn is_non_whitespace_or_eol_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { return !cursor.test_at(root, is_whitespace_or_eol, metrics); } @@ -3745,7 +3753,7 @@ pub const Editor = struct { } pub fn move_cursor_right_until_non_whitespace(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { - move_cursor_right_until(root, cursor, is_non_whitespace_at_cursor, metrics); + move_cursor_right_until(root, cursor, is_non_whitespace_or_eol_at_cursor, metrics); } pub fn move_word_left(self: *Self, ctx: Context) Result { diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index 38eacbee..f7067e50 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -2,6 +2,13 @@ const std = @import("std"); const command = @import("command"); const cmd = command.executeName; +const tui = @import("../tui.zig"); + +const Buffer = @import("Buffer"); +const Cursor = Buffer.Cursor; +const CurSel = @import("../editor.zig").CurSel; +const Editor = @import("../editor.zig").Editor; + var commands: Commands = undefined; pub fn init() !void { @@ -138,6 +145,125 @@ const cmds_ = struct { //TODO return undefined; } - pub const copy_line_meta: Meta = .{ .description = "Copies the current line" }; + + pub fn select_inside_word(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics); + } + pub const select_inside_word_meta: Meta = .{ .description = "Select inside word" }; + + pub fn select_around_word(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics); + } + pub const select_around_word_meta: Meta = .{ .description = "Select around word" }; + + pub fn cut_inside_word(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_word_meta: Meta = .{ .description = "Cut inside word" }; + + pub fn cut_around_word(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_word_meta: Meta = .{ .description = "Cut around word" }; + + pub fn copy_inside_word(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_word_meta: Meta = .{ .description = "Copy inside word" }; + + pub fn copy_around_word(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_word_meta: Meta = .{ .description = "Copy around word" }; }; + +fn is_tab_or_space(c: []const u8) bool { + return (c[0] == ' ') or (c[0] == '\t'); +} + +fn is_tab_or_space_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + return cursor.test_at(root, is_tab_or_space, metrics); +} +fn is_not_tab_or_space_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + return !cursor.test_at(root, is_tab_or_space, metrics); +} + +fn select_inside_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_word_textobject(root, cursel, metrics, .inside); +} + +fn select_around_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_word_textobject(root, cursel, metrics, .around); +} + +fn select_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, scope: enum { inside, around }) !void { + var prev = cursel.cursor; + var next = cursel.cursor; + + if (cursel.cursor.test_at(root, Editor.is_non_word_char, metrics)) { + if (cursel.cursor.test_at(root, Editor.is_whitespace_or_eol, metrics)) { + Editor.move_cursor_left_until(root, &prev, Editor.is_non_whitespace_at_cursor, metrics); + Editor.move_cursor_right_until(root, &next, Editor.is_non_whitespace_at_cursor, metrics); + } else { + Editor.move_cursor_left_until(root, &prev, Editor.is_whitespace_or_eol_at_cursor, metrics); + Editor.move_cursor_right_until(root, &next, Editor.is_whitespace_or_eol_at_cursor, metrics); + } + prev.move_right(root, metrics) catch {}; + } else { + Editor.move_cursor_left_until(root, &prev, Editor.is_word_boundary_left_vim, metrics); + Editor.move_cursor_right_until(root, &next, Editor.is_word_boundary_right_vim, metrics); + next.move_right(root, metrics) catch {}; + } + + if (scope == .around) { + const inside_prev = prev; + const inside_next = next; + + if (next.test_at(root, is_tab_or_space, metrics)) { + Editor.move_cursor_right_until(root, &next, is_not_tab_or_space_at_cursor, metrics); + } else { + next = inside_next; + prev.move_left(root, metrics) catch {}; + if (prev.test_at(root, is_tab_or_space, metrics)) { + Editor.move_cursor_left_until(root, &prev, is_not_tab_or_space_at_cursor, metrics); + prev.move_right(root, metrics) catch {}; + } else { + prev = inside_prev; + } + } + } + + const sel = cursel.enable_selection(root, metrics); + sel.begin = prev; + sel.end = next; + cursel.*.cursor = next; +} From ce7cc48a722fc5410ae653dab31b3a56216f9113 Mon Sep 17 00:00:00 2001 From: Paul Graydon Date: Sun, 8 Mar 2026 16:23:21 +0100 Subject: [PATCH 09/12] feat: [vim] Add bracket textobject actions --- src/keybind/builtin/vim.json | 52 +++++++ src/tui/editor.zig | 2 +- src/tui/mode/vim.zig | 274 +++++++++++++++++++++++++++++++++++ 3 files changed, 327 insertions(+), 1 deletion(-) diff --git a/src/keybind/builtin/vim.json b/src/keybind/builtin/vim.json index 587f148f..07d82099 100644 --- a/src/keybind/builtin/vim.json +++ b/src/keybind/builtin/vim.json @@ -82,7 +82,20 @@ ["\"_dd", "delete_line"], ["diw", "cut_inside_word"], + ["di(", "cut_inside_parentheses"], + ["di)", "cut_inside_parentheses"], + ["di[", "cut_inside_square_brackets"], + ["di]", "cut_inside_square_brackets"], + ["di{", "cut_inside_braces"], + ["di}", "cut_inside_braces"], + ["daw", "cut_around_word"], + ["da(", "cut_around_parentheses"], + ["da)", "cut_around_parentheses"], + ["da[", "cut_around_square_brackets"], + ["da]", "cut_around_square_brackets"], + ["da{", "cut_around_braces"], + ["da}", "cut_around_braces"], ["cc", ["enter_mode", "insert"], ["cut_internal_vim"]], ["C", ["enter_mode", "insert"], ["cut_to_end_vim"]], @@ -91,12 +104,38 @@ ["cb", ["enter_mode", "insert"], ["cut_word_left_vim"]], ["ciw", ["enter_mode", "insert"], ["cut_inside_word"]], + ["ci(", ["enter_mode", "insert"], ["cut_inside_parentheses"]], + ["ci)", ["enter_mode", "insert"], ["cut_inside_parentheses"]], + ["ci[", ["enter_mode", "insert"], ["cut_inside_square_brackets"]], + ["ci]", ["enter_mode", "insert"], ["cut_inside_square_brackets"]], + ["ci{", ["enter_mode", "insert"], ["cut_inside_braces"]], + ["ci}", ["enter_mode", "insert"], ["cut_inside_braces"]], + ["caw", ["enter_mode", "insert"], ["cut_around_word"]], + ["ca(", ["enter_mode", "insert"], ["cut_around_parentheses"]], + ["ca)", ["enter_mode", "insert"], ["cut_around_parentheses"]], + ["ca[", ["enter_mode", "insert"], ["cut_around_square_brackets"]], + ["ca]", ["enter_mode", "insert"], ["cut_around_square_brackets"]], + ["ca{", ["enter_mode", "insert"], ["cut_around_braces"]], + ["ca}", ["enter_mode", "insert"], ["cut_around_braces"]], ["yy", ["copy_line_internal_vim"], ["cancel"]], ["yiw", ["copy_inside_word"], ["cancel"]], + ["yi(", ["copy_inside_parentheses"], ["cancel"]], + ["yi)", ["copy_inside_parentheses"], ["cancel"]], + ["yi[", ["copy_inside_square_brackets"], ["cancel"]], + ["yi]", ["copy_inside_square_brackets"], ["cancel"]], + ["yi{", ["copy_inside_braces"], ["cancel"]], + ["yi}", ["copy_inside_braces"], ["cancel"]], + ["yaw", ["copy_around_word"], ["cancel"]], + ["ya(", ["copy_around_parentheses"], ["cancel"]], + ["ya)", ["copy_around_parentheses"], ["cancel"]], + ["ya[", ["copy_around_square_brackets"], ["cancel"]], + ["ya]", ["copy_around_square_brackets"], ["cancel"]], + ["ya{", ["copy_around_braces"], ["cancel"]], + ["ya}", ["copy_around_braces"], ["cancel"]], ["", "move_scroll_half_page_up_vim"], ["", "move_scroll_half_page_down_vim"], @@ -169,7 +208,20 @@ ["e", "select_word_right_end_vim"], ["iw", "select_inside_word"], + ["i(", "select_inside_parentheses"], + ["i)", "select_inside_parentheses"], + ["i[", "select_inside_square_brackets"], + ["i]", "select_inside_square_brackets"], + ["i{", "select_inside_braces"], + ["i}", "select_inside_braces"], + ["aw", "select_around_word"], + ["a(", "select_around_parentheses"], + ["a)", "select_around_parentheses"], + ["a[", "select_around_square_brackets"], + ["a]", "select_around_square_brackets"], + ["a{", "select_around_braces"], + ["a}", "select_around_braces"], ["^", "smart_move_begin"], ["$", "select_end"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index e65dd6d5..74a99168 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -41,7 +41,7 @@ const double_click_time_ms = 350; const syntax_full_reparse_time_limit = 0; // ms (0 = always use incremental) const syntax_full_reparse_error_threshold = 3; // number of tree-sitter errors that trigger a full reparse -const bracket_search_radius = if (builtin.mode == std.builtin.OptimizeMode.Debug) 8_192 else 65_536; +pub const bracket_search_radius = if (builtin.mode == std.builtin.OptimizeMode.Debug) 8_192 else 65_536; pub const max_matches = if (builtin.mode == std.builtin.OptimizeMode.Debug) 10_000 else 100_000; pub const max_match_lines = 15; diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index f7067e50..043ed35a 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -8,6 +8,7 @@ const Buffer = @import("Buffer"); const Cursor = Buffer.Cursor; const CurSel = @import("../editor.zig").CurSel; const Editor = @import("../editor.zig").Editor; +const bracket_search_radius = @import("../editor.zig").bracket_search_radius; var commands: Commands = undefined; @@ -165,6 +166,60 @@ const cmds_ = struct { } pub const select_around_word_meta: Meta = .{ .description = "Select around word" }; + pub fn select_inside_parentheses(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_parentheses_textobject, ed.metrics); + } + pub const select_inside_parentheses_meta: Meta = .{ .description = "Select inside ()" }; + + pub fn select_around_parentheses(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_parentheses_textobject, ed.metrics); + } + pub const select_around_parentheses_meta: Meta = .{ .description = "Select around ()" }; + + pub fn select_inside_square_brackets(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_square_brackets_textobject, ed.metrics); + } + pub const select_inside_square_brackets_meta: Meta = .{ .description = "Select inside []" }; + + pub fn select_around_square_brackets(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_square_brackets_textobject, ed.metrics); + } + pub const select_around_square_brackets_meta: Meta = .{ .description = "Select around []" }; + + pub fn select_inside_braces(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_braces_textobject, ed.metrics); + } + pub const select_inside_braces_meta: Meta = .{ .description = "Select inside {}" }; + + pub fn select_around_braces(_: *void, _: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_braces_textobject, ed.metrics); + } + pub const select_around_braces_meta: Meta = .{ .description = "Select around {}" }; + pub fn cut_inside_word(_: *void, ctx: Ctx) Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; @@ -185,6 +240,66 @@ const cmds_ = struct { } pub const cut_around_word_meta: Meta = .{ .description = "Cut around word" }; + pub fn cut_inside_parentheses(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_parentheses_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_parentheses_meta: Meta = .{ .description = "Cut inside ()" }; + + pub fn cut_around_parentheses(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_parentheses_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_parentheses_meta: Meta = .{ .description = "Cut around ()" }; + + pub fn cut_inside_square_brackets(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_square_brackets_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_square_brackets_meta: Meta = .{ .description = "Cut inside []" }; + + pub fn cut_around_square_brackets(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_square_brackets_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_square_brackets_meta: Meta = .{ .description = "Cut around []" }; + + pub fn cut_inside_braces(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_braces_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_inside_braces_meta: Meta = .{ .description = "Cut inside {}" }; + + pub fn cut_around_braces(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_braces_textobject, ed.metrics); + try ed.cut_internal_vim(ctx); + } + pub const cut_around_braces_meta: Meta = .{ .description = "Cut around {}" }; + pub fn copy_inside_word(_: *void, ctx: Ctx) Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; @@ -204,6 +319,66 @@ const cmds_ = struct { try ed.copy_internal_vim(ctx); } pub const copy_around_word_meta: Meta = .{ .description = "Copy around word" }; + + pub fn copy_inside_parentheses(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_parentheses_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_parentheses_meta: Meta = .{ .description = "Copy inside ()" }; + + pub fn copy_around_parentheses(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_parentheses_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_parentheses_meta: Meta = .{ .description = "Copy around ()" }; + + pub fn copy_inside_square_brackets(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_square_brackets_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_square_brackets_meta: Meta = .{ .description = "Copy inside []" }; + + pub fn copy_around_square_brackets(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_square_brackets_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_square_brackets_meta: Meta = .{ .description = "Copy around []" }; + + pub fn copy_inside_braces(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_inside_braces_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_inside_braces_meta: Meta = .{ .description = "Copy inside {}" }; + + pub fn copy_around_braces(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = ed.buf_root() catch return; + + try ed.with_cursels_const(root, select_around_braces_textobject, ed.metrics); + try ed.copy_internal_vim(ctx); + } + pub const copy_around_braces_meta: Meta = .{ .description = "Copy around {}" }; }; fn is_tab_or_space(c: []const u8) bool { @@ -267,3 +442,102 @@ fn select_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Me sel.end = next; cursel.*.cursor = next; } + +fn select_inside_parentheses_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "(", ")", .inside); +} + +fn select_around_parentheses_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "(", ")", .around); +} + +fn select_inside_square_brackets_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "[", "]", .inside); +} + +fn select_around_square_brackets_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "[", "]", .around); +} + +fn select_inside_braces_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "{", "}", .inside); +} + +fn select_around_braces_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + return try select_bracket_textobject(root, cursel, metrics, "{", "}", .around); +} + +fn select_bracket_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, opening_char: []const u8, closing_char: []const u8, scope: enum { inside, around }) !void { + const current = cursel.cursor; + var prev = cursel.cursor; + var next = cursel.cursor; + + const bracket_egc, _, _ = root.egc_at(current.row, current.col, metrics) catch { + return error.Stop; + }; + if (std.mem.eql(u8, bracket_egc, opening_char)) { + const closing_row, const closing_col = try Editor.match_bracket(root, current, metrics); + + prev = current; + next.row = closing_row; + next.col = closing_col; + } else if (std.mem.eql(u8, bracket_egc, closing_char)) { + const opening_row, const opening_col = try Editor.match_bracket(root, current, metrics); + + prev.row = opening_row; + prev.col = opening_col; + next = current; + } else { + const opening_pos, const closing_pos = find_bracket_pair(root, cursel, metrics, .left, opening_char) catch try find_bracket_pair(root, cursel, metrics, .right, opening_char); + + prev.row = opening_pos[0]; + prev.col = opening_pos[1]; + next.row = closing_pos[0]; + next.col = closing_pos[1]; + } + + prev.move_right(root, metrics) catch {}; + + if (scope == .around) { + prev.move_left(root, metrics) catch {}; + next.move_right(root, metrics) catch {}; + } + + const sel = cursel.enable_selection(root, metrics); + sel.begin = prev; + sel.end = next; + cursel.*.cursor = next; +} + +fn find_bracket_pair(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, direction: enum { left, right }, char: []const u8) error{Stop}!struct { struct { usize, usize }, struct { usize, usize } } { + const start = cursel.cursor; + var moving_cursor = cursel.cursor; + + var i: usize = 0; + while (i < bracket_search_radius) : (i += 1) { + switch (direction) { + .left => try moving_cursor.move_left(root, metrics), + .right => try moving_cursor.move_right(root, metrics), + } + + const curr_egc, _, _ = root.egc_at(moving_cursor.row, moving_cursor.col, metrics) catch { + return error.Stop; + }; + if (std.mem.eql(u8, char, curr_egc)) { + const closing_row, const closing_col = try Editor.match_bracket(root, moving_cursor, metrics); + + switch (direction) { + .left => if (closing_row > start.row or (closing_row == start.row and closing_col > start.col)) { + return .{ .{ moving_cursor.row, moving_cursor.col }, .{ closing_row, closing_col } }; + } else { + continue; + }, + .right => { + return .{ .{ moving_cursor.row, moving_cursor.col }, .{ closing_row, closing_col } }; + }, + } + } + } + + return error.Stop; +} From d53d155c6d94bd6caec6655c88e0744f4092a95e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 31 Mar 2026 14:01:22 +0200 Subject: [PATCH 10/12] feat: add V language support closes #509 --- build.zig.zon | 4 ++-- src/file_type_lsp.zig | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 48e0c7d0..71a537a6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,8 +6,8 @@ .dependencies = .{ .syntax = .{ - .url = "git+https://github.com/neurocyte/flow-syntax?ref=master#56929f0c523b59153e17919be2cd09d8bef32cd0", - .hash = "flow_syntax-0.7.2-X8jOoeFTAQBeP2Tn08Tw1jsMdifLEDBgPLqPqNelAupy", + .url = "git+https://github.com/neurocyte/flow-syntax?ref=master#7b1fd3a97f00aba3a95cc65b95f34162347ed1ea", + .hash = "flow_syntax-0.7.2-X8jOoQhWAQBPt1rBRmttAGI0Z2QC-hCSZuoBZoZgr6Vv", }, .flags = .{ .url = "git+https://github.com/neurocyte/flags?ref=main#984b27948da3e4e40a253f76c85b51ec1a9ada11", diff --git a/src/file_type_lsp.zig b/src/file_type_lsp.zig index 9064fc3c..a359e8a7 100644 --- a/src/file_type_lsp.zig +++ b/src/file_type_lsp.zig @@ -232,6 +232,11 @@ pub const typst = .{ pub const uxntal = .{}; +pub const v = .{ + .language_server = .{"v-analyzer"}, + .formatter = .{ "v", "fmt", "-" }, +}; + pub const vim = .{}; pub const xml = .{ From 310221bb266ae6a0252541daf6ebc2dacedbf5b7 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 31 Mar 2026 20:58:00 +0200 Subject: [PATCH 11/12] feat: support adding entirely new themes via the config --- src/main.zig | 6 +- src/soft_root.zig | 5 ++ src/tui/Widget.zig | 110 ++++++++++++++++++++++++- src/tui/mode/overlay/theme_palette.zig | 3 +- src/tui/tui.zig | 73 ++-------------- 5 files changed, 126 insertions(+), 71 deletions(-) diff --git a/src/main.zig b/src/main.zig index b191faf5..a8de7100 100644 --- a/src/main.zig +++ b/src/main.zig @@ -895,15 +895,15 @@ pub fn write_theme(theme_name: []const u8, content: []const u8) !void { pub fn list_themes(allocator: std.mem.Allocator) ![]const []const u8 { var dir = try std.fs.openDirAbsolute(try get_theme_directory(), .{ .iterate = true }); defer dir.close(); - var result = std.ArrayList([]const u8).init(allocator); + var result: std.ArrayList([]const u8) = .empty; var iter = dir.iterateAssumeFirstIteration(); while (try iter.next()) |entry| { switch (entry.kind) { - .file, .sym_link => try result.append(try allocator.dupe(u8, std.fs.path.stem(entry.name))), + .file, .sym_link => try result.append(allocator, try allocator.dupe(u8, std.fs.path.stem(entry.name))), else => continue, } } - return result.toOwnedSlice(); + return result.toOwnedSlice(allocator); } pub fn get_config_dir() ConfigDirError![]const u8 { diff --git a/src/soft_root.zig b/src/soft_root.zig index a792e730..d076e942 100644 --- a/src/soft_root.zig +++ b/src/soft_root.zig @@ -29,6 +29,7 @@ pub const root = struct { pub const read_theme = if (@hasDecl(hard_root, "read_theme")) hard_root.read_theme else dummy.read_theme; pub const write_theme = if (@hasDecl(hard_root, "write_theme")) hard_root.write_theme else dummy.write_theme; + pub const list_themes = if (@hasDecl(hard_root, "list_themes")) hard_root.list_themes else dummy.list_themes; pub const get_theme_file_name = if (@hasDecl(hard_root, "get_theme_file_name")) hard_root.get_theme_file_name else dummy.get_theme_file_name; pub const exit = if (@hasDecl(hard_root, "exit")) hard_root.exit else dummy.exit; @@ -109,6 +110,10 @@ const dummy = struct { pub fn write_theme(_: []const u8, _: []const u8) !void { @panic("dummy write_theme call"); } + pub fn list_themes(_: std.mem.Allocator) ![]const []const u8 { + @panic("dummy list_themes call"); + } + pub fn get_theme_file_name(_: []const u8) ![]const u8 { @panic("dummy get_theme_file_name call"); } diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 6db55a94..9a473ecf 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -1,6 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const tp = @import("thespian"); +const root = @import("soft_root").root; const Plane = @import("renderer").Plane; const EventHandler = @import("EventHandler"); @@ -9,7 +10,6 @@ const tui = @import("tui.zig"); pub const Box = @import("Box.zig"); pub const Pos = struct { y: i32 = 0, x: i32 = 0 }; 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; @@ -42,6 +42,114 @@ pub const Layout = union(enum) { } }; +pub const ThemeInfo = struct { + name: []const u8, + storage: ?std.json.Parsed(Theme) = null, + + pub fn get(self: *@This(), allocator: std.mem.Allocator) ?Theme { + if (load_theme_file(allocator, self.name) catch null) |parsed_theme| { + self.storage = parsed_theme; + return self.storage.?.value; + } + + for (static_themes) |theme_| { + if (std.mem.eql(u8, theme_.name, self.name)) + return theme_; + } + return null; + } + + fn load_theme_file(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) { + return load_theme_file_internal(allocator, theme_name) catch |e| { + std.log.err("Error loading theme '{s}' from file: {t}", .{ theme_name, e }); + return e; + }; + } + fn load_theme_file_internal(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) { + const json_str = root.read_theme(allocator, theme_name) orelse return null; + defer allocator.free(json_str); + return try std.json.parseFromSlice(Theme, allocator, json_str, .{ .allocate = .alloc_always }); + } +}; + +var themes_: ?std.StringHashMap(*ThemeInfo) = null; +var theme_names_: ?[]const []const u8 = null; +const static_themes = @import("themes").themes; + +fn get_themes(allocator: std.mem.Allocator) *std.StringHashMap(*ThemeInfo) { + if (themes_) |*themes__| return themes__; + + const theme_files = root.list_themes(allocator) catch @panic("OOM get_themes"); + var themes: std.StringHashMap(*ThemeInfo) = .init(allocator); + defer allocator.free(theme_files); + for (theme_files) |file| { + const theme_info = allocator.create(ThemeInfo) catch @panic("OOM get_themes"); + theme_info.* = .{ + .name = file, + }; + themes.put(theme_info.name, theme_info) catch @panic("OOM get_themes"); + } + + for (static_themes) |theme_| if (!themes.contains(theme_.name)) { + const theme_info = allocator.create(ThemeInfo) catch @panic("OOM get_themes"); + theme_info.* = .{ + .name = theme_.name, + }; + themes.put(theme_info.name, theme_info) catch @panic("OOM get_themes"); + }; + themes_ = themes; + return &themes_.?; +} + +fn get_theme_names() []const []const u8 { + if (theme_names_) |names_| return names_; + const themes = themes_ orelse return &.{}; + var i = get_themes(themes.allocator).iterator(); + var names: std.ArrayList([]const u8) = .empty; + while (i.next()) |theme_| names.append(themes.allocator, theme_.value_ptr.*.name) catch @panic("OOM get_theme_names"); + std.mem.sort([]const u8, names.items, {}, struct { + fn cmp(_: void, lhs: []const u8, rhs: []const u8) bool { + return std.mem.order(u8, lhs, rhs) == .lt; + } + }.cmp); + theme_names_ = names.toOwnedSlice(themes.allocator) catch @panic("OOM get_theme_names"); + return theme_names_.?; +} + +pub fn get_theme_by_name(allocator: std.mem.Allocator, name_: []const u8) ?Theme { + const themes = get_themes(allocator); + const theme = themes.get(name_) orelse return null; + return theme.get(allocator); +} + +pub fn get_next_theme_by_name(name_: []const u8) []const u8 { + const theme_names = get_theme_names(); + var next = false; + for (theme_names) |theme_name| { + if (next) + return theme_name; + if (std.mem.eql(u8, theme_name, name_)) + next = true; + } + return theme_names[0]; +} + +pub fn get_prev_theme_by_name(name_: []const u8) []const u8 { + const theme_names = get_theme_names(); + const last = theme_names[theme_names.len - 1]; + var prev: ?[]const u8 = null; + for (theme_names) |theme_name| { + if (std.mem.eql(u8, theme_name, name_)) + return prev orelse last; + prev = theme_name; + } + return last; +} + +pub fn list_themes() []const []const u8 { + return get_theme_names(); +} + pub const VTable = struct { 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/mode/overlay/theme_palette.zig b/src/tui/mode/overlay/theme_palette.zig index a58e9844..1f9396df 100644 --- a/src/tui/mode/overlay/theme_palette.zig +++ b/src/tui/mode/overlay/theme_palette.zig @@ -33,7 +33,8 @@ pub fn load_entries(palette: *Type) !usize { var longest_hint: usize = 0; var idx: usize = 0; try set_previous_theme(palette, tui.theme().name); - for (Widget.themes) |theme| { + for (Widget.list_themes()) |theme_name_| { + const theme = Widget.get_theme_by_name(palette.allocator, theme_name_) orelse continue; idx += 1; (try palette.entries.addOne(palette.allocator)).* = .{ .label = theme.description, diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 3d9a1ad6..6b04a30c 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -62,9 +62,7 @@ logger: log.Logger, drag_source: ?Widget = null, drag_button: input.MouseType = 0, dark_theme: Widget.Theme, -dark_parsed_theme: ?std.json.Parsed(Widget.Theme), light_theme: Widget.Theme, -light_parsed_theme: ?std.json.Parsed(Widget.Theme), idle_frame_count: usize = 0, unrendered_input_events_count: usize = 0, init_timer: ?tp.timeout, @@ -160,9 +158,9 @@ fn init(allocator: Allocator) InitError!*Self { if (@hasDecl(renderer, "install_crash_handler") and conf.start_debugger_on_crash) renderer.jit_debugger_enabled = true; - const dark_theme, const dark_parsed_theme = get_theme_by_name(allocator, conf.theme) orelse get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme; + const dark_theme = Widget.get_theme_by_name(allocator, conf.theme) orelse Widget.get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme; conf.theme = dark_theme.name; - const light_theme, const light_parsed_theme = get_theme_by_name(allocator, conf.light_theme) orelse get_theme_by_name(allocator, "default-light") orelse return error.UnknownTheme; + const light_theme = Widget.get_theme_by_name(allocator, conf.light_theme) orelse Widget.get_theme_by_name(allocator, "default-light") orelse return error.UnknownTheme; conf.light_theme = light_theme.name; if (build_options.gui) conf.enable_terminal_cursor = false; @@ -203,8 +201,6 @@ fn init(allocator: Allocator) InitError!*Self { .query_cache_ = try syntax.QueryCache.create(allocator, .{}), .dark_theme = dark_theme, .light_theme = light_theme, - .dark_parsed_theme = dark_parsed_theme, - .light_parsed_theme = light_parsed_theme, }; instance_ = self; defer instance_ = null; @@ -987,21 +983,13 @@ fn refresh_input_mode(self: *Self) command.Result { } fn set_theme_by_name(self: *Self, name: []const u8, action: enum { none, store }) !void { - const theme_, const parsed_theme = get_theme_by_name(self.allocator, name) orelse { + const theme_ = Widget.get_theme_by_name(self.allocator, name) orelse { self.logger.print("theme not found: {s}", .{name}); return; }; switch (self.color_scheme) { - .dark => { - if (self.dark_parsed_theme) |p| p.deinit(); - self.dark_parsed_theme = parsed_theme; - self.dark_theme = theme_; - }, - .light => { - if (self.light_parsed_theme) |p| p.deinit(); - self.light_parsed_theme = parsed_theme; - self.light_theme = theme_; - }, + .dark => self.dark_theme = theme_, + .light => self.light_theme = theme_, } self.set_terminal_style(&theme_); self.logger.print("theme: {s}", .{theme_.description}); @@ -1141,13 +1129,13 @@ const cmds = struct { pub const set_theme_meta: Meta = .{ .arguments = &.{.string} }; pub fn theme_next(self: *Self, _: Ctx) Result { - const name = get_next_theme_by_name(self.current_theme().name); + const name = Widget.get_next_theme_by_name(self.current_theme().name); return self.set_theme_by_name(name, .store); } 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.current_theme().name); + const name = Widget.get_prev_theme_by_name(self.current_theme().name); return self.set_theme_by_name(name, .store); } pub const theme_prev_meta: Meta = .{ .description = "Previous color theme" }; @@ -2020,40 +2008,6 @@ pub fn theme() *const Widget.Theme { return current().current_theme(); } -pub fn get_theme_by_name(allocator: std.mem.Allocator, name: []const u8) ?struct { Widget.Theme, ?std.json.Parsed(Widget.Theme) } { - if (load_theme_file(allocator, name) catch null) |parsed_theme| { - std.log.info("loaded theme from file: {s}", .{name}); - return .{ parsed_theme.value, parsed_theme }; - } - - for (Widget.themes) |theme_| { - if (std.mem.eql(u8, theme_.name, name)) - return .{ theme_, null }; - } - return null; -} - -fn get_next_theme_by_name(name: []const u8) []const u8 { - var next = false; - for (Widget.themes) |theme_| { - if (next) - return theme_.name; - if (std.mem.eql(u8, theme_.name, name)) - next = true; - } - return Widget.themes[0].name; -} - -fn get_prev_theme_by_name(name: []const u8) []const u8 { - var prev: ?Widget.Theme = null; - for (Widget.themes) |theme_| { - if (std.mem.eql(u8, theme_.name, name)) - return (prev orelse Widget.themes[Widget.themes.len - 1]).name; - prev = theme_; - } - return Widget.themes[Widget.themes.len - 1].name; -} - pub fn find_scope_style(theme_: *const Widget.Theme, scope: []const u8) ?Widget.Theme.Token { return if (find_scope_fallback(scope)) |tm_scope| scope_to_theme_token(theme_, tm_scope) orelse @@ -2442,19 +2396,6 @@ fn get_or_create_theme_file(self: *Self, allocator: std.mem.Allocator) ![]const return try root.get_theme_file_name(theme_name); } -fn load_theme_file(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Widget.Theme) { - return load_theme_file_internal(allocator, theme_name) catch |e| { - std.log.err("loaded theme from file failed: {}", .{e}); - return e; - }; -} -fn load_theme_file_internal(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Widget.Theme) { - _ = std.json.Scanner; - const json_str = root.read_theme(allocator, theme_name) orelse return null; - defer allocator.free(json_str); - return try std.json.parseFromSlice(Widget.Theme, allocator, json_str, .{ .allocate = .alloc_always }); -} - pub const WidgetType = @import("config").WidgetType; pub const ConfigWidgetStyle = @import("config").WidgetStyle; pub const WidgetStyle = @import("WidgetStyle.zig"); From cf7fc6af5455646a84c3b571cd6c8de85ccf4af1 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 31 Mar 2026 20:58:54 +0200 Subject: [PATCH 12/12] fix: allow loading custom theme files up to 512Kb in size closes #544 --- src/main.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index a8de7100..054340ca 100644 --- a/src/main.zig +++ b/src/main.zig @@ -882,7 +882,10 @@ pub fn read_theme(allocator: std.mem.Allocator, theme_name: []const u8) ?[]const const file_name = get_theme_file_name(theme_name) catch return null; var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; defer file.close(); - return file.readToEndAlloc(allocator, 64 * 1024) catch null; + return file.readToEndAlloc(allocator, 512 * 1024) catch |e| { + std.log.err("Error reading theme file: {t}", .{e}); + return null; + }; } pub fn write_theme(theme_name: []const u8, content: []const u8) !void {