//! Applications `Element` implementations to be used in *zterm* `Container`'s pub fn Root(App: type) type { return struct { gpa: Allocator, diffs: std.ArrayListUnmanaged(App.Scrollable) = .empty, container: *App.Container = undefined, pub fn init(gpa: Allocator) @This() { return .{ .gpa = gpa }; } fn deinit(ctx: *anyopaque) void { const this: *@This() = @ptrCast(@alignCast(ctx)); for (this.diffs.items) |*scrollable| scrollable.element().deinit(); this.diffs.deinit(this.gpa); // the container is the root container, which is calling this function, do not deinitialize it! } pub fn element(this: *@This()) App.Element { return .{ .ptr = this, .vtable = &.{ .deinit = deinit, .handle = handle, }, }; } fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { // NOTE assume that there is at least one diff .init => this.container.elements.items[this.container.elements.items.len - 1].element = this.diffs.items[0].element(), .file => |idx| this.container.elements.items[this.container.elements.items.len - 1].element = this.diffs.items[idx].element(), .quit => this.container.elements.items[this.container.elements.items.len - 1].element = .{}, else => {}, } } }; } // NOTE base contents from the model! // -> assume that the model does not change! it is created initially and then never changed! pub fn Tree(App: type) type { return struct { scrollback: usize = 0, // scrollback index for the changed files idx: usize = 0, // index for the currently selected entry len: usize = 0, // total amount of changed files size: Point = .{}, // current available size for the `Element` queue: *App.Queue, pub fn init(queue: *App.Queue) @This() { return .{ .queue = queue }; } pub fn element(this: *@This()) App.Element { return .{ .ptr = this, .vtable = &.{ .resize = resize, .minSize = minSize, .handle = handle, .content = content, }, }; } fn resize(ctx: *anyopaque, _: *const App.Model, size: Point) void { const this: *@This() = @ptrCast(@alignCast(ctx)); this.size = size; } fn minSize(ctx: *anyopaque, model: *const App.Model, _: Point) Point { // NOTE as we assume the model contents do not change we could calculate the // maximum width required for this `Element` and return that, instead of // calculating the width every time anew. For now this works fine. const this: *@This() = @ptrCast(@alignCast(ctx)); const changes: []const Model.Index = model.changes.keys(); const files = changes[this.scrollback..]; var width: u16 = 0; for (files) |file| width = @max(@as(u16, @intCast(file.len)), width); return .{ .x = width }; } fn handle(ctx: *anyopaque, model: *App.Model, event: App.Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { .init => { this.len = model.changes.keys().len; this.scrollback = 0; this.idx = 0; }, .key => |key| { if (key.eql(.{ .cp = 'J' }) and this.idx < this.len - 1) { this.idx += 1; this.queue.push(.{ .file = this.idx }); } if (key.eql(.{ .cp = 'K' }) and this.idx > 0) { this.idx -= 1; this.queue.push(.{ .file = this.idx }); } }, .mouse => |mouse| if (this.len > 0) switch (mouse.button) { .left => if (mouse.y + this.scrollback < this.len) { this.idx = mouse.y + this.scrollback; this.queue.push(.{ .file = this.idx }); }, .wheel_up => this.scrollback -|= 3, .wheel_down => this.scrollback = @min(this.len - 1, this.scrollback + 3), else => {}, }, else => {}, } } fn content(ctx: *anyopaque, model: *const App.Model, cells: []Cell, size: Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); if (this.len == 0) return; var cell_idx: usize = 0; const changes: []const Model.Index = model.changes.keys(); const files = changes[this.scrollback..]; for (0..size.y) |row| { if (row >= files.len) break; const idx = this.scrollback + row; const file = files[row]; const value = model.content[file.idx .. file.idx + file.len]; var value_idx: usize = 0; if (value_idx >= value.len) break; const row_color: zterm.Color = if (this.idx == idx) .blue else .default; cell_idx = row * size.x; for (0..size.x) |_| { if (value_idx >= value.len) break; const cp = value[value_idx]; defer value_idx += 1; if (cell_idx >= cells.len) break; switch (cp) { '\n' => cell_idx += 1, '\t' => cell_idx += 2, else => {}, } cells[cell_idx].cp = switch (cp) { '\n', '\t' => continue, else => cp, }; cells[cell_idx].style.fg = row_color; cell_idx += 1; } } } }; } /// Individual `Change` associated to a parsed diff header. pub fn Change(App: type) type { return struct { change: Model.Index, // how does an individual `Change` knows it relation to the `Model` containing it's contents? pub fn init(change: Model.Index) @This() { return .{ .change = change }; } pub fn element(this: *@This()) App.Element { return .{ .ptr = this, .vtable = &.{ .minSize = minSize, .content = content, }, }; } fn minSize(ctx: *anyopaque, model: *const App.Model, size: Point) Point { const this: *@This() = @ptrCast(@alignCast(ctx)); // TODO handle `\n`, '\t', etc. correctly in the fill for the `cells` switch (model.render_mode) { .side_by_side => {}, .stacked => {}, } var y: u16 = 0; const value = model.content[this.change.idx .. this.change.idx + this.change.len]; if (this.change.idx + this.change.len == model.content.len) y += 1; for (value) |cp| if (cp == '\n') { y += 1; }; return .{ .x = size.x, .y = y }; } fn content(ctx: *anyopaque, model: *const App.Model, cells: []Cell, size: Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); // TODO render different modes accordingly var cell_idx: usize = 0; const value = model.content[this.change.idx .. this.change.idx + this.change.len]; var value_idx: usize = 0; switch (model.render_mode) { .side_by_side => for (0..size.y) |row| { if (value_idx >= value.len) break; const row_color: zterm.Color = switch (value[value_idx]) { '+' => .green, '-' => .red, '@' => .blue, else => .default, }; cell_idx = row * size.x; for (0..size.x) |_| { cell_idx += 1; if (value_idx >= value.len) break; const cp = value[value_idx]; defer value_idx += 1; if (cell_idx >= cells.len) break; switch (cp) { '\n' => {}, '\t' => cell_idx += 2, else => {}, } cells[cell_idx].cp = switch (cp) { '\n' => break, '\t' => continue, else => cp, }; cells[cell_idx].style.fg = row_color; } }, .stacked => for (0..size.y) |row| { if (value_idx >= value.len) break; const row_color: zterm.Color = switch (value[value_idx]) { '+' => .green, '-' => .red, '@' => .blue, else => .default, }; cell_idx = row * size.x; for (0..size.x) |_| { cell_idx += 1; if (value_idx >= value.len) break; const cp = value[value_idx]; defer value_idx += 1; if (cell_idx >= cells.len) break; switch (cp) { '\n' => {}, '\t' => cell_idx += 2, else => {}, } cells[cell_idx].cp = switch (cp) { '\n' => break, '\t' => continue, else => cp, }; cells[cell_idx].style.fg = row_color; } }, } } }; } /// `Diff` describes a collection of `Change`'s (i.e. through a root `Container` inside an `App.Scrollable`) pub fn Diff(App: type, gpa: Allocator, changes: []Change(App)) !App.Scrollable { var diff: App.Container = try .init(gpa, .{ .layout = .{ .direction = .vertical, .separator = .{ .enabled = true, }, }, }, .{}); for (changes) |*change| try diff.append(try .init(gpa, .{}, change.element())); return .init(diff, .disabled); } var random_alg = std.Random.DefaultCsprng.init(@splat(1)); var random = random_alg.random(); const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const zterm = @import("zterm"); const Model = @import("model.zig"); const Cell = zterm.Cell; const Point = zterm.Point;