Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 50s
293 lines
11 KiB
Zig
293 lines
11 KiB
Zig
//! 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;
|