╭ changed ──────╮ │src/test/ui.zig│ ╰───────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │200 .ref_or_oid_value = oid0, │ │201 .base_oid = base_oid, │ │202 .commits = &.{ │ │203 - .{ .oid = oid0, .date = "2024-01-01", .message = "first", .window = .{} }, │ │203 + .{ .oid = oid0, .date = "2024-01-01", .message = "first", .timestamp = "Jan 1, 2024, 00:00:00 UTC", .window = .{} },│ │204 }, │ │205 .next_start = next_oid, │ │206 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │386 } │ │387 } │ │388 const message = try arena.allocator().dupe(u8, msg_writer.written()); │ │389 - _ = try template_repo.commit(io, allocator, .{ .message = message, .timestamp = base_ts + c * std.time.s_per_day }); │ │389 + // the newest commit was committed by someone other than its │ │390 + // author, so the detail pane shows a committer box. │ │391 + const committer: ?[]const u8 = if (c == commit_count - 1) "alice <alice@example.test>" else null; │ │392 + _ = try template_repo.commit(io, allocator, .{ .message = message, .committer = committer, .timestamp = base_ts + c * std.time.s_per_day });│ │393 try addNextTags(&template_repo, io, allocator, &tag_num); │ │394 } │ │395 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────╮ │ 5 const xit = @import("xit"); │ │ 6 const rp = xit.repo; │ │ 7 const Diff = @import("Diff.zig"); │ │ 8 +const Undo = @import("Undo.zig"); │ │ 9 const obj = xit.object; │ │10 const srch_cmmt = @import("../../search_commit.zig");│ │11 const srch = @import("../../search.zig"); │ ╰─────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────╮ │32 // whether `message` is a shortened preview. │ │33 message_truncated: bool = false, │ │34 author: ui.Author = .unknown, │ │35 + // .unknown when the committer is the author.│ │36 + committer: ui.Author = .unknown, │ │37 + // the committer timestamp, human-readable. │ │38 + timestamp: []const u8, │ │39 stats: ?xit.patch.CommitStats = null, │ │40 window: Diff.Window = .{}, │ │41 }; │ ╰─────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │257 .message = text, │ │258 .message_truncated = truncated, │ │259 .author = try ui.Author.init(admin_moment, arena, md.author orelse ""), │ │260 + .committer = if (md.committer) |committer| │ │261 + (if (std.mem.eql(u8, identityOf(committer), identityOf(md.author orelse ""))) .unknown else try ui.Author.init(admin_moment, arena, committer))│ │262 + else │ │263 + .unknown, │ │264 + .timestamp = try Undo.formatTimestamp(aa, std.math.cast(i64, md.timestamp) orelse -1), │ │265 .stats = if (repo_kind == .xit) try repo.commitStats(io, gpa, .{ .oid = &commit_object.oid }) else null, │ │266 }; │ │267 +} │ │268 + │ │269 +// an author or committer line without its timestamp. │ │270 +fn identityOf(line: []const u8) []const u8 { │ │271 + const close_bracket = std.mem.indexOfScalar(u8, line, '>') orelse return line; │ │272 + return line[0 .. close_bracket + 1]; │ │273 } │ │274 │ │275 // an empty listing pinned to a ref, for the wasm / no-repo / unresolved paths. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │672 .diff => |d| { │ │673 if (d.path.len == 0) { │ │674 try self.addMessageBox(allocator, inner, commit, .preview); │ │659 - if (commit.author != .unknown) { │ │660 - var tb = try ui.authorBox(allocator, self.session.page_arena, commit.author); │ │675 + if (commit.author != .unknown or commit.committer != .unknown) { │ │676 + var row = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │677 + errdefer row.deinit(allocator); │ │678 + if (commit.author != .unknown) { │ │679 + var tb = try ui.authorBox(allocator, self.session.page_arena, commit.author); │ │680 + errdefer tb.deinit(allocator); │ │681 + try row.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │682 + } │ │683 + if (commit.committer != .unknown) { │ │684 + var tb = try ui.authorBox(allocator, self.session.page_arena, commit.committer); │ │685 + errdefer tb.deinit(allocator); │ │686 + tb.options.label = " committer "; │ │687 + try row.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │688 + } │ │689 + row.getFocus().child_id = row.children.keys()[0]; │ │690 + try inner.children.put(allocator, row.getFocus().id, .{ .widget = .{ .box = row }, .rect = null, .min_size = null }); │ │691 + } │ │692 + { │ │693 + var tb = try wgt.TextBox.init(allocator, commit.timestamp, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none, .label = " timestamp " });│ │694 errdefer tb.deinit(allocator); │ │695 + tb.getFocus().mode = .all; │ │696 try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │697 } │ │698 if (commit.stats) |stats| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │752 } │ │753 if (direction == .up and self.contentAtTop() and self.focusHeader(root_focus)) return; │ │754 if (self.diffActive()) { │ │755 + const diff_view = &self.diffOuter().children.values()[0].widget.diff_view; │ │756 if (key == .arrow_left and self.diffScroll().x == 0) { │ │722 - self.focusList(root_focus); │ │757 + if (!diff_view.moveInRow(root_focus, false)) self.focusList(root_focus); │ │758 } else { │ │724 - try self.diffOuter().children.values()[0].widget.diff_view.input(allocator, key, root_focus);│ │759 + try diff_view.input(allocator, key, root_focus); │ │760 } │ │761 } else { │ │762 try self.listInput(key, root_focus); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/ui/Repo/Diff.zig│ ╰────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │271 const sc = &self.scroll; │ │272 switch (key) { │ │273 // scroll horizontally within the diff │ │274 - .arrow_left => { │ │275 - if (sc.x > 0) { │ │276 - sc.x -= 1; │ │277 - self.scroll.clampToContent(); │ │278 - } │ │274 + .arrow_left => if (!self.moveInRow(root_focus, false) and sc.x > 0) {│ │275 + sc.x -= 1; │ │276 + self.scroll.clampToContent(); │ │277 }, │ │280 - .arrow_right => { │ │278 + .arrow_right => if (!self.moveInRow(root_focus, true)) { │ │279 sc.x += 1; │ │280 self.scroll.clampToContent(); │ │281 }, │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │293 }, │ │294 else => {}, │ │295 } │ │296 + } │ │297 + │ │298 + // step across the selected row's boxes, false when the row has none that way. │ │299 + pub fn moveInRow(self: *View, root_focus: *Focus, right: bool) bool { │ │300 + const box = self.inner(); │ │301 + const selected = box.getFocus().child_id orelse return false; │ │302 + return switch ((box.children.getPtr(selected) orelse return false).widget) {│ │303 + .box => |*row| ui.widget.moveInRow(row, root_focus, right), │ │304 + else => false, │ │305 + }; │ │306 } │ │307 │ │308 // move one step in `delta` (+ down, - up). in the terminal, `delta` is a │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │312 const box = self.inner(); │ │313 const keys = box.children.keys(); │ │314 if (keys.len == 0) return; │ │307 - const cur: usize = if (root_focus.grandchild_id) |g| (box.children.getIndex(g) orelse 0) else 0;│ │315 + const cur: usize = if (box.getFocus().child_id) |c| (box.children.getIndex(c) orelse 0) else 0; │ │316 const target = @as(isize, @intCast(cur)) + delta; │ │317 const in_range = target >= 0 and target < @as(isize, @intCast(keys.len)); │ │318 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯