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

Updated libvaxis dependency - Updated way images are processed - Pushing a filled circ queue will now return the item overwritten allowing us to free its memory

+66 -37
+1 -1
.gitignore
··· 1 - zig-cache/ 2 zig-out/
··· 1 + .zig-cache/ 2 zig-out/
+2 -2
build.zig.zon
··· 7 8 .dependencies = .{ 9 .vaxis = .{ 10 - .url = "git+https://github.com/rockorager/libvaxis#75ac36ca61999c2f29467e02016322551e98bbd7", 11 - .hash = "1220fbb1b748ced787d6a19d3be8a28839e000548b76bae0f183a6e13504e6bc7b20", 12 }, 13 .fuzzig = .{ 14 .url = "git+https://github.com/fjebaker/fuzzig#0fd156d5097365151e85a85eef9d8cf0eebe7b00",
··· 7 8 .dependencies = .{ 9 .vaxis = .{ 10 + .url = "git+https://github.com/rockorager/libvaxis#fcdeb321feccc1b2b62391c1e257c385a799810e", 11 + .hash = "1220be1b2c1cf8809459629fde0d7ff2eb1bca854a99e4ac69fa19e03afec9428460", 12 }, 13 .fuzzig = .{ 14 .url = "git+https://github.com/fjebaker/fuzzig#0fd156d5097365151e85a85eef9d8cf0eebe7b00",
+54 -33
src/app.zig
··· 154 } 155 156 pub fn inputToSlice(self: *App) []const u8 { 157 - self.text_input.cursor_idx = self.text_input.grapheme_count; 158 return self.text_input.sliceToCursor(&self.text_input_buf); 159 } 160 ··· 202 if (self.directories.dir.openDir(entry.name, .{ .iterate = true })) |dir| { 203 self.directories.dir = dir; 204 205 - self.directories.history.push(.{ 206 .selected = self.directories.entries.selected, 207 .offset = self.directories.entries.offset, 208 }); ··· 268 269 try self.notification.write("Deleting item...", .info); 270 if (self.directories.dir.rename(entry.name, tmp_path)) { 271 - // TODO: Will leak memory if pushing to a full stack. 272 - self.actions.push(.{ 273 .delete = .{ .old = old_path, .new = tmp_path }, 274 - }); 275 try self.notification.write("Deleted item.", .info); 276 277 self.directories.remove_selected(); 278 - } else |_| { 279 - try self.notification.write_err(.UnableToDeleteItem); 280 } 281 }, 282 'd' => { 283 self.state = .new_dir; 284 }, 285 '%' => { 286 self.state = .new_file; 287 }, 288 'u' => { ··· 366 pub fn handle_input_event(self: *App, event: Event) !InputReturnStatus { 367 switch (event) { 368 .key_press => |key| { 369 - if ((key.codepoint == 'c' and key.mods.ctrl) or key.codepoint == 'q') { 370 return .exit; 371 } 372 ··· 448 error.PathAlreadyExists => try self.notification.write_err(.ItemAlreadyExists), 449 else => try self.notification.write_err(.UnknownError), 450 }; 451 - // TODO: Will leak memory if pushing to a full stack. 452 - self.actions.push(.{ 453 .rename = .{ 454 .old = try std.fs.path.join(self.alloc, &.{ dir_prefix, old.name }), 455 .new = try std.fs.path.join(self.alloc, &.{ dir_prefix, new }), 456 }, 457 - }); 458 459 self.directories.cleanup(); 460 self.directories.populate_entries("") catch |err| { ··· 609 610 // Handle image. 611 if (config.show_images == true) unsupported_terminal: { 612 - const supported: [1][]const u8 = .{".png"}; 613 - 614 - for (supported) |ext| { 615 - if (std.mem.eql(u8, std.fs.path.extension(entry.name), ext)) { 616 - if (!std.mem.eql(u8, self.last_item_path, self.current_item_path)) { 617 - if (self.vx.loadImage(self.alloc, self.tty.anyWriter(), .{ .path = self.current_item_path })) |img| { 618 - self.image = img; 619 - } else |_| { 620 - self.image = null; 621 - break :unsupported_terminal; 622 - } 623 - } 624 - 625 if (self.image) |img| { 626 - try img.draw(preview_win, .{ .scale = .fit }); 627 } 628 629 - break :file; 630 - } else { 631 - // Free any image we might have already. 632 - if (self.image) |img| { 633 - self.vx.freeImage(self.tty.anyWriter(), img.id); 634 - } 635 } 636 } 637 } 638 639 // Handle pdf. ··· 745 746 // Display info box. 747 if (self.notification.len > 0) { 748 - if (self.text_input.grapheme_count > 0) { 749 self.text_input.clearAndFree(); 750 } 751 ··· 767 self.text_input.draw(info_win); 768 }, 769 .normal => { 770 - if (self.text_input.grapheme_count > 0) { 771 self.text_input.draw(info_win); 772 } 773
··· 154 } 155 156 pub fn inputToSlice(self: *App) []const u8 { 157 + self.text_input.buf.cursor = self.text_input.buf.realLength(); 158 return self.text_input.sliceToCursor(&self.text_input_buf); 159 } 160 ··· 202 if (self.directories.dir.openDir(entry.name, .{ .iterate = true })) |dir| { 203 self.directories.dir = dir; 204 205 + _ = self.directories.history.push(.{ 206 .selected = self.directories.entries.selected, 207 .offset = self.directories.entries.offset, 208 }); ··· 268 269 try self.notification.write("Deleting item...", .info); 270 if (self.directories.dir.rename(entry.name, tmp_path)) { 271 + if (self.actions.push(.{ 272 .delete = .{ .old = old_path, .new = tmp_path }, 273 + })) |prev_elem| { 274 + self.alloc.free(prev_elem.delete.old); 275 + self.alloc.free(prev_elem.delete.new); 276 + } 277 try self.notification.write("Deleted item.", .info); 278 279 self.directories.remove_selected(); 280 + } else |err| { 281 + switch (err) { 282 + error.RenameAcrossMountPoints => try self.notification.write_err(.UnableToDeleteAcrossMountPoints), 283 + else => try self.notification.write_err(.UnableToDeleteItem), 284 + } 285 + self.alloc.free(old_path); 286 + self.alloc.free(tmp_path); 287 } 288 }, 289 'd' => { 290 + self.text_input.clearAndFree(); 291 + self.directories.cleanup(); 292 + self.directories.populate_entries("") catch |err| { 293 + switch (err) { 294 + error.AccessDenied => try self.notification.write_err(.PermissionDenied), 295 + else => try self.notification.write_err(.UnknownError), 296 + } 297 + }; 298 self.state = .new_dir; 299 }, 300 '%' => { 301 + self.text_input.clearAndFree(); 302 + self.directories.cleanup(); 303 + self.directories.populate_entries("") catch |err| { 304 + switch (err) { 305 + error.AccessDenied => try self.notification.write_err(.PermissionDenied), 306 + else => try self.notification.write_err(.UnknownError), 307 + } 308 + }; 309 self.state = .new_file; 310 }, 311 'u' => { ··· 389 pub fn handle_input_event(self: *App, event: Event) !InputReturnStatus { 390 switch (event) { 391 .key_press => |key| { 392 + if ((key.codepoint == 'c' and key.mods.ctrl)) { 393 return .exit; 394 } 395 ··· 471 error.PathAlreadyExists => try self.notification.write_err(.ItemAlreadyExists), 472 else => try self.notification.write_err(.UnknownError), 473 }; 474 + if (self.actions.push(.{ 475 .rename = .{ 476 .old = try std.fs.path.join(self.alloc, &.{ dir_prefix, old.name }), 477 .new = try std.fs.path.join(self.alloc, &.{ dir_prefix, new }), 478 }, 479 + })) |prev_elem| { 480 + self.alloc.free(prev_elem.rename.old); 481 + self.alloc.free(prev_elem.rename.new); 482 + } 483 484 self.directories.cleanup(); 485 self.directories.populate_entries("") catch |err| { ··· 634 635 // Handle image. 636 if (config.show_images == true) unsupported_terminal: { 637 + if (!std.mem.eql(u8, self.last_item_path, self.current_item_path)) { 638 + var image = vaxis.zigimg.Image.fromFilePath(self.alloc, self.current_item_path) catch { 639 + break :unsupported_terminal; 640 + }; 641 + defer image.deinit(); 642 + if (self.vx.transmitImage(self.alloc, self.tty.anyWriter(), &image, .rgba)) |img| { 643 + self.image = img; 644 + } else |_| { 645 if (self.image) |img| { 646 + self.vx.freeImage(self.tty.anyWriter(), img.id); 647 } 648 + self.image = null; 649 + break :unsupported_terminal; 650 + } 651 652 + if (self.image) |img| { 653 + try img.draw(preview_win, .{ .scale = .contain }); 654 } 655 } 656 + 657 + break :file; 658 } 659 660 // Handle pdf. ··· 766 767 // Display info box. 768 if (self.notification.len > 0) { 769 + if (self.text_input.buf.realLength() > 0) { 770 self.text_input.clearAndFree(); 771 } 772 ··· 788 self.text_input.draw(info_win); 789 }, 790 .normal => { 791 + if (self.text_input.buf.realLength() > 0) { 792 self.text_input.draw(info_win); 793 } 794
+5 -1
src/circ_stack.zig
··· 17 self.count = 0; 18 } 19 20 - pub fn push(self: *Self, v: T) void { 21 self.buf[self.head] = v; 22 self.head = (self.head + 1) % capacity; 23 if (self.count != capacity) self.count += 1; 24 } 25 26 pub fn pop(self: *Self) ?T {
··· 17 self.count = 0; 18 } 19 20 + pub fn push(self: *Self, v: T) ?T { 21 + const prev_elem = if (self.count == capacity) self.buf[self.head] else null; 22 + 23 self.buf[self.head] = v; 24 self.head = (self.head + 1) % capacity; 25 if (self.count != capacity) self.count += 1; 26 + 27 + return prev_elem; 28 } 29 30 pub fn pop(self: *Self) ?T {
+4
src/notification.zig
··· 13 UnableToUndo, 14 UnableToOpenFile, 15 UnableToDeleteItem, 16 EditorNotSet, 17 ItemAlreadyExists, 18 UnableToRename, ··· 41 .UnknownError => self.write("An unknown error occurred.", .err), 42 .UnableToOpenFile => self.write("Unable to open file.", .err), 43 .UnableToDeleteItem => self.write("Unable to delete item.", .err), 44 .UnableToUndo => self.write("Unable to undo previous action.", .err), 45 .ItemAlreadyExists => self.write("Item already exists.", .err), 46 .UnableToRename => self.write("Unable to rename item.", .err), 47 .IncorrectPath => self.write("Unable to find path.", .err), 48 .EditorNotSet => self.write("$EDITOR is not set.", .err), 49 }; 50 } 51
··· 13 UnableToUndo, 14 UnableToOpenFile, 15 UnableToDeleteItem, 16 + UnableToDeleteAcrossMountPoints, 17 + UnsupportedImageFormat, 18 EditorNotSet, 19 ItemAlreadyExists, 20 UnableToRename, ··· 43 .UnknownError => self.write("An unknown error occurred.", .err), 44 .UnableToOpenFile => self.write("Unable to open file.", .err), 45 .UnableToDeleteItem => self.write("Unable to delete item.", .err), 46 + .UnableToDeleteAcrossMountPoints => self.write("Unable to move item to /tmp. Failed to delete.", .err), 47 .UnableToUndo => self.write("Unable to undo previous action.", .err), 48 .ItemAlreadyExists => self.write("Item already exists.", .err), 49 .UnableToRename => self.write("Unable to rename item.", .err), 50 .IncorrectPath => self.write("Unable to find path.", .err), 51 .EditorNotSet => self.write("$EDITOR is not set.", .err), 52 + .UnsupportedImageFormat => self.write("Unsupported image format.", .err), 53 }; 54 } 55