╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │279 const start = writer.written().len; │ │280 if ((line + fi + c) % 8 == 0) { │ │281 const tag = words[(line + fi + c * 3) % words.len]; │ │282 - try writer.writer.print("line {d}: rev {d} {s}", .{ line, c, tag });│ │283 - } else { │ │284 - try writer.writer.print("line {d}", .{line}); │ │282 + try writer.writer.print("rev {d} {s}", .{ c, tag }); │ │283 } │ │284 const target = 40 + (line * 17 + fi * 23) % 70; │ │285 var w = line + fi; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │287 const word = words[w % words.len]; │ │288 const len = writer.written().len - start; │ │289 if (len >= target or len + 1 + word.len > 120) break;│ │292 - try writer.writer.print(" {s}", .{word}); │ │290 + if (len == 0) │ │291 + try writer.writer.print("{s}", .{word}) │ │292 + else │ │293 + try writer.writer.print(" {s}", .{word}); │ │294 } │ │295 try writer.writer.writeByte('\n'); │ │296 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │18 const diff_page = 10; │ │19 │ │20 pub const Hunk = struct { │ │21 + path: ?[]const u8 = null, // the file this hunk belongs to│ │22 lines: []const []const u8, │ │23 }; │ │24 │ ╰──────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │177 var hunk_iter = df.HunkIterator(.xit, .{}).init(gpa, &pair.a, &pair.b) catch continue;│ │178 defer hunk_iter.deinit(gpa); │ │179 │ │179 - // the file header rides on the first of this file's hunks we actually show. │ │180 - var file_header_emitted = false; │ │180 + // the path label rides on the first of this file's hunks we actually show. │ │181 + var path_attached = false; │ │182 while (try hunk_iter.next(gpa)) |hunk_val| { │ │183 var hunk = hunk_val; │ │184 defer hunk.deinit(gpa); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │188 break :file_loop; │ │189 } │ │190 var lines: std.ArrayList([]const u8) = .empty; │ │190 - if (!file_header_emitted) { │ │191 - for (hunk_iter.header_lines.items) |hl| { │ │192 - try lines.append(arena, try arena.dupe(u8, hl)); │ │193 - } │ │194 - file_header_emitted = true; │ │195 - } │ │191 try appendHunkLines(arena, &lines, &hunk_iter, &hunk); │ │197 - try hunks.append(arena, .{ .lines = try lines.toOwnedSlice(arena) }); │ │192 + try hunks.append(arena, .{ │ │193 + .path = if (path_attached) null else try arena.dupe(u8, pair.path),│ │194 + .lines = try lines.toOwnedSlice(arena), │ │195 + }); │ │196 + path_attached = true; │ │197 } │ │198 } │ │199 │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │201 return .{ .hunks = try hunks.toOwnedSlice(arena), .shown_hunks = shown, .has_more = has_more };│ │202 } │ │203 │ │205 -// append a hunk's header and its edit lines │ │204 +// append a hunk's header and its edit lines, each prefixed with a right-aligned │ │205 +// line number in a column wide enough for the hunk's largest number, then a space │ │206 fn appendHunkLines( │ │207 arena: std.mem.Allocator, │ │208 lines: *std.ArrayList([]const u8), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │209 hunk_iter: *df.HunkIterator(.xit, .{}), │ │210 hunk: *df.Hunk(.xit, .{}), │ │211 ) !void { │ │212 - const o = hunk.offsets(); │ │213 - try lines.append(arena, try std.fmt.allocPrint(arena, "@@ -{d},{d} +{d},{d} @@", .{│ │214 - o.del_start, o.del_count, o.ins_start, o.ins_count, │ │215 - })); │ │212 + var max_num: usize = 1; │ │213 + for (hunk.edits.items) |edit| { │ │214 + const n = editLineNum(edit) + 1; │ │215 + if (n > max_num) max_num = n; │ │216 + } │ │217 + const width = std.fmt.count("{d}", .{max_num}); │ │218 + // a run of spaces sliced to each line's leading pad. │ │219 + const indent = try arena.alloc(u8, width); │ │220 + @memset(indent, ' '); │ │221 + │ │222 for (hunk.edits.items) |edit| { │ │223 const text = switch (edit) { │ │224 .eql => |e| try hunk_iter.line_iter_b.get(e.new_line.num), │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │234 .ins => '+', │ │235 .del => '-', │ │236 }; │ │231 - try lines.append(arena, try std.fmt.allocPrint(arena, "{c}{s}", .{ prefix, text }));│ │237 + const num_str = try std.fmt.allocPrint(arena, "{d}", .{editLineNum(edit) + 1}); │ │238 + try lines.append(arena, try std.fmt.allocPrint(arena, "{s}{s} {c}{s}", .{ │ │239 + indent[0 .. width - num_str.len], num_str, prefix, text, │ │240 + })); │ │241 } │ │242 } │ │243 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │244 +// the line number to show for an edit: the new-side number for kept and inserted│ │245 +// lines, the old-side number for deletions (0-based in the diff machinery). │ │246 +fn editLineNum(edit: df.Edit) usize { │ │247 + return switch (edit) { │ │248 + .eql => |e| e.new_line.num, │ │249 + .ins => |e| e.new_line.num, │ │250 + .del => |e| e.old_line.num, │ │251 + }; │ │252 +} │ │253 + │ │254 // "YYYY-MM-DD" for a unix timestamp. │ │255 fn formatDate(arena: std.mem.Allocator, timestamp: u64) ![]const u8 { │ │256 const epoch_secs = std.time.epoch.EpochSeconds{ .secs = timestamp }; │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │357 try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │358 } │ │359 │ │360 + // a focusable file-path label shown above the first hunk of each file. │ │361 + fn addPathBox(self: *View, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), path: []const u8) !void { │ │362 + _ = self; │ │363 + var tb = try wgt.TextBox(ui.Widget).init(allocator, path, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .none });│ │364 + errdefer tb.deinit(allocator); │ │365 + tb.getFocus().focusable = true; │ │366 + try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │367 + } │ │368 + │ │369 // the focusable "load more" row. it's a navigation link to this commit's │ │370 // route with a larger `after`, so activating it (the host follows the "a:" │ │371 // link) reloads the page showing more hunks — the same on the TUI and web. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯