+9
.gila/todo/grave_zone_233/grave_zone_233.md
+9
.gila/todo/grave_zone_233/grave_zone_233.md
···
···
1
+
---
2
+
title: feat: support cursor placement in input
3
+
status: todo
4
+
priority_value: 50
5
+
priority: high
6
+
owner: bjeyn
7
+
created: 2026-01-11T21:53:52Z
8
+
---
9
+
Currently the user cannot move their cursor left or right when typing in the input field. This is painful when renaming files or changing directories.
+9
.gila/todo/intelligent_dino_17y/intelligent_dino_17y.md
+9
.gila/todo/intelligent_dino_17y/intelligent_dino_17y.md
-9
PROJECT_BOARD.md
-9
PROJECT_BOARD.md
+1
-1
README.md
+1
-1
README.md
+8
-6
build.zig
+8
-6
build.zig
···
2
const builtin = @import("builtin");
3
4
///Must match the `version` in `build.zig.zon`.
5
-
const version = std.SemanticVersion{ .major = 1, .minor = 3, .patch = 0 };
6
7
const targets: []const std.Target.Query = &.{
8
.{ .cpu_arch = .aarch64, .os_tag = .macos },
···
20
) !*std.Build.Step.Compile {
21
const libvaxis = b.dependency("vaxis", .{ .target = target, .optimize = optimize }).module("vaxis");
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
const zeit = b.dependency("zeit", .{ .target = target, .optimize = optimize }).module("zeit");
25
26
const exe = b.addExecutable(.{
27
.name = exe_name,
28
-
.root_source_file = b.path("src/main.zig"),
29
-
.target = target,
30
-
.optimize = optimize,
31
});
32
33
exe.root_module.addImport("options", build_options);
34
exe.root_module.addImport("vaxis", libvaxis);
35
exe.root_module.addImport("fuzzig", fuzzig);
36
-
exe.root_module.addImport("zuid", zuid);
37
exe.root_module.addImport("zeit", zeit);
38
39
return exe;
40
}
···
2
const builtin = @import("builtin");
3
4
///Must match the `version` in `build.zig.zon`.
5
+
const version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 0 };
6
7
const targets: []const std.Target.Query = &.{
8
.{ .cpu_arch = .aarch64, .os_tag = .macos },
···
20
) !*std.Build.Step.Compile {
21
const libvaxis = b.dependency("vaxis", .{ .target = target, .optimize = optimize }).module("vaxis");
22
const fuzzig = b.dependency("fuzzig", .{ .target = target, .optimize = optimize }).module("fuzzig");
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
26
const exe = b.addExecutable(.{
27
.name = exe_name,
28
+
.root_module = b.createModule(.{
29
+
.root_source_file = b.path("src/main.zig"),
30
+
.target = target,
31
+
.optimize = optimize,
32
+
}),
33
});
34
35
exe.root_module.addImport("options", build_options);
36
exe.root_module.addImport("vaxis", libvaxis);
37
exe.root_module.addImport("fuzzig", fuzzig);
38
exe.root_module.addImport("zeit", zeit);
39
+
exe.root_module.addImport("zuid", zuid);
40
41
return exe;
42
}
+17
-10
build.zig.zon
+17
-10
build.zig.zon
···
2
.name = .jido,
3
.fingerprint = 0xee45eabe36cafb57,
4
.version = "1.3.0",
5
-
.minimum_zig_version = "0.14.0",
6
7
.dependencies = .{
8
.vaxis = .{
9
-
.url = "git+https://github.com/rockorager/libvaxis#1e24e0dfb509e974e1c8713bcd119d0ae032a8c7",
10
-
.hash = "vaxis-0.1.0-BWNV_MHyCAARemSCSwwc3sA1etNgv7ge0BCIXspX6CZv",
11
},
12
.fuzzig = .{
13
-
.url = "git+https://github.com/fjebaker/fuzzig#44c04733c7c0fee3db83672aaaaf4ed03e943156",
14
-
.hash = "fuzzig-0.1.1-AAAAALNIAQBmbHr-MPalGuR393Vem2pTQXI7_LXeNJgX",
15
},
16
.zuid = .{
17
-
.url = "git+https://github.com/KeithBrown39423/zuid#b6129f6cee45bd90b7ac97b8839dc28d21bedcb2",
18
-
.hash = "zuid-2.0.0-AAAAADxXAAA4MAzwwRhfZ9AC2FMPZ8hUrZbfpmJ_azpK",
19
},
20
-
.zeit = .{
21
-
.url = "git+https://github.com/rockorager/zeit/#175cf91a641790799e9d676878a9fe814aaed134",
22
-
.hash = "zeit-0.6.0-5I6bk5daAgC-P60TjxRqW0bYknfCGxJp-03eS9UjGrO7",
23
},
24
},
25
···
2
.name = .jido,
3
.fingerprint = 0xee45eabe36cafb57,
4
.version = "1.3.0",
5
+
.minimum_zig_version = "0.15.2",
6
7
.dependencies = .{
8
+
// Replace with rockorager/libvaxis once https://github.com/rockorager/libvaxis/pull/293 is merged
9
.vaxis = .{
10
+
.url = "git+https://github.com/rob9315/libvaxis.git#8d04cffd9137b4a8c56b356de98b32023ae752f3",
11
+
.hash = "vaxis-0.5.1-BWNV_OA-CQDeFBHIx9ryyASogr2GE3FsAm-l5Ii5-HZT",
12
},
13
.fuzzig = .{
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",
20
},
21
+
// Replace with KeithBrown39423/zuid once https://github.com/KeithBrown39423/zuid/pull/4 is merged
22
.zuid = .{
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",
25
},
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",
30
},
31
},
32
+34
-15
src/app.zig
+34
-15
src/app.zig
···
74
75
pub const Event = union(enum) {
76
image_ready,
77
key_press: Key,
78
winsize: vaxis.Winsize,
79
};
···
82
const Status = enum {
83
ready,
84
processing,
85
};
86
87
///Only use on first transmission. Subsequent draws should use
···
91
path: ?[]const u8 = null,
92
status: Status = .processing,
93
94
-
pub fn deinit(self: @This(), alloc: std.mem.Allocator) void {
95
if (self.data) |data| {
96
var d = data;
97
-
d.deinit();
98
}
99
if (self.path) |path| alloc.free(path);
100
}
···
108
alloc: std.mem.Allocator,
109
should_quit: bool,
110
vx: vaxis.Vaxis = undefined,
111
tty: vaxis.Tty = undefined,
112
loop: vaxis.Loop(Event) = undefined,
113
state: State = .normal,
···
149
.alloc = alloc,
150
.should_quit = false,
151
.vx = vx,
152
-
.tty = try vaxis.Tty.init(),
153
.directories = try Directories.init(alloc, entry_dir),
154
.help_menu = help_menu,
155
-
.text_input = vaxis.widgets.TextInput.init(alloc, &vx.unicode),
156
.actions = CircStack(Action, actions_len).init(),
157
.last_known_height = vx.window().height,
158
.images = .{ .cache = .init(alloc) },
159
};
160
-
161
app.loop = vaxis.Loop(Event){
162
.vaxis = &app.vx,
163
.tty = &app.tty,
···
191
self.help_menu.deinit();
192
self.directories.deinit();
193
self.text_input.deinit();
194
-
self.vx.deinit(self.alloc, self.tty.anyWriter());
195
self.tty.deinit();
196
if (self.file_logger) |file_logger| file_logger.deinit();
197
198
var image_iter = self.images.cache.iterator();
199
while (image_iter.next()) |img| {
200
-
img.value_ptr.deinit(self.alloc);
201
}
202
self.images.cache.deinit();
203
}
204
205
-
pub fn inputToSlice(self: *App) []const u8 {
206
-
self.text_input.buf.cursor = self.text_input.buf.realLength();
207
-
return self.text_input.sliceToCursor(&self.text_input_buf);
208
}
209
210
pub fn repopulateDirectory(self: *App, fuzzy: []const u8) error{OutOfMemory}!void {
···
222
try self.loop.start();
223
defer self.loop.stop();
224
225
-
try self.vx.enterAltScreen(self.tty.anyWriter());
226
-
try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s);
227
self.vx.caps.kitty_graphics = true;
228
229
while (!self.should_quit) {
···
248
249
try self.drawer.draw(self);
250
251
-
var buffered = self.tty.bufferedWriter();
252
-
try self.vx.render(buffered.writer().any());
253
-
try buffered.flush();
254
}
255
256
if (config.empty_trash_on_exit) {
···
74
75
pub const Event = union(enum) {
76
image_ready,
77
+
notification,
78
key_press: Key,
79
winsize: vaxis.Winsize,
80
};
···
83
const Status = enum {
84
ready,
85
processing,
86
+
failed,
87
};
88
89
///Only use on first transmission. Subsequent draws should use
···
93
path: ?[]const u8 = null,
94
status: Status = .processing,
95
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
+
}
100
if (self.data) |data| {
101
var d = data;
102
+
d.deinit(alloc);
103
}
104
if (self.path) |path| alloc.free(path);
105
}
···
113
alloc: std.mem.Allocator,
114
should_quit: bool,
115
vx: vaxis.Vaxis = undefined,
116
+
tty_buffer: [1024]u8 = undefined,
117
tty: vaxis.Tty = undefined,
118
loop: vaxis.Loop(Event) = undefined,
119
state: State = .normal,
···
155
.alloc = alloc,
156
.should_quit = false,
157
.vx = vx,
158
.directories = try Directories.init(alloc, entry_dir),
159
.help_menu = help_menu,
160
+
.text_input = vaxis.widgets.TextInput.init(alloc),
161
.actions = CircStack(Action, actions_len).init(),
162
.last_known_height = vx.window().height,
163
.images = .{ .cache = .init(alloc) },
164
};
165
+
app.tty = try vaxis.Tty.init(&app.tty_buffer);
166
app.loop = vaxis.Loop(Event){
167
.vaxis = &app.vx,
168
.tty = &app.tty,
···
196
self.help_menu.deinit();
197
self.directories.deinit();
198
self.text_input.deinit();
199
+
self.vx.deinit(self.alloc, self.tty.writer());
200
self.tty.deinit();
201
if (self.file_logger) |file_logger| file_logger.deinit();
202
203
var image_iter = self.images.cache.iterator();
204
while (image_iter.next()) |img| {
205
+
img.value_ptr.deinit(self.alloc, self.vx, &self.tty);
206
}
207
self.images.cache.deinit();
208
}
209
210
+
/// Reads the current text input without consuming it.
211
+
/// The returned slice is valid until the next call to readInput() or until
212
+
/// the text_input buffer is modified.
213
+
pub fn readInput(self: *App) []const u8 {
214
+
const first = self.text_input.buf.firstHalf();
215
+
const second = self.text_input.buf.secondHalf();
216
+
var dest_idx: usize = 0;
217
+
218
+
const first_len = @min(first.len, self.text_input_buf.len - dest_idx);
219
+
@memcpy(self.text_input_buf[dest_idx .. dest_idx + first_len], first[0..first_len]);
220
+
dest_idx += first_len;
221
+
222
+
const second_len = @min(second.len, self.text_input_buf.len - dest_idx);
223
+
@memcpy(self.text_input_buf[dest_idx .. dest_idx + second_len], second[0..second_len]);
224
+
dest_idx += second_len;
225
+
226
+
return self.text_input_buf[0..dest_idx];
227
}
228
229
pub fn repopulateDirectory(self: *App, fuzzy: []const u8) error{OutOfMemory}!void {
···
241
try self.loop.start();
242
defer self.loop.stop();
243
244
+
try self.vx.enterAltScreen(self.tty.writer());
245
+
try self.vx.queryTerminal(self.tty.writer(), 1 * std.time.ns_per_s);
246
self.vx.caps.kitty_graphics = true;
247
248
while (!self.should_quit) {
···
267
268
try self.drawer.draw(self);
269
270
+
const writer = self.tty.writer();
271
+
try self.vx.render(writer);
272
+
try writer.flush();
273
}
274
275
if (config.empty_trash_on_exit) {
-1
src/directories.zig
-1
src/directories.zig
+63
-17
src/drawer.zig
+63
-17
src/drawer.zig
···
62
try self.drawFilePreview(app, win, file_name_bar);
63
}
64
65
-
const input = app.inputToSlice();
66
drawUserInput(app.state, &app.text_input, input, win);
67
68
// Notification should be drawn last.
···
208
break :file;
209
}
210
211
if (cache_entry.image) |img| {
212
img.draw(preview_win, .{ .scale = .contain }) catch |err| {
213
const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err});
···
224
} else {
225
if (cache_entry.data == null) {
226
const path = try app.alloc.dupe(u8, self.current_item_path);
227
-
processImage(app, path) catch break :unsupported;
228
}
229
230
-
if (app.vx.transmitImage(app.alloc, app.tty.anyWriter(), &cache_entry.data.?, .rgba)) |img| {
231
img.draw(preview_win, .{ .scale = .contain }) catch |err| {
232
const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err});
233
defer app.alloc.free(message);
···
240
break :file;
241
};
242
cache_entry.image = img;
243
-
cache_entry.data.?.deinit();
244
cache_entry.data = null;
245
} else |_| {
246
break :unsupported;
···
249
250
break :file;
251
} else {
252
const path = try app.alloc.dupe(u8, self.current_item_path);
253
-
processImage(app, path) catch break :unsupported;
254
}
255
256
break :file;
···
350
351
// Time created / last modified
352
if (self.verbose) lbl: {
353
-
var maybe_meta: ?std.fs.File.Metadata = null;
354
if (entry.kind == .directory) {
355
-
maybe_meta = directories.dir.metadata() catch break :lbl;
356
} else if (entry.kind == .file) {
357
var file = directories.dir.openFile(entry.name, .{}) catch break :lbl;
358
-
maybe_meta = file.metadata() catch break :lbl;
359
}
360
361
const meta = maybe_meta orelse break :lbl;
···
365
defer local.deinit();
366
367
const ctime_instant = zeit.instant(.{
368
-
.source = .{ .unix_nano = meta.created().? },
369
.timezone = &local,
370
}) catch break :lbl;
371
const ctime = ctime_instant.time();
372
ctime.strftime(fbs.writer().any(), "Created: %Y-%m-%d %H:%M:%S\n") catch break :lbl;
373
374
const mtime_instant = zeit.instant(.{
375
-
.source = .{ .unix_nano = meta.modified() },
376
.timezone = &local,
377
}) catch break :lbl;
378
const mtime = mtime_instant.time();
···
441
442
break :lbl 0;
443
};
444
-
if (size) |s| try fbs.writer().print("{s}{:.2}\n", .{
445
if (self.verbose) "Size: " else "",
446
-
std.fmt.fmtIntSizeDec(s),
447
});
448
449
// Extension.
···
640
const load_img_thread = std.Thread.spawn(.{}, loadImage, .{
641
app,
642
path,
643
-
}) catch return error.Unsupported;
644
load_img_thread.detach();
645
}
646
647
-
fn loadImage(app: *App, path: []const u8) error{ Unsupported, OutOfMemory }!void {
648
-
const data = vaxis.zigimg.Image.fromFilePath(app.alloc, path) catch {
649
const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to read image from path.", .{path});
650
defer app.alloc.free(message);
651
app.notification.write(message, .err) catch {};
652
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
653
-
return error.Unsupported;
654
};
655
656
app.images.mutex.lock();
657
if (app.images.cache.getPtr(path)) |entry| {
658
entry.status = .ready;
659
entry.data = data;
660
} else {
661
const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to add image to cache.", .{path});
662
defer app.alloc.free(message);
663
app.notification.write(message, .err) catch {};
664
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
665
-
return error.Unsupported;
666
}
667
app.images.mutex.unlock();
668
···
62
try self.drawFilePreview(app, win, file_name_bar);
63
}
64
65
+
const input = app.readInput();
66
drawUserInput(app.state, &app.text_input, input, win);
67
68
// Notification should be drawn last.
···
208
break :file;
209
}
210
211
+
if (cache_entry.status == .failed) {
212
+
_ = preview_win.print(&.{
213
+
.{ .text = "Failed to process image." },
214
+
}, .{});
215
+
break :file;
216
+
}
217
+
218
if (cache_entry.image) |img| {
219
img.draw(preview_win, .{ .scale = .contain }) catch |err| {
220
const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err});
···
231
} else {
232
if (cache_entry.data == null) {
233
const path = try app.alloc.dupe(u8, self.current_item_path);
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;
242
}
243
244
+
if (app.vx.transmitImage(app.alloc, app.tty.writer(), &cache_entry.data.?, .rgba)) |img| {
245
img.draw(preview_win, .{ .scale = .contain }) catch |err| {
246
const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err});
247
defer app.alloc.free(message);
···
254
break :file;
255
};
256
cache_entry.image = img;
257
+
if (cache_entry.data) |data| {
258
+
var d = data;
259
+
d.deinit(app.alloc);
260
+
}
261
cache_entry.data = null;
262
} else |_| {
263
break :unsupported;
···
266
267
break :file;
268
} else {
269
+
_ = preview_win.print(&.{
270
+
.{ .text = "Processing image." },
271
+
}, .{});
272
+
273
const path = try app.alloc.dupe(u8, self.current_item_path);
274
+
processImage(app, path) catch {
275
+
app.alloc.free(path);
276
+
break :unsupported;
277
+
};
278
}
279
280
break :file;
···
374
375
// Time created / last modified
376
if (self.verbose) lbl: {
377
+
var maybe_meta: ?std.fs.File.Stat = null;
378
if (entry.kind == .directory) {
379
+
maybe_meta = directories.dir.stat() catch break :lbl;
380
} else if (entry.kind == .file) {
381
var file = directories.dir.openFile(entry.name, .{}) catch break :lbl;
382
+
maybe_meta = file.stat() catch break :lbl;
383
}
384
385
const meta = maybe_meta orelse break :lbl;
···
389
defer local.deinit();
390
391
const ctime_instant = zeit.instant(.{
392
+
.source = .{ .unix_nano = meta.ctime },
393
.timezone = &local,
394
}) catch break :lbl;
395
const ctime = ctime_instant.time();
396
ctime.strftime(fbs.writer().any(), "Created: %Y-%m-%d %H:%M:%S\n") catch break :lbl;
397
398
const mtime_instant = zeit.instant(.{
399
+
.source = .{ .unix_nano = meta.mtime },
400
.timezone = &local,
401
}) catch break :lbl;
402
const mtime = mtime_instant.time();
···
465
466
break :lbl 0;
467
};
468
+
if (size) |s| try fbs.writer().print("{s}{B:.2}\n", .{
469
if (self.verbose) "Size: " else "",
470
+
s,
471
});
472
473
// Extension.
···
664
const load_img_thread = std.Thread.spawn(.{}, loadImage, .{
665
app,
666
path,
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
+
};
681
load_img_thread.detach();
682
}
683
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
+
693
const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to read image from path.", .{path});
694
defer app.alloc.free(message);
695
app.notification.write(message, .err) catch {};
696
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
697
+
698
+
return;
699
};
700
701
app.images.mutex.lock();
702
if (app.images.cache.getPtr(path)) |entry| {
703
entry.status = .ready;
704
entry.data = data;
705
+
entry.path = path;
706
} else {
707
const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to add image to cache.", .{path});
708
defer app.alloc.free(message);
709
app.notification.write(message, .err) catch {};
710
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
711
+
return;
712
}
713
app.images.mutex.unlock();
714
+1
-1
src/environment.zig
+1
-1
src/environment.zig
+12
-11
src/event_handlers.zig
+12
-11
src/event_handlers.zig
···
133
}
134
},
135
.image_ready => {},
136
-
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
137
}
138
}
139
···
161
.new_file => try events.createNewFile(app),
162
.rename => try events.rename(app),
163
.change_dir => {
164
-
const path = app.inputToSlice();
165
try commands.cd(app, path);
166
-
app.text_input.clearAndFree();
167
},
168
.command => {
169
-
const command = app.inputToSlice();
170
171
// Push command to history if it's not empty.
172
if (!std.mem.eql(u8, std.mem.trim(u8, command, " "), ":")) {
···
208
break :supported;
209
}
210
211
-
app.text_input.clearAndFree();
212
try app.text_input.insertSliceAtCursor(":UnsupportedCommand");
213
}
214
···
220
if (app.state != .help_menu) app.state = .normal;
221
app.directories.entries.selected = selected;
222
},
223
-
Key.left => app.text_input.cursorLeft(),
224
-
Key.right => app.text_input.cursorRight(),
225
Key.up => {
226
if (app.state == .command) {
227
if (app.command_history.previous()) |command| {
···
260
261
switch (app.state) {
262
.fuzzy => {
263
-
const fuzzy = app.inputToSlice();
264
try app.repopulateDirectory(fuzzy);
265
},
266
.command => {
267
-
const command = app.inputToSlice();
268
if (!std.mem.startsWith(u8, command, ":")) {
269
app.text_input.clearAndFree();
270
app.text_input.insertSliceAtCursor(":") catch |err| {
···
283
}
284
},
285
.image_ready => {},
286
-
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
287
}
288
}
289
···
298
}
299
},
300
.image_ready => {},
301
-
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
302
}
303
}
···
133
}
134
},
135
.image_ready => {},
136
+
.notification => {},
137
+
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
138
}
139
}
140
···
162
.new_file => try events.createNewFile(app),
163
.rename => try events.rename(app),
164
.change_dir => {
165
+
const path = try app.text_input.toOwnedSlice();
166
+
defer app.alloc.free(path);
167
try commands.cd(app, path);
168
},
169
.command => {
170
+
const command = try app.text_input.toOwnedSlice();
171
+
defer app.alloc.free(command);
172
173
// Push command to history if it's not empty.
174
if (!std.mem.eql(u8, std.mem.trim(u8, command, " "), ":")) {
···
210
break :supported;
211
}
212
213
try app.text_input.insertSliceAtCursor(":UnsupportedCommand");
214
}
215
···
221
if (app.state != .help_menu) app.state = .normal;
222
app.directories.entries.selected = selected;
223
},
224
Key.up => {
225
if (app.state == .command) {
226
if (app.command_history.previous()) |command| {
···
259
260
switch (app.state) {
261
.fuzzy => {
262
+
const fuzzy = app.readInput();
263
try app.repopulateDirectory(fuzzy);
264
},
265
.command => {
266
+
const command = app.readInput();
267
if (!std.mem.startsWith(u8, command, ":")) {
268
app.text_input.clearAndFree();
269
app.text_input.insertSliceAtCursor(":") catch |err| {
···
282
}
283
},
284
.image_ready => {},
285
+
.notification => {},
286
+
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
287
}
288
}
289
···
298
}
299
},
300
.image_ready => {},
301
+
.notification => {},
302
+
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
303
}
304
}
+11
-17
src/events.zig
+11
-17
src/events.zig
···
49
return;
50
}
51
52
-
const tmp_path = try std.fmt.allocPrint(app.alloc, "{s}/{s}-{s}", .{ trash_dir_path, entry.name, zuid.new.v4() });
53
if (app.directories.dir.rename(entry.name, tmp_path)) {
54
if (app.actions.push(.{
55
.delete = .{ .prev_path = prev_path_alloc, .new_path = tmp_path },
···
87
return;
88
};
89
90
-
const new_path = app.inputToSlice();
91
92
if (environment.fileExists(app.directories.dir, new_path)) {
93
message = try std.fmt.allocPrint(app.alloc, "Can not rename file - '{s}' already exists.", .{new_path});
···
111
}
112
113
try app.repopulateDirectory("");
114
-
app.text_input.clearAndFree();
115
116
message = try std.fmt.allocPrint(app.alloc, "Renamed '{s}' to '{s}'.", .{ entry.name, new_path });
117
app.notification.write(message.?, .info) catch {};
118
}
119
-
120
-
app.text_input.clearAndFree();
121
}
122
123
pub fn forceDelete(app: *App) error{OutOfMemory}!void {
···
419
},
420
.file => {
421
if (environment.getEditor()) |editor| {
422
-
try app.vx.exitAltScreen(app.tty.anyWriter());
423
-
try app.vx.resetState(app.tty.anyWriter());
424
app.loop.stop();
425
426
environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch |err| {
···
430
};
431
432
try app.loop.start();
433
-
try app.vx.enterAltScreen(app.tty.anyWriter());
434
-
try app.vx.enableDetectedFeatures(app.tty.anyWriter());
435
app.vx.queueRefresh();
436
} else {
437
app.notification.write("Can not open file - $EDITOR not set.", .warn) catch {};
···
445
var message: ?[]const u8 = null;
446
defer if (message) |msg| app.alloc.free(msg);
447
448
-
const dir = app.inputToSlice();
449
450
app.directories.dir.makeDir(dir) catch |err| {
451
message = try std.fmt.allocPrint(app.alloc, "Failed to create directory '{s}' - {}", .{ dir, err });
452
app.notification.write(message.?, .err) catch {};
453
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
454
-
app.text_input.clearAndFree();
455
return;
456
};
457
458
try app.repopulateDirectory("");
459
-
app.text_input.clearAndFree();
460
461
message = try std.fmt.allocPrint(app.alloc, "Created new directory '{s}'.", .{dir});
462
app.notification.write(message.?, .info) catch {};
···
466
var message: ?[]const u8 = null;
467
defer if (message) |msg| app.alloc.free(msg);
468
469
-
const file = app.inputToSlice();
470
471
if (environment.fileExists(app.directories.dir, file)) {
472
message = try std.fmt.allocPrint(app.alloc, "Can not create file - '{s}' already exists.", .{file});
···
476
message = try std.fmt.allocPrint(app.alloc, "Failed to create file '{s}' - {}", .{ file, err });
477
app.notification.write(message.?, .err) catch {};
478
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
479
-
app.text_input.clearAndFree();
480
return;
481
};
482
483
try app.repopulateDirectory("");
484
-
app.text_input.clearAndFree();
485
486
message = try std.fmt.allocPrint(app.alloc, "Created new file '{s}'.", .{file});
487
app.notification.write(message.?, .info) catch {};
488
}
489
-
490
-
app.text_input.clearAndFree();
491
}
492
493
pub fn undo(app: *App) error{OutOfMemory}!void {
···
49
return;
50
}
51
52
+
const tmp_path = try std.fmt.allocPrint(app.alloc, "{s}/{s}-{f}", .{ trash_dir_path, entry.name, zuid.new.v4() });
53
if (app.directories.dir.rename(entry.name, tmp_path)) {
54
if (app.actions.push(.{
55
.delete = .{ .prev_path = prev_path_alloc, .new_path = tmp_path },
···
87
return;
88
};
89
90
+
const new_path = try app.text_input.toOwnedSlice();
91
+
defer app.alloc.free(new_path);
92
93
if (environment.fileExists(app.directories.dir, new_path)) {
94
message = try std.fmt.allocPrint(app.alloc, "Can not rename file - '{s}' already exists.", .{new_path});
···
112
}
113
114
try app.repopulateDirectory("");
115
116
message = try std.fmt.allocPrint(app.alloc, "Renamed '{s}' to '{s}'.", .{ entry.name, new_path });
117
app.notification.write(message.?, .info) catch {};
118
}
119
}
120
121
pub fn forceDelete(app: *App) error{OutOfMemory}!void {
···
417
},
418
.file => {
419
if (environment.getEditor()) |editor| {
420
+
try app.vx.exitAltScreen(app.tty.writer());
421
+
try app.vx.resetState(app.tty.writer());
422
app.loop.stop();
423
424
environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch |err| {
···
428
};
429
430
try app.loop.start();
431
+
try app.vx.enterAltScreen(app.tty.writer());
432
+
try app.vx.enableDetectedFeatures(app.tty.writer());
433
app.vx.queueRefresh();
434
} else {
435
app.notification.write("Can not open file - $EDITOR not set.", .warn) catch {};
···
443
var message: ?[]const u8 = null;
444
defer if (message) |msg| app.alloc.free(msg);
445
446
+
const dir = try app.text_input.toOwnedSlice();
447
+
defer app.alloc.free(dir);
448
449
app.directories.dir.makeDir(dir) catch |err| {
450
message = try std.fmt.allocPrint(app.alloc, "Failed to create directory '{s}' - {}", .{ dir, err });
451
app.notification.write(message.?, .err) catch {};
452
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
453
return;
454
};
455
456
try app.repopulateDirectory("");
457
458
message = try std.fmt.allocPrint(app.alloc, "Created new directory '{s}'.", .{dir});
459
app.notification.write(message.?, .info) catch {};
···
463
var message: ?[]const u8 = null;
464
defer if (message) |msg| app.alloc.free(msg);
465
466
+
const file = try app.text_input.toOwnedSlice();
467
+
defer app.alloc.free(file);
468
469
if (environment.fileExists(app.directories.dir, file)) {
470
message = try std.fmt.allocPrint(app.alloc, "Can not create file - '{s}' already exists.", .{file});
···
474
message = try std.fmt.allocPrint(app.alloc, "Failed to create file '{s}' - {}", .{ file, err });
475
app.notification.write(message.?, .err) catch {};
476
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
477
return;
478
};
479
480
try app.repopulateDirectory("");
481
482
message = try std.fmt.allocPrint(app.alloc, "Created new file '{s}'.", .{file});
483
app.notification.write(message.?, .info) catch {};
484
}
485
}
486
487
pub fn undo(app: *App) error{OutOfMemory}!void {
+13
-15
src/file_logger.zig
+13
-15
src/file_logger.zig
···
24
file: ?std.fs.File,
25
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
-
}
39
40
return .{ .dir = dir, .file = file };
41
}
···
49
50
pub fn write(self: FileLogger, msg: []const u8, level: LogLevel) !void {
51
const file = if (self.file) |file| file else return error.NoLogFile;
52
-
if (try file.tryLock(std.fs.File.Lock.shared)) {
53
defer file.unlock();
54
-
try file.seekFromEnd(0);
55
56
-
try file.writer().print(
57
"({d}) {s}: {s}\n",
58
.{ std.time.timestamp(), LogLevel.toString(level), msg },
59
);
60
}
61
}
···
24
file: ?std.fs.File,
25
26
pub fn init(dir: std.fs.Dir) FileLogger {
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
+
};
31
32
return .{ .dir = dir, .file = file };
33
}
···
41
42
pub fn write(self: FileLogger, msg: []const u8, level: LogLevel) !void {
43
const file = if (self.file) |file| file else return error.NoLogFile;
44
+
45
+
if (try file.tryLock(.exclusive)) {
46
defer file.unlock();
47
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(
54
"({d}) {s}: {s}\n",
55
.{ std.time.timestamp(), LogLevel.toString(level), msg },
56
);
57
+
try file_writer.flush();
58
}
59
}
+4
-4
src/list.zig
+4
-4
src/list.zig
···
12
pub fn init(alloc: std.mem.Allocator) Self {
13
return Self{
14
.alloc = alloc,
15
-
.items = std.ArrayList(T).init(alloc),
16
.selected = 0,
17
};
18
}
19
20
pub fn deinit(self: *Self) void {
21
-
self.items.deinit();
22
}
23
24
pub fn append(self: *Self, item: T) !void {
25
-
try self.items.append(item);
26
}
27
28
pub fn clear(self: *Self) void {
29
-
self.items.clearAndFree();
30
self.selected = 0;
31
}
32
···
12
pub fn init(alloc: std.mem.Allocator) Self {
13
return Self{
14
.alloc = alloc,
15
+
.items = .empty,
16
.selected = 0,
17
};
18
}
19
20
pub fn deinit(self: *Self) void {
21
+
self.items.deinit(self.alloc);
22
}
23
24
pub fn append(self: *Self, item: T) !void {
25
+
try self.items.append(self.alloc, item);
26
}
27
28
pub fn clear(self: *Self) void {
29
+
self.items.clearAndFree(self.alloc);
30
self.selected = 0;
31
}
32
+11
-3
src/main.zig
+11
-3
src/main.zig
···
104
}
105
106
if (opts.version) {
107
-
std.debug.print("jido v{}\n", .{options.version});
108
return;
109
}
110
···
139
},
140
};
141
142
-
app.file_logger = if (config.config_dir) |dir| FileLogger.init(dir) else null;
143
144
try app.run();
145
···
151
// Must be printed after app has deinit as part of that process clears
152
// the screen.
153
if (last_dir) |path| {
154
-
const stdout = std.io.getStdOut().writer();
155
stdout.print("{s}\n", .{path}) catch {};
156
alloc.free(path);
157
}
158
}
···
104
}
105
106
if (opts.version) {
107
+
std.debug.print("jido v{f}\n", .{options.version});
108
return;
109
}
110
···
139
},
140
};
141
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;
147
148
try app.run();
149
···
155
// Must be printed after app has deinit as part of that process clears
156
// the screen.
157
if (last_dir) |path| {
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;
161
stdout.print("{s}\n", .{path}) catch {};
162
+
stdout.flush() catch {};
163
+
164
alloc.free(path);
165
}
166
}
+8
src/notification.zig
+8
src/notification.zig
···
1
const std = @import("std");
2
const FileLogger = @import("file_logger.zig");
3
4
const Self = @This();
···
18
fbs: std.io.FixedBufferStream([]u8) = std.io.fixedBufferStream(&buf),
19
/// How long until the notification disappears in seconds.
20
timer: i64 = 0,
21
22
pub fn write(self: *Self, text: []const u8, style: Style) !void {
23
self.fbs.reset();
24
_ = try self.fbs.write(text);
25
self.timer = std.time.timestamp();
26
self.style = style;
27
}
28
29
pub fn reset(self: *Self) void {
···
1
const std = @import("std");
2
+
const vaxis = @import("vaxis");
3
+
const Event = @import("app.zig").Event;
4
+
5
const FileLogger = @import("file_logger.zig");
6
7
const Self = @This();
···
21
fbs: std.io.FixedBufferStream([]u8) = std.io.fixedBufferStream(&buf),
22
/// How long until the notification disappears in seconds.
23
timer: i64 = 0,
24
+
loop: ?*vaxis.Loop(Event) = null,
25
26
pub fn write(self: *Self, text: []const u8, style: Style) !void {
27
self.fbs.reset();
28
_ = try self.fbs.write(text);
29
self.timer = std.time.timestamp();
30
self.style = style;
31
+
32
+
if (self.loop) |loop| {
33
+
loop.postEvent(.notification);
34
+
}
35
}
36
37
pub fn reset(self: *Self) void {