Compare commits

...

3 Commits

Author SHA1 Message Date
9bfeea89d9 mod: read file from argument for diff contents
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m26s
For example you can run the following bash command:

```bash
tui-diff <(git diff)
```

With this command you create a temporary file containing
contents of the `git diff` command and is provided to
`tui-diff` to open and render.
2026-01-10 18:18:44 +01:00
b950deda6b mod(ci): update location of external setup-zig
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 58s
2026-01-08 23:26:44 +01:00
0b34a432d1 fix(lint): format using zig fmt
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 50s
2026-01-08 23:23:05 +01:00
4 changed files with 141 additions and 117 deletions

View File

@@ -15,7 +15,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup zig installation - name: Setup zig installation
uses: mlugg/setup-zig@v2 uses: https://codeberg.org/mlugg/setup-zig@v2
with: with:
version: master version: master
- name: Lint check - name: Lint check

View File

@@ -14,7 +14,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup zig installation - name: Setup zig installation
uses: mlugg/setup-zig@v2 uses: https://codeberg.org/mlugg/setup-zig@v2
with: with:
version: master version: master
- name: Lint check - name: Lint check

View File

@@ -83,7 +83,6 @@ pub fn Tree(App: type) type {
return .{ .x = width }; return .{ .x = width };
} }
fn handle(ctx: *anyopaque, model: *App.Model, event: App.Event) !void { fn handle(ctx: *anyopaque, model: *App.Model, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
@@ -93,15 +92,15 @@ pub fn Tree(App: type) type {
this.idx = 0; this.idx = 0;
}, },
.key => |key| { .key => |key| {
if (key.eql(.{ .cp = 'J' }) and this.idx < this.len - 1) { if (key.eql(.{ .cp = 'J' }) and this.idx < this.len - 1) {
this.idx += 1; this.idx += 1;
this.queue.push(.{ .file = this.idx }); this.queue.push(.{ .file = this.idx });
} }
if (key.eql(.{ .cp = 'K' }) and this.idx > 0) { if (key.eql(.{ .cp = 'K' }) and this.idx > 0) {
this.idx -= 1; this.idx -= 1;
this.queue.push(.{ .file = this.idx }); this.queue.push(.{ .file = this.idx });
} }
}, },
.mouse => |mouse| if (this.len > 0) switch (mouse.button) { .mouse => |mouse| if (this.len > 0) switch (mouse.button) {
.left => if (mouse.y + this.scrollback < this.len) { .left => if (mouse.y + this.scrollback < this.len) {

View File

@@ -3,106 +3,106 @@
// FIX known issues: // FIX known issues:
const diff = const diff =
\\diff --git a/build.zig.zon b/build.zig.zon \\diff --git a/build.zig.zon b/build.zig.zon
\\index a039487..944fc49 100644 \\index a039487..944fc49 100644
\\--- a/build.zig.zon \\--- a/build.zig.zon
\\+++ b/build.zig.zon \\+++ b/build.zig.zon
\\@@ -3,8 +3,8 @@ \\@@ -3,8 +3,8 @@
\\ .version = "0.0.1", \\ .version = "0.0.1",
\\ .dependencies = .{ \\ .dependencies = .{
\\ .zterm = .{ \\ .zterm = .{
\\- .url = "git+https://gitea.yves-biener.de/yves-biener/zterm#e972a2ea0f7a9f8caffd439ef206474b46475f91", \\- .url = "git+https://gitea.yves-biener.de/yves-biener/zterm#e972a2ea0f7a9f8caffd439ef206474b46475f91",
\\- .hash = "zterm-0.3.0-1xmmENkhHAB2rmNJFH-9rRqiRLnT673xwuMrqLwOnlT_", \\- .hash = "zterm-0.3.0-1xmmENkhHAB2rmNJFH-9rRqiRLnT673xwuMrqLwOnlT_",
\\+ .url = "git+https://gitea.yves-biener.de/yves-biener/zterm#b1a0d60ae379bb91b862d7c4a8a2210cd74c4387", \\+ .url = "git+https://gitea.yves-biener.de/yves-biener/zterm#b1a0d60ae379bb91b862d7c4a8a2210cd74c4387",
\\+ .hash = "zterm-0.3.0-1xmmEMotHACXUXNtsX1P2iz3XQgabNz1PfF7oazYtNam", \\+ .hash = "zterm-0.3.0-1xmmEMotHACXUXNtsX1P2iz3XQgabNz1PfF7oazYtNam",
\\ }, \\ },
\\ }, \\ },
\\ .minimum_zig_version = "0.16.0-dev.1254+bf15c791f", \\ .minimum_zig_version = "0.16.0-dev.1254+bf15c791f",
\\diff --git a/src/elements.zig b/src/elements.zig \\diff --git a/src/elements.zig b/src/elements.zig
\\index c0225ae..fb1ad68 100644 \\index c0225ae..fb1ad68 100644
\\--- a/src/elements.zig \\--- a/src/elements.zig
\\+++ b/src/elements.zig \\+++ b/src/elements.zig
\\@@ -59,6 +59,7 @@ pub fn Tree(App: type) type { \\@@ -59,6 +59,7 @@ pub fn Tree(App: type) type {
\\ .ptr = this, \\ .ptr = this,
\\ .vtable = &.{ \\ .vtable = &.{
\\ .resize = resize, \\ .resize = resize,
\\+ .minSize = minSize, \\+ .minSize = minSize,
\\ .handle = handle, \\ .handle = handle,
\\ .content = content, \\ .content = content,
\\ }, \\ },
\\@@ -70,6 +71,19 @@ pub fn Tree(App: type) type { \\@@ -70,6 +71,19 @@ pub fn Tree(App: type) type {
\\ this.size = size; \\ this.size = size;
\\ } \\ }
\\ \\
\\+ fn minSize(ctx: *anyopaque, model: *const App.Model, _: Point) Point { \\+ fn minSize(ctx: *anyopaque, model: *const App.Model, _: Point) Point {
\\+ // NOTE as we assume the model contents do not change we could calculate the \\+ // 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 \\+ // maximum width required for this `Element` and return that, instead of
\\+ // calculating the width every time anew. For now this works fine. \\+ // calculating the width every time anew. For now this works fine.
\\+ const this: *@This() = @ptrCast(@alignCast(ctx)); \\+ const this: *@This() = @ptrCast(@alignCast(ctx));
\\+ const changes: []const Model.Index = model.changes.keys(); \\+ const changes: []const Model.Index = model.changes.keys();
\\+ const files = changes[this.scrollback..]; \\+ const files = changes[this.scrollback..];
\\+ var width: u16 = 0; \\+ var width: u16 = 0;
\\+ for (files) |file| width = @max(@as(u16, @intCast(file.len)), width); \\+ for (files) |file| width = @max(@as(u16, @intCast(file.len)), width);
\\+ return .{ .x = width }; \\+ return .{ .x = width };
\\+ } \\+ }
\\+ \\+
\\+ \\+
\\ fn handle(ctx: *anyopaque, model: *App.Model, event: App.Event) !void { \\ fn handle(ctx: *anyopaque, model: *App.Model, event: App.Event) !void {
\\ const this: *@This() = @ptrCast(@alignCast(ctx)); \\ const this: *@This() = @ptrCast(@alignCast(ctx));
\\ switch (event) { \\ switch (event) {
\\@@ -78,7 +92,17 @@ pub fn Tree(App: type) type { \\@@ -78,7 +92,17 @@ pub fn Tree(App: type) type {
\\ this.scrollback = 0; \\ this.scrollback = 0;
\\ this.idx = 0; \\ this.idx = 0;
\\ }, \\ },
\\- // TODO also support key inputs to change the current file? \\- // TODO also support key inputs to change the current file?
\\+ .key => |key| { \\+ .key => |key| {
\\+ if (key.eql(.{ .cp = 'J' }) and this.idx < this.len - 1) { \\+ if (key.eql(.{ .cp = 'J' }) and this.idx < this.len - 1) {
\\+ this.idx += 1; \\+ this.idx += 1;
\\+ this.queue.push(.{ .file = this.idx }); \\+ this.queue.push(.{ .file = this.idx });
\\+ } \\+ }
\\+ \\+
\\+ if (key.eql(.{ .cp = 'K' }) and this.idx > 0) { \\+ if (key.eql(.{ .cp = 'K' }) and this.idx > 0) {
\\+ this.idx -= 1; \\+ this.idx -= 1;
\\+ this.queue.push(.{ .file = this.idx }); \\+ this.queue.push(.{ .file = this.idx });
\\+ } \\+ }
\\+ }, \\+ },
\\ .mouse => |mouse| if (this.len > 0) switch (mouse.button) { \\ .mouse => |mouse| if (this.len > 0) switch (mouse.button) {
\\ .left => if (mouse.y + this.scrollback < this.len) { \\ .left => if (mouse.y + this.scrollback < this.len) {
\\ this.idx = mouse.y + this.scrollback; \\ this.idx = mouse.y + this.scrollback;
\\@@ -112,7 +136,6 @@ pub fn Tree(App: type) type { \\@@ -112,7 +136,6 @@ pub fn Tree(App: type) type {
\\ const row_color: zterm.Color = if (this.idx == idx) .blue else .default; \\ const row_color: zterm.Color = if (this.idx == idx) .blue else .default;
\\ cell_idx = row * size.x; \\ cell_idx = row * size.x;
\\ for (0..size.x) |_| { \\ for (0..size.x) |_| {
\\- cell_idx += 1; \\- cell_idx += 1;
\\ if (value_idx >= value.len) break; \\ if (value_idx >= value.len) break;
\\ const cp = value[value_idx]; \\ const cp = value[value_idx];
\\ defer value_idx += 1; \\ defer value_idx += 1;
\\@@ -128,6 +151,7 @@ pub fn Tree(App: type) type { \\@@ -128,6 +151,7 @@ pub fn Tree(App: type) type {
\\ else => cp, \\ else => cp,
\\ }; \\ };
\\ cells[cell_idx].style.fg = row_color; \\ cells[cell_idx].style.fg = row_color;
\\+ cell_idx += 1; \\+ cell_idx += 1;
\\ } \\ }
\\ } \\ }
\\ } \\ }
\\diff --git a/src/root.zig b/src/root.zig \\diff --git a/src/root.zig b/src/root.zig
\\index 29aa832..54d3631 100644 \\index 29aa832..54d3631 100644
\\--- a/src/root.zig \\--- a/src/root.zig
\\+++ b/src/root.zig \\+++ b/src/root.zig
\\@@ -17,7 +17,11 @@ pub fn Container(App: type, gpa: Allocator, element: *elements.Root(App), tree: \\@@ -17,7 +17,11 @@ pub fn Container(App: type, gpa: Allocator, element: *elements.Root(App), tree:
\\ }, \\ },
\\ }, \\ },
\\ }, element.element()); \\ }, element.element());
\\- try root.append(try .init(gpa, .{}, tree.element())); \\- try root.append(try .init(gpa, .{}, tree.element()));
\\+ try root.append(try .init(gpa, .{ \\+ try root.append(try .init(gpa, .{
\\+ .size = .{ \\+ .size = .{
\\+ .grow = .vertical, \\+ .grow = .vertical,
\\+ }, \\+ },
\\+ }, tree.element())); \\+ }, tree.element()));
\\ try root.append(try .init(gpa, .{}, .{})); // empty container holding the scrollable element for the diff of each file \\ try root.append(try .init(gpa, .{}, .{})); // empty container holding the scrollable element for the diff of each file
\\ return root; \\ return root;
\\ } \\ }
\\ \\
; ;
pub fn main() !void { pub fn main() !void {
@@ -120,6 +120,13 @@ pub fn main() !void {
else => area.allocator(), else => area.allocator(),
}; };
var threaded_io: std.Io.Threaded = .init(allocator, .{});
errdefer threaded_io.deinit();
const io = threaded_io.io();
var diff_input: [:0]u8 = undefined;
defer allocator.free(diff_input);
// argument handling // argument handling
{ {
var arg_it = try std.process.argsWithAllocator(allocator); var arg_it = try std.process.argsWithAllocator(allocator);
@@ -127,21 +134,39 @@ pub fn main() !void {
// TODO may there be other options? // TODO may there be other options?
// usage: tui-diff // usage: tui-diff
// skip own executable name // skip own executable name
_ = arg_it.skip(); _ = arg_it.skip();
// only handle the first argument otherwise ignore!
if (arg_it.next()) |file| {
var buffer: [4096]u8 = undefined;
var stream = std.Io.File.readerStreaming(
try std.Io.Dir.openFileAbsolute(
io,
file,
.{ .mode = .read_only },
),
io,
&buffer,
);
const reader = &stream.interface;
diff_input = try reader.allocRemainingAlignedSentinel(
allocator,
.unlimited,
.of(u8),
0,
);
} else {
// TODO detect VCS in the current working directory (and traversing upwards if none found at point?)
}
} }
// tui creation // tui creation
errdefer |err| log.err("Application Error: {any}", .{err}); errdefer |err| log.err("Application Error: {any}", .{err});
var threaded_io: std.Io.Threaded = .init(allocator, .{});
errdefer threaded_io.deinit();
const io = threaded_io.io();
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
var app: App = .init(io, try .init(allocator, diff)); var app: App = .init(io, try .init(allocator, diff_input));
defer app.model.deinit(allocator); defer app.model.deinit(allocator);
var element_root: tui_diff.elements.Root(App) = .init(allocator); var element_root: tui_diff.elements.Root(App) = .init(allocator);