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

Compare changes

Choose any two refs to compare.

+1 -1
README.md
··· 10 10 Vim-like bindings and a minimalist interface, Jido focuses on speed and 11 11 simplicity. 12 12 13 - Jido used Zig `0.14.0`. 13 + Jido is built with Zig v`0.15.2`. 14 14 15 15 - [Installation](#installation) 16 16 - [Integrations](#integrations)
+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
··· 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
··· 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
··· 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
··· 38 38 const extension = std.fs.path.extension(relative_path); 39 39 break :lbl try std.fmt.bufPrint( 40 40 buf, 41 - "{s}-{s}{s}", 41 + "{s}-{f}{s}", 42 42 .{ relative_path[0 .. relative_path.len - extension.len], zuid.new.v4(), extension }, 43 43 ); 44 44 } else lbl: {
+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
··· 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
··· 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
··· 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
··· 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
··· 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 {