╭ changed ───────────╮ │src/ui/Repo/Diff.zig│ ╰────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │207 if (data.window.start > 0) try addLink(allocator, box, "← previous", try data.route.link(session.page_arena, data.window.start -| page_size, data.path));│ │208 for (data.window.hunks) |hunk| { │ │209 if (data.path.len == 0) if (hunk.path) |path| try addLink(allocator, box, path, try data.route.link(session.page_arena, 0, path)); │ │210 - if (hunk.text.len != 0) try addLink(allocator, box, hunk.text, ""); │ │210 + if (hunk.text.len != 0) try addHunk(allocator, box, hunk.text); │ │211 } │ │212 if (data.window.has_more) try addLink(allocator, box, "next →", try data.route.link(session.page_arena, data.window.start + page_size, data.path)); │ │213 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │214 │ │215 fn addLink(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), text: []const u8, link: []const u8) !void { │ │216 - var tb = try wgt.TextBox.init(allocator, text, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │216 + try addSpans(allocator, box, &.{.{ .text = text }}, link); │ │217 +} │ │218 + │ │219 +// a hunk rendered by renderHunk: each line is a right-aligned line number, a │ │220 +// space, then the edit's +/-/space prefix. inserted lines are green and │ │221 +// deleted lines red. the text box copies the text, so the spans only live │ │222 +// until init. │ │223 +fn addHunk(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), text: []const u8) !void { │ │224 + var spans: std.ArrayList(wgt.Span) = .empty; │ │225 + defer spans.deinit(allocator); │ │226 + var start: usize = 0; │ │227 + while (start < text.len) { │ │228 + // each span keeps its trailing newline so the text box still breaks there │ │229 + const end = if (std.mem.indexOfScalarPos(u8, text, start, '\n')) |nl| nl + 1 else text.len; │ │230 + const line = text[start..end]; │ │231 + const num = std.mem.trimStart(u8, line, " "); │ │232 + const digits = std.mem.indexOfNone(u8, num, "0123456789") orelse num.len; │ │233 + const prefix: u8 = if (digits + 1 < num.len) num[digits + 1] else ' '; │ │234 + const style: wgt.Style = switch (prefix) { │ │235 + '+' => .{ .fg = .{ .ansi = .green } }, │ │236 + '-' => .{ .fg = .{ .ansi = .red } }, │ │237 + else => .{}, │ │238 + }; │ │239 + try spans.append(allocator, .{ .text = line, .style = style }); │ │240 + start = end; │ │241 + } │ │242 + try addSpans(allocator, box, spans.items, ""); │ │243 +} │ │244 + │ │245 +fn addSpans(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), spans: []const wgt.Span, link: []const u8) !void { │ │246 + var tb = try wgt.TextBox.initSpans(allocator, spans, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │247 errdefer tb.deinit(allocator); │ │248 tb.getFocus().mode = .all; │ │249 if (link.len != 0) tb.getFocus().kind = .{ .custom = link }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯