+1
-1
README.md
+1
-1
README.md
+8
-6
build.zig
+8
-6
build.zig
···
2
2
const builtin = @import("builtin");
3
3
4
4
///Must match the `version` in `build.zig.zon`.
5
-
const version = std.SemanticVersion{ .major = 1, .minor = 3, .patch = 0 };
5
+
const version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 0 };
6
6
7
7
const targets: []const std.Target.Query = &.{
8
8
.{ .cpu_arch = .aarch64, .os_tag = .macos },
···
20
20
) !*std.Build.Step.Compile {
21
21
const libvaxis = b.dependency("vaxis", .{ .target = target, .optimize = optimize }).module("vaxis");
22
22
const fuzzig = b.dependency("fuzzig", .{ .target = target, .optimize = optimize }).module("fuzzig");
23
-
const zuid = b.dependency("zuid", .{ .target = target, .optimize = optimize }).module("zuid");
24
23
const zeit = b.dependency("zeit", .{ .target = target, .optimize = optimize }).module("zeit");
24
+
const zuid = b.dependency("zuid", .{ .target = target, .optimize = optimize }).module("zuid");
25
25
26
26
const exe = b.addExecutable(.{
27
27
.name = exe_name,
28
-
.root_source_file = b.path("src/main.zig"),
29
-
.target = target,
30
-
.optimize = optimize,
28
+
.root_module = b.createModule(.{
29
+
.root_source_file = b.path("src/main.zig"),
30
+
.target = target,
31
+
.optimize = optimize,
32
+
}),
31
33
});
32
34
33
35
exe.root_module.addImport("options", build_options);
34
36
exe.root_module.addImport("vaxis", libvaxis);
35
37
exe.root_module.addImport("fuzzig", fuzzig);
36
-
exe.root_module.addImport("zuid", zuid);
37
38
exe.root_module.addImport("zeit", zeit);
39
+
exe.root_module.addImport("zuid", zuid);
38
40
39
41
return exe;
40
42
}
+17
-10
build.zig.zon
+17
-10
build.zig.zon
···
2
2
.name = .jido,
3
3
.fingerprint = 0xee45eabe36cafb57,
4
4
.version = "1.3.0",
5
-
.minimum_zig_version = "0.14.0",
5
+
.minimum_zig_version = "0.15.2",
6
6
7
7
.dependencies = .{
8
+
// Replace with rockorager/libvaxis once https://github.com/rockorager/libvaxis/pull/293 is merged
8
9
.vaxis = .{
9
-
.url = "git+https://github.com/rockorager/libvaxis#1e24e0dfb509e974e1c8713bcd119d0ae032a8c7",
10
-
.hash = "vaxis-0.1.0-BWNV_MHyCAARemSCSwwc3sA1etNgv7ge0BCIXspX6CZv",
10
+
.url = "git+https://github.com/rob9315/libvaxis.git#8d04cffd9137b4a8c56b356de98b32023ae752f3",
11
+
.hash = "vaxis-0.5.1-BWNV_OA-CQDeFBHIx9ryyASogr2GE3FsAm-l5Ii5-HZT",
11
12
},
12
13
.fuzzig = .{
13
-
.url = "git+https://github.com/fjebaker/fuzzig#44c04733c7c0fee3db83672aaaaf4ed03e943156",
14
-
.hash = "fuzzig-0.1.1-AAAAALNIAQBmbHr-MPalGuR393Vem2pTQXI7_LXeNJgX",
14
+
.url = "git+https://github.com/fjebaker/fuzzig#4251fe4230d38e721514394a485db62ee1667ff3",
15
+
.hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D",
16
+
},
17
+
.zeit = .{
18
+
.url = "git+https://github.com/rockorager/zeit#7ac64d72dbfb1a4ad549102e7d4e232a687d32d8",
19
+
.hash = "zeit-0.6.0-5I6bk36tAgATpSl9wjFmRPMqYN2Mn0JQHgIcRNcqDpJA",
15
20
},
21
+
// Replace with KeithBrown39423/zuid once https://github.com/KeithBrown39423/zuid/pull/4 is merged
16
22
.zuid = .{
17
-
.url = "git+https://github.com/KeithBrown39423/zuid#b6129f6cee45bd90b7ac97b8839dc28d21bedcb2",
18
-
.hash = "zuid-2.0.0-AAAAADxXAAA4MAzwwRhfZ9AC2FMPZ8hUrZbfpmJ_azpK",
23
+
.url = "https://github.com/BrookJeynes/zuid/archive/refs/heads/bj/2025-12-31/feat/0.15.1.tar.gz",
24
+
.hash = "zuid-3.0.0-l7aPyUlXAAAk9BLSDm2roA3i78Sy6_GvQI4hwe0PHI_m",
19
25
},
20
-
.zeit = .{
21
-
.url = "git+https://github.com/rockorager/zeit/#175cf91a641790799e9d676878a9fe814aaed134",
22
-
.hash = "zeit-0.6.0-5I6bk5daAgC-P60TjxRqW0bYknfCGxJp-03eS9UjGrO7",
26
+
// Replace with zigimg/zigimg once https://github.com/zigimg/zigimg/pull/305 is merged
27
+
.zigimg = .{
28
+
.url = "git+https://github.com/brookjeynes/zigimg.git#9714df09f76891323c7fdbbbf23a17b79024fffb",
29
+
.hash = "zigimg-0.1.0-8_eo2j4mFwCU7tWnqvkYtzqe-OPRn_bxEql_IJhW85LT",
23
30
},
24
31
},
25
32
+15
-12
src/app.zig
+15
-12
src/app.zig
···
74
74
75
75
pub const Event = union(enum) {
76
76
image_ready,
77
+
notification,
77
78
key_press: Key,
78
79
winsize: vaxis.Winsize,
79
80
};
···
82
83
const Status = enum {
83
84
ready,
84
85
processing,
86
+
failed,
85
87
};
86
88
87
89
///Only use on first transmission. Subsequent draws should use
···
91
93
path: ?[]const u8 = null,
92
94
status: Status = .processing,
93
95
94
-
pub fn deinit(self: @This(), alloc: std.mem.Allocator) void {
96
+
pub fn deinit(self: @This(), alloc: std.mem.Allocator, vx: vaxis.Vaxis, tty: *vaxis.Tty) void {
97
+
if (self.image) |image| {
98
+
vx.freeImage(tty.writer(), image.id);
99
+
}
95
100
if (self.data) |data| {
96
101
var d = data;
97
-
d.deinit();
102
+
d.deinit(alloc);
98
103
}
99
104
if (self.path) |path| alloc.free(path);
100
105
}
···
108
113
alloc: std.mem.Allocator,
109
114
should_quit: bool,
110
115
vx: vaxis.Vaxis = undefined,
116
+
tty_buffer: [1024]u8 = undefined,
111
117
tty: vaxis.Tty = undefined,
112
118
loop: vaxis.Loop(Event) = undefined,
113
119
state: State = .normal,
···
149
155
.alloc = alloc,
150
156
.should_quit = false,
151
157
.vx = vx,
152
-
.tty = try vaxis.Tty.init(),
153
158
.directories = try Directories.init(alloc, entry_dir),
154
159
.help_menu = help_menu,
155
-
.text_input = vaxis.widgets.TextInput.init(alloc, &vx.unicode),
160
+
.text_input = vaxis.widgets.TextInput.init(alloc),
156
161
.actions = CircStack(Action, actions_len).init(),
157
162
.last_known_height = vx.window().height,
158
163
.images = .{ .cache = .init(alloc) },
159
164
};
160
-
165
+
app.tty = try vaxis.Tty.init(&app.tty_buffer);
161
166
app.loop = vaxis.Loop(Event){
162
167
.vaxis = &app.vx,
163
168
.tty = &app.tty,
···
191
196
self.help_menu.deinit();
192
197
self.directories.deinit();
193
198
self.text_input.deinit();
194
-
self.vx.deinit(self.alloc, self.tty.anyWriter());
199
+
self.vx.deinit(self.alloc, self.tty.writer());
195
200
self.tty.deinit();
196
201
if (self.file_logger) |file_logger| file_logger.deinit();
197
202
198
203
var image_iter = self.images.cache.iterator();
199
204
while (image_iter.next()) |img| {
200
-
img.value_ptr.deinit(self.alloc);
205
+
img.value_ptr.deinit(self.alloc, self.vx, &self.tty);
201
206
}
202
207
self.images.cache.deinit();
203
208
}
···
222
227
try self.loop.start();
223
228
defer self.loop.stop();
224
229
225
-
try self.vx.enterAltScreen(self.tty.anyWriter());
226
-
try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s);
230
+
try self.vx.enterAltScreen(self.tty.writer());
231
+
try self.vx.queryTerminal(self.tty.writer(), 1 * std.time.ns_per_s);
227
232
self.vx.caps.kitty_graphics = true;
228
233
229
234
while (!self.should_quit) {
···
248
253
249
254
try self.drawer.draw(self);
250
255
251
-
var buffered = self.tty.bufferedWriter();
252
-
try self.vx.render(buffered.writer().any());
253
-
try buffered.flush();
256
+
try self.vx.render(self.tty.writer());
254
257
}
255
258
256
259
if (config.empty_trash_on_exit) {
+62
-16
src/drawer.zig
+62
-16
src/drawer.zig
···
208
208
break :file;
209
209
}
210
210
211
+
if (cache_entry.status == .failed) {
212
+
_ = preview_win.print(&.{
213
+
.{ .text = "Failed to process image." },
214
+
}, .{});
215
+
break :file;
216
+
}
217
+
211
218
if (cache_entry.image) |img| {
212
219
img.draw(preview_win, .{ .scale = .contain }) catch |err| {
213
220
const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err});
···
224
231
} else {
225
232
if (cache_entry.data == null) {
226
233
const path = try app.alloc.dupe(u8, self.current_item_path);
227
-
processImage(app, path) catch break :unsupported;
234
+
processImage(app, path) catch {
235
+
app.alloc.free(path);
236
+
break :unsupported;
237
+
};
238
+
_ = preview_win.print(&.{
239
+
.{ .text = "Image still processing." },
240
+
}, .{});
241
+
break :file;
228
242
}
229
243
230
-
if (app.vx.transmitImage(app.alloc, app.tty.anyWriter(), &cache_entry.data.?, .rgba)) |img| {
244
+
if (app.vx.transmitImage(app.alloc, app.tty.writer(), &cache_entry.data.?, .rgba)) |img| {
231
245
img.draw(preview_win, .{ .scale = .contain }) catch |err| {
232
246
const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err});
233
247
defer app.alloc.free(message);
···
240
254
break :file;
241
255
};
242
256
cache_entry.image = img;
243
-
cache_entry.data.?.deinit();
257
+
if (cache_entry.data) |data| {
258
+
var d = data;
259
+
d.deinit(app.alloc);
260
+
}
244
261
cache_entry.data = null;
245
262
} else |_| {
246
263
break :unsupported;
···
249
266
250
267
break :file;
251
268
} else {
269
+
_ = preview_win.print(&.{
270
+
.{ .text = "Processing image." },
271
+
}, .{});
272
+
252
273
const path = try app.alloc.dupe(u8, self.current_item_path);
253
-
processImage(app, path) catch break :unsupported;
274
+
processImage(app, path) catch {
275
+
app.alloc.free(path);
276
+
break :unsupported;
277
+
};
254
278
}
255
279
256
280
break :file;
···
350
374
351
375
// Time created / last modified
352
376
if (self.verbose) lbl: {
353
-
var maybe_meta: ?std.fs.File.Metadata = null;
377
+
var maybe_meta: ?std.fs.File.Stat = null;
354
378
if (entry.kind == .directory) {
355
-
maybe_meta = directories.dir.metadata() catch break :lbl;
379
+
maybe_meta = directories.dir.stat() catch break :lbl;
356
380
} else if (entry.kind == .file) {
357
381
var file = directories.dir.openFile(entry.name, .{}) catch break :lbl;
358
-
maybe_meta = file.metadata() catch break :lbl;
382
+
maybe_meta = file.stat() catch break :lbl;
359
383
}
360
384
361
385
const meta = maybe_meta orelse break :lbl;
···
365
389
defer local.deinit();
366
390
367
391
const ctime_instant = zeit.instant(.{
368
-
.source = .{ .unix_nano = meta.created().? },
392
+
.source = .{ .unix_nano = meta.ctime },
369
393
.timezone = &local,
370
394
}) catch break :lbl;
371
395
const ctime = ctime_instant.time();
372
396
ctime.strftime(fbs.writer().any(), "Created: %Y-%m-%d %H:%M:%S\n") catch break :lbl;
373
397
374
398
const mtime_instant = zeit.instant(.{
375
-
.source = .{ .unix_nano = meta.modified() },
399
+
.source = .{ .unix_nano = meta.mtime },
376
400
.timezone = &local,
377
401
}) catch break :lbl;
378
402
const mtime = mtime_instant.time();
···
441
465
442
466
break :lbl 0;
443
467
};
444
-
if (size) |s| try fbs.writer().print("{s}{:.2}\n", .{
468
+
if (size) |s| try fbs.writer().print("{s}{B:.2}\n", .{
445
469
if (self.verbose) "Size: " else "",
446
-
std.fmt.fmtIntSizeDec(s),
470
+
s,
447
471
});
448
472
449
473
// Extension.
···
640
664
const load_img_thread = std.Thread.spawn(.{}, loadImage, .{
641
665
app,
642
666
path,
643
-
}) catch return error.Unsupported;
667
+
}) catch {
668
+
app.images.mutex.lock();
669
+
if (app.images.cache.getPtr(path)) |entry| {
670
+
entry.status = .failed;
671
+
}
672
+
app.images.mutex.unlock();
673
+
674
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to spawn processing thread.", .{path});
675
+
defer app.alloc.free(message);
676
+
app.notification.write(message, .err) catch {};
677
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
678
+
679
+
return error.Unsupported;
680
+
};
644
681
load_img_thread.detach();
645
682
}
646
683
647
-
fn loadImage(app: *App, path: []const u8) error{ Unsupported, OutOfMemory }!void {
648
-
const data = vaxis.zigimg.Image.fromFilePath(app.alloc, path) catch {
684
+
fn loadImage(app: *App, path: []const u8) error{OutOfMemory}!void {
685
+
var buf: [(1024 * 1024) * 5]u8 = undefined; // 5mb
686
+
const data = vaxis.zigimg.Image.fromFilePath(app.alloc, path, &buf) catch {
687
+
app.images.mutex.lock();
688
+
if (app.images.cache.getPtr(path)) |entry| {
689
+
entry.status = .failed;
690
+
}
691
+
app.images.mutex.unlock();
692
+
649
693
const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to read image from path.", .{path});
650
694
defer app.alloc.free(message);
651
695
app.notification.write(message, .err) catch {};
652
696
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
653
-
return error.Unsupported;
697
+
698
+
return;
654
699
};
655
700
656
701
app.images.mutex.lock();
657
702
if (app.images.cache.getPtr(path)) |entry| {
658
703
entry.status = .ready;
659
704
entry.data = data;
705
+
entry.path = path;
660
706
} else {
661
707
const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to add image to cache.", .{path});
662
708
defer app.alloc.free(message);
663
709
app.notification.write(message, .err) catch {};
664
710
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
665
-
return error.Unsupported;
711
+
return;
666
712
}
667
713
app.images.mutex.unlock();
668
714
+1
-1
src/environment.zig
+1
-1
src/environment.zig
+6
-3
src/event_handlers.zig
+6
-3
src/event_handlers.zig
···
133
133
}
134
134
},
135
135
.image_ready => {},
136
-
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
136
+
.notification => {},
137
+
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
137
138
}
138
139
}
139
140
···
283
284
}
284
285
},
285
286
.image_ready => {},
286
-
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
287
+
.notification => {},
288
+
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
287
289
}
288
290
}
289
291
···
298
300
}
299
301
},
300
302
.image_ready => {},
301
-
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
303
+
.notification => {},
304
+
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
302
305
}
303
306
}
+5
-5
src/events.zig
+5
-5
src/events.zig
···
49
49
return;
50
50
}
51
51
52
-
const tmp_path = try std.fmt.allocPrint(app.alloc, "{s}/{s}-{s}", .{ trash_dir_path, entry.name, zuid.new.v4() });
52
+
const tmp_path = try std.fmt.allocPrint(app.alloc, "{s}/{s}-{f}", .{ trash_dir_path, entry.name, zuid.new.v4() });
53
53
if (app.directories.dir.rename(entry.name, tmp_path)) {
54
54
if (app.actions.push(.{
55
55
.delete = .{ .prev_path = prev_path_alloc, .new_path = tmp_path },
···
419
419
},
420
420
.file => {
421
421
if (environment.getEditor()) |editor| {
422
-
try app.vx.exitAltScreen(app.tty.anyWriter());
423
-
try app.vx.resetState(app.tty.anyWriter());
422
+
try app.vx.exitAltScreen(app.tty.writer());
423
+
try app.vx.resetState(app.tty.writer());
424
424
app.loop.stop();
425
425
426
426
environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch |err| {
···
430
430
};
431
431
432
432
try app.loop.start();
433
-
try app.vx.enterAltScreen(app.tty.anyWriter());
434
-
try app.vx.enableDetectedFeatures(app.tty.anyWriter());
433
+
try app.vx.enterAltScreen(app.tty.writer());
434
+
try app.vx.enableDetectedFeatures(app.tty.writer());
435
435
app.vx.queueRefresh();
436
436
} else {
437
437
app.notification.write("Can not open file - $EDITOR not set.", .warn) catch {};
+13
-15
src/file_logger.zig
+13
-15
src/file_logger.zig
···
24
24
file: ?std.fs.File,
25
25
26
26
pub fn init(dir: std.fs.Dir) FileLogger {
27
-
var file: ?std.fs.File = null;
28
-
if (!environment.fileExists(dir, LOG_PATH)) {
29
-
file = dir.createFile(LOG_PATH, .{}) catch lbl: {
30
-
std.log.err("Failed to create log file.", .{});
31
-
break :lbl null;
32
-
};
33
-
} else {
34
-
file = dir.openFile(LOG_PATH, .{ .mode = .write_only }) catch lbl: {
35
-
std.log.err("Failed to open log file.", .{});
36
-
break :lbl null;
37
-
};
38
-
}
27
+
const file = dir.createFile(LOG_PATH, .{ .truncate = false, .read = true }) catch |err| {
28
+
std.log.err("Failed to create/open log file: {s}", .{@errorName(err)});
29
+
return .{ .dir = dir, .file = null };
30
+
};
39
31
40
32
return .{ .dir = dir, .file = file };
41
33
}
···
49
41
50
42
pub fn write(self: FileLogger, msg: []const u8, level: LogLevel) !void {
51
43
const file = if (self.file) |file| file else return error.NoLogFile;
52
-
if (try file.tryLock(std.fs.File.Lock.shared)) {
44
+
45
+
if (try file.tryLock(.exclusive)) {
53
46
defer file.unlock();
54
-
try file.seekFromEnd(0);
55
47
56
-
try file.writer().print(
48
+
var buffer: [1024]u8 = undefined;
49
+
var file_writer_impl = file.writer(&buffer);
50
+
const file_writer = &file_writer_impl.interface;
51
+
try file_writer_impl.seekTo(file.getEndPos() catch 0);
52
+
53
+
try file_writer.print(
57
54
"({d}) {s}: {s}\n",
58
55
.{ std.time.timestamp(), LogLevel.toString(level), msg },
59
56
);
57
+
try file_writer.flush();
60
58
}
61
59
}
+4
-4
src/list.zig
+4
-4
src/list.zig
···
12
12
pub fn init(alloc: std.mem.Allocator) Self {
13
13
return Self{
14
14
.alloc = alloc,
15
-
.items = std.ArrayList(T).init(alloc),
15
+
.items = .empty,
16
16
.selected = 0,
17
17
};
18
18
}
19
19
20
20
pub fn deinit(self: *Self) void {
21
-
self.items.deinit();
21
+
self.items.deinit(self.alloc);
22
22
}
23
23
24
24
pub fn append(self: *Self, item: T) !void {
25
-
try self.items.append(item);
25
+
try self.items.append(self.alloc, item);
26
26
}
27
27
28
28
pub fn clear(self: *Self) void {
29
-
self.items.clearAndFree();
29
+
self.items.clearAndFree(self.alloc);
30
30
self.selected = 0;
31
31
}
32
32
+11
-3
src/main.zig
+11
-3
src/main.zig
···
104
104
}
105
105
106
106
if (opts.version) {
107
-
std.debug.print("jido v{}\n", .{options.version});
107
+
std.debug.print("jido v{f}\n", .{options.version});
108
108
return;
109
109
}
110
110
···
139
139
},
140
140
};
141
141
142
-
app.file_logger = if (config.config_dir) |dir| FileLogger.init(dir) else null;
142
+
app.file_logger = if (config.config_dir) |dir| FileLogger.init(dir) else logger: {
143
+
std.log.err("Failed to initialise file logger - no config directory found", .{});
144
+
break :logger null;
145
+
};
146
+
app.notification.loop = &app.loop;
143
147
144
148
try app.run();
145
149
···
151
155
// Must be printed after app has deinit as part of that process clears
152
156
// the screen.
153
157
if (last_dir) |path| {
154
-
const stdout = std.io.getStdOut().writer();
158
+
var stdout_buffer: [std.fs.max_path_bytes]u8 = undefined;
159
+
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
160
+
const stdout = &stdout_writer.interface;
155
161
stdout.print("{s}\n", .{path}) catch {};
162
+
stdout.flush() catch {};
163
+
156
164
alloc.free(path);
157
165
}
158
166
}
+8
src/notification.zig
+8
src/notification.zig
···
1
1
const std = @import("std");
2
+
const vaxis = @import("vaxis");
3
+
const Event = @import("app.zig").Event;
4
+
2
5
const FileLogger = @import("file_logger.zig");
3
6
4
7
const Self = @This();
···
18
21
fbs: std.io.FixedBufferStream([]u8) = std.io.fixedBufferStream(&buf),
19
22
/// How long until the notification disappears in seconds.
20
23
timer: i64 = 0,
24
+
loop: ?*vaxis.Loop(Event) = null,
21
25
22
26
pub fn write(self: *Self, text: []const u8, style: Style) !void {
23
27
self.fbs.reset();
24
28
_ = try self.fbs.write(text);
25
29
self.timer = std.time.timestamp();
26
30
self.style = style;
31
+
32
+
if (self.loop) |loop| {
33
+
loop.postEvent(.notification);
34
+
}
27
35
}
28
36
29
37
pub fn reset(self: *Self) void {