地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.

Compare changes

Choose any two refs to compare.

+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
···
··· 1 + --- 2 + title: feat: add archive extraction support 3 + status: todo 4 + priority_value: 50 5 + priority: low 6 + owner: bjeyn 7 + created: 2026-01-11T21:52:59Z 8 + --- 9 + Allow users to extract archives via a keybind
-9
PROJECT_BOARD.md
··· 1 - # Project board 2 - 3 - Key: 4 - - `[ ]` Todo 5 - - `[-]` In progress 6 - - `[x]` Done 7 - 8 - ## Backlog 9 - - [ ] Keybind to unzip archives.
···
+1 -1
README.md
··· 10 Vim-like bindings and a minimalist interface, Jido focuses on speed and 11 simplicity. 12 13 - Jido used Zig `0.14.0`. 14 15 - [Installation](#installation) 16 - [Integrations](#integrations)
··· 10 Vim-like bindings and a minimalist interface, Jido focuses on speed and 11 simplicity. 12 13 + Jido is built with Zig v`0.15.2`. 14 15 - [Installation](#installation) 16 - [Integrations](#integrations)
+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
··· 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
··· 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
··· 2 const List = @import("./list.zig").List; 3 const CircStack = @import("./circ_stack.zig").CircularStack; 4 const config = &@import("./config.zig").config; 5 - const vaxis = @import("vaxis"); 6 const fuzzig = @import("fuzzig"); 7 8 const history_len: usize = 100;
··· 2 const List = @import("./list.zig").List; 3 const CircStack = @import("./circ_stack.zig").CircularStack; 4 const config = &@import("./config.zig").config; 5 const fuzzig = @import("fuzzig"); 6 7 const history_len: usize = 100;
+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
··· 38 const extension = std.fs.path.extension(relative_path); 39 break :lbl try std.fmt.bufPrint( 40 buf, 41 - "{s}-{s}{s}", 42 .{ relative_path[0 .. relative_path.len - extension.len], zuid.new.v4(), extension }, 43 ); 44 } else lbl: {
··· 38 const extension = std.fs.path.extension(relative_path); 39 break :lbl try std.fmt.bufPrint( 40 buf, 41 + "{s}-{f}{s}", 42 .{ relative_path[0 .. relative_path.len - extension.len], zuid.new.v4(), extension }, 43 ); 44 } else lbl: {
+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
··· 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
··· 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
··· 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
··· 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
··· 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 {