From 645e0d4e3bc96492a09ade8c18be4e341acbf43e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 12 Feb 2026 12:59:16 +0100 Subject: [PATCH 1/4] refactor: rename TabBar.splits_list --- src/tui/status/tabs.zig | 58 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index b7bc505..4ec5eb3 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -100,8 +100,8 @@ pub fn create(allocator: std.mem.Allocator, parent: Plane, event_handler: ?Event pub const TabBar = struct { allocator: std.mem.Allocator, plane: Plane, - widget_list: *WidgetList, - widget_list_widget: Widget, + splits_list: *WidgetList, + splits_list_widget: Widget, event_handler: ?EventHandler, tabs: []TabBarTab = &[_]TabBarTab{}, active_focused_buffer_ref: ?Buffer.Ref = null, @@ -133,8 +133,8 @@ pub const TabBar = struct { return .{ .allocator = allocator, .plane = w.plane, - .widget_list = w, - .widget_list_widget = w.widget(), + .splits_list = w, + .splits_list_widget = w.widget(), .event_handler = event_handler, .tab_style = tab_style, .tab_style_bufs = tab_style_bufs, @@ -145,13 +145,13 @@ pub const TabBar = struct { pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { root.free_config(self.allocator, self.tab_style_bufs); self.allocator.free(self.tabs); - self.widget_list_widget.deinit(allocator); + self.splits_list_widget.deinit(allocator); allocator.destroy(self); } pub fn layout(self: *Self) Widget.Layout { return if (self.tabs.len >= self.minimum_tabs_shown) - self.widget_list_widget.layout() + self.splits_list_widget.layout() else .{ .static = 0 }; } @@ -159,9 +159,9 @@ pub const TabBar = struct { pub fn update(self: *Self) void { const drag_source, const drag_btn = tui.get_drag_source(); self.update_tabs(drag_source) catch {}; - self.widget_list_widget.resize(Widget.Box.from(self.plane)); - self.widget_list_widget.update(); - for (self.widget_list.widgets.items) |*split_widgetstate| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| + self.splits_list_widget.resize(Widget.Box.from(self.plane)); + self.splits_list_widget.update(); + for (self.splits_list.widgets.items) |*split_widgetstate| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| for (split.widgets.items) |*widgetstate| if (widgetstate.widget.dynamic_cast(Tab.ButtonType)) |btn| if (btn.drag_pos) |_| tui.update_drag_source(&widgetstate.widget, drag_btn); tui.refresh_hover(@src()); @@ -236,7 +236,7 @@ pub const TabBar = struct { fn handle_event_drop_target(self: *Self, dragging: usize) tp.result { var hover_view: ?usize = null; - for (self.widget_list.widgets.items, 0..) |*split_widgetstate, idx| + for (self.splits_list.widgets.items, 0..) |*split_widgetstate, idx| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| { for (split.widgets.items) |*widgetstate| if (widgetstate.widget.dynamic_cast(drop_target.ButtonType)) |btn| { @@ -251,20 +251,20 @@ pub const TabBar = struct { } pub fn handle_resize(self: *Self, pos: Widget.Box) void { - self.widget_list_widget.resize(pos); - self.plane = self.widget_list.plane; + self.splits_list_widget.resize(pos); + self.plane = self.splits_list.plane; } pub fn get(self: *const Self, name: []const u8) ?*const Widget { - return self.widget_list_widget.get(name); + return self.splits_list_widget.get(name); } pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn, _: *Widget) bool { - return self.widget_list_widget.walk(ctx, f); + return self.splits_list_widget.walk(ctx, f); } pub fn hover(self: *Self) bool { - return self.widget_list_widget.hover(); + return self.splits_list_widget.hover(); } fn update_tabs(self: *Self, drag_source: ?*Widget) !void { @@ -273,7 +273,7 @@ pub const TabBar = struct { if (tab.widget.dynamic_cast(Tab.ButtonType)) |btn| if (btn.drag_pos) |_| break true; } else false; - if (!dragging and !buffers_changed and self.widget_list.widgets.items.len > 0) return; + if (!dragging and !buffers_changed and self.splits_list.widgets.items.len > 0) return; try self.update_tab_widgets(drag_source); } @@ -282,19 +282,19 @@ pub const TabBar = struct { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); var prev_widget_count: usize = 0; - for (self.widget_list.widgets.items) |*split_widgetstate| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| { + for (self.splits_list.widgets.items) |*split_widgetstate| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| { prev_widget_count += 1; for (split.widgets.items) |_| prev_widget_count += 1; }; - for (self.widget_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| { + for (self.splits_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| { for (split.widgets.items) |*widget| if (&widget.widget == drag_source) tui.reset_drag_context(); }; - while (self.widget_list.pop()) |split_widget| if (split_widget.dynamic_cast(WidgetList)) |split| { + while (self.splits_list.pop()) |split_widget| if (split_widget.dynamic_cast(WidgetList)) |split| { while (split.pop()) |widget| if (widget.dynamic_cast(Tab.ButtonType) == null) - widget.deinit(self.widget_list.allocator); - split.deinit(self.widget_list.allocator); + widget.deinit(self.splits_list.allocator); + split.deinit(self.splits_list.allocator); }; for (self.tabs) |*tab| if (buffer_manager.buffer_from_ref(tab.buffer_ref)) |buffer| { @@ -306,8 +306,8 @@ pub const TabBar = struct { var widget_count: usize = 0; for (0..views) |view| { var first = true; - var view_widget_list = try WidgetList.createH(self.allocator, self.widget_list.plane, "split", .dynamic); - try self.widget_list.add(view_widget_list.widget()); + var view_widget_list = try WidgetList.createH(self.allocator, self.splits_list.plane, "split", .dynamic); + try self.splits_list.add(view_widget_list.widget()); widget_count += 1; for (self.tabs) |tab| { const tab_view = tab.view orelse 0; @@ -413,7 +413,7 @@ pub const TabBar = struct { } fn find_first_tab_buffer(self: *Self) ?Buffer.Ref { - for (self.widget_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| + for (self.splits_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| for (split.widgets.items) |*widget_state| if (widget_state.widget.dynamic_cast(Tab.ButtonType)) |btn| return btn.opts.ctx.buffer_ref; return null; @@ -421,7 +421,7 @@ pub const TabBar = struct { fn find_last_tab_buffer(self: *Self) ?Buffer.Ref { var last: ?Buffer.Ref = null; - for (self.widget_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| + for (self.splits_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| for (split.widgets.items) |*widget_state| if (widget_state.widget.dynamic_cast(Tab.ButtonType)) |btn| { last = btn.opts.ctx.buffer_ref; }; @@ -430,7 +430,7 @@ pub const TabBar = struct { fn find_next_tab_buffer(self: *Self) struct { ?Buffer.Ref, usize } { var found_active: bool = false; - for (self.widget_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| + for (self.splits_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| for (split.widgets.items) |*widget_state| if (widget_state.widget.dynamic_cast(Tab.ButtonType)) |btn| { if (found_active) return .{ btn.opts.ctx.buffer_ref, btn.opts.ctx.view }; @@ -443,7 +443,7 @@ pub const TabBar = struct { fn find_previous_tab_buffer(self: *Self) struct { ?Buffer.Ref, usize } { var previous: ?Buffer.Ref = null; var previous_view: usize = 0; - for (self.widget_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| + for (self.splits_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| for (split.widgets.items) |*widget_state| if (widget_state.widget.dynamic_cast(Tab.ButtonType)) |btn| { if (btn.opts.ctx.buffer_ref == self.active_focused_buffer_ref) return .{ previous, previous_view }; @@ -663,7 +663,7 @@ const Tab = struct { ) !Widget { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); const buffer = buffer_manager.buffer_from_ref(buffer_ref); - return Button.create_widget(Tab, tabbar.allocator, tabbar.widget_list.plane, .{ + return Button.create_widget(Tab, tabbar.allocator, tabbar.splits_list.plane, .{ .ctx = .{ .tabbar = tabbar, .buffer_ref = buffer_ref, .view = if (buffer) |buf| buf.get_last_view() orelse 0 else 0, .tab_style = tab_style }, .label = if (buffer) |buf| name_from_buffer(buf) else "???", .on_click = Tab.on_click, @@ -1026,7 +1026,7 @@ const drop_target = struct { tabbar: *TabBar, view: usize, ) !Widget { - return Button.create_widget(@This(), tabbar.allocator, tabbar.widget_list.plane, .{ + return Button.create_widget(@This(), tabbar.allocator, tabbar.splits_list.plane, .{ .ctx = .{ .tabbar = tabbar, .view = view }, .label = &.{}, .on_layout = @This().layout, From ac12252ce11b9579008f48bd42f1eca5af62e897 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 12 Feb 2026 13:59:20 +0100 Subject: [PATCH 2/4] fix: clip tabs that do not fit in their split --- src/tui/status/tabs.zig | 42 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index 4ec5eb3..3d72f1c 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -30,6 +30,7 @@ const @"style.config" = struct { close_icon_fg: colors = .Error, save_icon: []const u8 = "󰆓", save_icon_fg: ?colors = null, + clipping_indicator: []const u8 = "»", spacer: []const u8 = "|", spacer_fg: colors = .active_bg, @@ -177,10 +178,40 @@ pub const TabBar = struct { }); self.plane.fill(" "); self.plane.home(); - for (self.tabs) |*tab| _ = tab.widget.render(theme); + for (self.tabs) |*tab| { + const clipped, const clip_box = self.is_tab_clipped(tab); + if (clipped) { + if (clip_box) |box| self.render_clipping_indicator(box, theme); + continue; + } + _ = tab.widget.render(theme); + } return false; } + fn is_tab_clipped(self: *const Self, tab: *const TabBarTab) struct { bool, ?Widget.Box } { + const view = tab.view orelse return .{ true, null }; + const split_idx = if (view < self.splits_list.widgets.items.len) view else return .{ true, null }; + const split = self.splits_list.widgets.items[split_idx]; + const split_box = Widget.Box.from(split.widget.plane.*); + const widget_box = tab.widget.box(); + const dragging = if (tab.widget.dynamic_cast(Tab.ButtonType)) |btn| if (btn.drag_pos) |_| true else false else false; + if (dragging) return .{ false, split_box }; + if (split_box.y + split_box.h < widget_box.y + widget_box.h or + split_box.x + split_box.w < widget_box.x + widget_box.w) + return .{ true, split_box }; + return .{ false, split_box }; + } + + fn render_clipping_indicator(self: *@This(), box: Widget.Box, theme: *const Widget.Theme) void { + self.plane.set_style(.{ + .fg = self.tab_style.bar_fg.from_theme(theme), + .bg = self.tab_style.bar_bg.from_theme(theme), + }); + self.plane.cursor_move_yx(0, @intCast(box.x + box.w -| 1)); + self.plane.putchar(self.tab_style.clipping_indicator); + } + pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { var buffer_ref: Buffer.Ref = undefined; if (try m.match(.{"next_tab"})) { @@ -259,8 +290,13 @@ pub const TabBar = struct { return self.splits_list_widget.get(name); } - pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn, _: *Widget) bool { - return self.splits_list_widget.walk(ctx, f); + pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn, self_w: *Widget) bool { + for (self.tabs) |*tab| { + const clipped, _ = self.is_tab_clipped(tab); + if (!clipped) + if (tab.widget.walk(ctx, f)) return true; + } + return f(ctx, self_w); } pub fn hover(self: *Self) bool { From 414668c4cd90a156795d9d6acb09c67e27a4a8b3 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 12 Feb 2026 14:00:22 +0100 Subject: [PATCH 3/4] refactor: add Editor.get_vcs_blame --- src/buffer/Buffer.zig | 2 +- src/tui/editor.zig | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 679a150..110e952 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -2,7 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const cbor = @import("cbor"); const TypedInt = @import("TypedInt"); -const VcsBlame = @import("VcsBlame"); +pub const VcsBlame = @import("VcsBlame"); const file_type_config = @import("file_type_config"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 27e44e8..287bcd1 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -1538,13 +1538,16 @@ pub const Editor = struct { return if (head_line < 0) null else @intCast(head_line); } + pub fn get_vcs_blame(self: *const Self, row: usize) ?*const Buffer.VcsBlame.Commit { + const buffer = self.buffer orelse return null; + const blame_row = self.get_delta_lines_until_row(row) orelse return null; + return buffer.get_vcs_blame(blame_row) orelse return null; + } + fn render_blame(self: *Self, theme: *const Widget.Theme, hl_row: ?usize, cell_map: CellMap) !void { const cursor = self.get_primary().cursor; const pos = self.screen_cursor(&cursor) orelse return; - const buffer = self.buffer orelse return; - - const blame_row = self.get_delta_lines_until_row(cursor.row) orelse return; - const commit = buffer.get_vcs_blame(blame_row) orelse return; + const commit = self.get_vcs_blame(cursor.row) orelse return; const screen_width = self.view.cols; var space_begin = screen_width; From 0df97f5ad5a35fc9894a613da786d11bd7b4a2fb Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 12 Feb 2026 14:00:42 +0100 Subject: [PATCH 4/4] feat: add {{blame_commit}} variable for expansion --- src/tui/expansion.zig | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/tui/expansion.zig b/src/tui/expansion.zig index 5f34a0c..c98ab39 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) +/// {{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); defer result.deinit(); @@ -160,6 +161,18 @@ const functions = struct { try stream.writer.print("{d}", .{ed.indent_size}); 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 &.{}; + const ed = mv.get_active_editor() orelse return &.{}; + const row = ed.get_primary().cursor.row; + const commit = ed.get_vcs_blame(row); + const id = if (commit) |c| c.id else ""; + var stream: std.Io.Writer.Allocating = .init(allocator); + try stream.writer.print("{s}", .{id}); + return stream.toOwnedSlice(); + } }; fn get_functions() []struct { []const u8, Function } {