From 64c7c9338f121628c612a57742e49b681ce98a18 Mon Sep 17 00:00:00 2001 From: BrookJeynes Date: Tue, 25 Mar 2025 12:32:49 +1000 Subject: [PATCH] - feat: Added keybind `v` to view additional information about the selected entry. - feat: Added config option `true_dir_size` to see the true size of directories. - docs: Added default config values to readme. --- CHANGELOG.md | 4 ++ README.md | 45 ++++++++------ build.zig | 2 + build.zig.zon | 6 +- src/app.zig | 4 +- src/config.zig | 2 + src/directories.zig | 19 ++++++ src/drawer.zig | 131 +++++++++++++++++++++++++++++++++-------- src/event_handlers.zig | 1 + 9 files changed, 169 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d420b7..ae47799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.9.2 (2025-03-23) +- feat: Added keybind `v` to view additional information about the selected entry. +- feat: Added config option `true_dir_size` to see the true size of directories. + ## v0.9.1 (2025-03-23) - feat: File permissions are now displayed in the file information bar to the bottom of Jido. diff --git a/README.md b/README.md index da97dcf..ca41dba 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,11 @@ u :Undo delete/rename. d :Create directory. Will enter input mode. % :Create file. Will enter input mode. / :Fuzzy search directory. Will enter input mode. -. :Show/Hide hidden files. +. :Toggle hidden files. : :Allows for Jido commands to be entered. Please refer to the "Command mode" section for available commands. Will enter input mode. +v :Verbose mode. Provides more information about selected entry. Input mode: :Cancel input. @@ -73,34 +74,42 @@ Configure `jido` by editing the external configuration file located at either: - `$HOME/.jido/config.json` - `$XDG_CONFIG_HOME/jido/config.json`. -Jido will look for these env variables specifically. If they are not set, Jido will -not be able to find the config file. +Jido will look for these env variables specifically. If they are not set, Jido +will not be able to find the config file. An example config file can be found [here](https://github.com/BrookJeynes/jido/blob/main/example-config.json). Config schema: ``` Config = struct { - .show_hidden: bool, - .sort_dirs: bool, - .show_images: bool, - .preview_file: bool, - .empty_trash_on_exit: bool, + .show_hidden: bool = true, + .sort_dirs: bool = true, + .show_images: bool = true, -- Images are only supported in a terminal + supporting the `kitty image protocol`. + .preview_file: bool = true, + .empty_trash_on_exit: bool = false, -- Emptying the trash permanently deletes + all files within the trash. These + files are not recoverable past this + point. + .true_dir_size: bool = false, -- Display size of directory including + all its children. This can and will + cause lag on deeply nested directories. .keybinds: Keybinds, .styles: Styles } Keybinds = struct { - .toggle_hidden_files: Char, - .delete: Char, - .rename: Char, - .create_dir: Char, - .create_file: Char, - .fuzzy_find: Char, - .change_dir: Char, - .enter_command_mode: Char - .jump_top: Char - .jump_bottom: Char + .toggle_hidden_files: Char = '.', + .delete: Char = 'D', + .rename: Char = 'R', + .create_dir: Char = 'd', + .create_file: Char = '%', + .fuzzy_find: Char = '/', + .change_dir: Char = 'c', + .enter_command_mode: Char = ':', + .jump_top: Char = 'g', + .jump_bottom: Char = 'G', + .toggle_verbose_file_information: Char = 'v' } NotificationStyles = struct { diff --git a/build.zig b/build.zig index 98a6627..660740f 100644 --- a/build.zig +++ b/build.zig @@ -12,6 +12,7 @@ fn createExe(b: *std.Build, exe_name: []const u8, target: std.Build.ResolvedTarg const libvaxis = b.dependency("vaxis", .{ .target = target }).module("vaxis"); const fuzzig = b.dependency("fuzzig", .{ .target = target }).module("fuzzig"); const zuid = b.dependency("zuid", .{ .target = target }).module("zuid"); + const zeit = b.dependency("zeit", .{ .target = target }).module("zeit"); const exe = b.addExecutable(.{ .name = exe_name, @@ -23,6 +24,7 @@ fn createExe(b: *std.Build, exe_name: []const u8, target: std.Build.ResolvedTarg exe.root_module.addImport("vaxis", libvaxis); exe.root_module.addImport("fuzzig", fuzzig); exe.root_module.addImport("zuid", zuid); + exe.root_module.addImport("zeit", zeit); return exe; } diff --git a/build.zig.zon b/build.zig.zon index c066018..6baee13 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .jido, .fingerprint = 0xee45eabe36cafb57, - .version = "0.9.1", + .version = "0.9.2", .minimum_zig_version = "0.14.0", .dependencies = .{ @@ -17,6 +17,10 @@ .url = "git+https://github.com/KeithBrown39423/zuid#b6129f6cee45bd90b7ac97b8839dc28d21bedcb2", .hash = "zuid-2.0.0-AAAAADxXAAA4MAzwwRhfZ9AC2FMPZ8hUrZbfpmJ_azpK", }, + .zeit = .{ + .url = "git+https://github.com/rockorager/zeit/#175cf91a641790799e9d676878a9fe814aaed134", + .hash = "zeit-0.6.0-5I6bk5daAgC-P60TjxRqW0bYknfCGxJp-03eS9UjGrO7", + }, }, .paths = .{ diff --git a/src/app.zig b/src/app.zig index e2b5334..6a249f9 100644 --- a/src/app.zig +++ b/src/app.zig @@ -52,6 +52,7 @@ tty: vaxis.Tty = undefined, state: State = .normal, actions: CircStack(Action, actions_len), command_history: CommandHistory = CommandHistory{}, +drawer: Drawer = Drawer{}, directories: Directories, notification: Notification, @@ -114,7 +115,6 @@ pub fn deinit(self: *App) void { } pub fn run(self: *App) !void { - var drawer = Drawer{}; try self.directories.populateEntries(""); var loop: vaxis.Loop(Event) = .{ @@ -171,7 +171,7 @@ pub fn run(self: *App) !void { } } - try drawer.draw(self); + try self.drawer.draw(self); var buffered = self.tty.bufferedWriter(); try self.vx.render(buffered.writer().any()); diff --git a/src/config.zig b/src/config.zig index 628af5a..7ada7bb 100644 --- a/src/config.zig +++ b/src/config.zig @@ -16,6 +16,7 @@ const Config = struct { show_images: bool = true, preview_file: bool = true, empty_trash_on_exit: bool = false, + true_dir_size: bool = false, // TODO(10-01-25): This needs to be implemented. // command_history_len: usize = 10, styles: Styles = .{}, @@ -190,6 +191,7 @@ pub const Keybinds = struct { enter_command_mode: Char = @enumFromInt(':'), jump_top: Char = @enumFromInt('g'), jump_bottom: Char = @enumFromInt('G'), + toggle_verbose_file_information: Char = @enumFromInt('v'), }; const Styles = struct { diff --git a/src/directories.zig b/src/directories.zig index 006c62d..d7fd45d 100644 --- a/src/directories.zig +++ b/src/directories.zig @@ -71,6 +71,25 @@ pub fn fullPath(self: *Self, relative_path: []const u8) ![]const u8 { return try self.dir.realpath(relative_path, &self.path_buf); } +pub fn getDirSize(self: Self, dir: std.fs.Dir) !usize { + var total_size: usize = 0; + + var walker = try dir.walk(self.alloc); + defer walker.deinit(); + + while (try walker.next()) |entry| { + switch (entry.kind) { + .file => { + const stat = try entry.dir.statFile(entry.basename); + total_size += stat.size; + }, + else => {}, + } + } + + return total_size; +} + pub fn populateChildEntries( self: *Self, relative_path: []const u8, diff --git a/src/drawer.zig b/src/drawer.zig index e7a8ad3..040bcb9 100644 --- a/src/drawer.zig +++ b/src/drawer.zig @@ -6,12 +6,12 @@ const config = &@import("./config.zig").config; const vaxis = @import("vaxis"); const Git = @import("./git.zig"); const inputToSlice = @import("./event_handlers.zig").inputToSlice; +const zeit = @import("zeit"); const Drawer = @This(); const top_div: u16 = 1; const info_div: u16 = 1; -const bottom_div: u16 = 1; // Used to detect whether to re-render an image. current_item_path_buf: [std.fs.max_path_bytes]u8 = undefined, @@ -21,14 +21,15 @@ last_item_path: []u8 = "", file_info_buf: [std.fs.max_path_bytes]u8 = undefined, file_name_buf: [std.fs.max_path_bytes + 2]u8 = undefined, // +2 to accomodate for [] git_branch: [1024]u8 = undefined, +verbose: bool = false, pub fn draw(self: *Drawer, app: *App) !void { const win = app.vx.window(); win.clear(); const abs_file_path_bar = try self.drawAbsFilePath(app.alloc, &app.directories, win); - const file_info_bar = try self.drawFileInfo(&app.directories, win); - app.last_known_height = try drawDirList( + const file_info_bar = try self.drawFileInfo(app.alloc, &app.directories, win); + app.last_known_height = try self.drawDirList( &app.directories, win, abs_file_path_bar, @@ -81,6 +82,8 @@ fn drawFilePreview( win: vaxis.Window, file_name_win: vaxis.Window, ) !void { + const bottom_div: u16 = 1; + const preview_win = win.child(.{ .x_off = win.width / 2, .y_off = top_div + 1, @@ -116,7 +119,10 @@ fn drawFilePreview( } }, .file => file: { - var file = app.directories.dir.openFile(entry.name, .{ .mode = .read_only }) catch |err| { + var file = app.directories.dir.openFile( + entry.name, + .{ .mode = .read_only }, + ) catch |err| { switch (err) { error.AccessDenied => try app.notification.writeErr(.PermissionDenied), else => try app.notification.writeErr(.UnknownError), @@ -233,9 +239,12 @@ fn drawFilePreview( fn drawFileInfo( self: *Drawer, + alloc: std.mem.Allocator, directories: *Directories, win: vaxis.Window, ) !vaxis.Window { + const bottom_div: u16 = if (self.verbose) 6 else 1; + const file_info_win = win.child(.{ .x_off = 0, .y_off = win.height - bottom_div, @@ -250,13 +259,53 @@ fn drawFileInfo( }; var fbs = std.io.fixedBufferStream(&self.file_info_buf); + + // Selected entry. try fbs.writer().print( - "{d}/{d} ", - .{ directories.entries.selected + 1, directories.entries.len() }, + "{s}{d}/{d}{s}", + .{ + if (self.verbose) "Entry: " else "", + directories.entries.selected + 1, + directories.entries.len(), + if (self.verbose) "\n" else " ", + }, ); + // Time created / last modified + if (self.verbose) lbl: { + var maybe_meta: ?std.fs.File.Metadata = null; + if (entry.kind == .directory) { + maybe_meta = try directories.dir.metadata(); + } else if (entry.kind == .file) { + var file = try directories.dir.openFile(entry.name, .{}); + maybe_meta = try file.metadata(); + } + + const meta = maybe_meta orelse break :lbl; + var env = try std.process.getEnvMap(alloc); + defer env.deinit(); + const local = try zeit.local(alloc, &env); + defer local.deinit(); + + const ctime_instant = try zeit.instant(.{ + .source = .{ .unix_nano = meta.created().? }, + .timezone = &local, + }); + const ctime = ctime_instant.time(); + try ctime.strftime(fbs.writer().any(), "Created: %Y-%m-%d %H:%M:%S\n"); + + const mtime_instant = try zeit.instant(.{ + .source = .{ .unix_nano = meta.modified() }, + .timezone = &local, + }); + const mtime = mtime_instant.time(); + try mtime.strftime(fbs.writer().any(), "Last modified: %Y-%m-%d %H:%M:%S\n"); + } + + // File permissions. var file_perm_buf: [11]u8 = undefined; - const file_perms: ?usize = lbl: { + const file_perms: usize = lbl: { + if (self.verbose) try fbs.writer().writeAll("Permissions: "); var file_perm_fbs = std.io.fixedBufferStream(&file_perm_buf); if (entry.kind == .directory) { @@ -268,7 +317,10 @@ fn drawFileInfo( "r--", "r-x", "rw-", "rwx", }; - const stat = directories.dir.statFile(entry.name) catch break :lbl null; + const stat = directories.dir.statFile(entry.name) catch { + _ = try file_perm_fbs.write("---------\n"); + break :lbl 10; + }; // Ignore upper bytes as they represent file type. const perms = @as(u9, @truncate(stat.mode)); @@ -278,7 +330,11 @@ fn drawFileInfo( _ = try file_perm_fbs.write(perm_strings[perm]); } - _ = try file_perm_fbs.write(" "); + if (self.verbose) { + _ = try file_perm_fbs.write("\n"); + } else { + _ = try file_perm_fbs.write(" "); + } if (entry.kind == .directory) { break :lbl 11; @@ -286,24 +342,46 @@ fn drawFileInfo( break :lbl 10; } }; - if (file_perms) |perms_bytes| try fbs.writer().writeAll(file_perm_buf[0..perms_bytes]); - - if (entry.kind == .directory) { - _ = file_info_win.printSegment(.{ - .text = fbs.getWritten(), - .style = config.styles.file_information, - }, .{}); - return file_info_win; - } + try fbs.writer().writeAll(file_perm_buf[0..file_perms]); - const file_size: u64 = lbl: { - const stat = directories.dir.statFile(entry.name) catch break :lbl 0; - break :lbl stat.size; + // Size. + const size: ?usize = lbl: { + const stat = directories.dir.statFile(entry.name) catch break :lbl null; + if (entry.kind == .file) { + break :lbl stat.size; + } else if (entry.kind == .directory) { + if (config.true_dir_size) { + var dir = directories.dir.openDir( + entry.name, + .{ .iterate = true }, + ) catch break :lbl null; + defer dir.close(); + break :lbl directories.getDirSize(dir) catch break :lbl null; + } else { + break :lbl stat.size; + } + } + + break :lbl 0; }; - try fbs.writer().print("{:.2} ", .{std.fmt.fmtIntSizeDec(file_size)}); + if (size) |s| try fbs.writer().print("{s}{:.2}\n", .{ + if (self.verbose) "Size: " else "", + std.fmt.fmtIntSizeDec(s), + }); + // Extension. const extension = std.fs.path.extension(entry.name); - if (extension.len > 0) try fbs.writer().print("{s} ", .{extension}); + if (self.verbose) { + try fbs.writer().print( + "Extension: {s}\n", + .{if (entry.kind == .directory) "Dir" else extension}, + ); + } else { + try fbs.writer().print( + "{s} ", + .{if (entry.kind == .directory) "dir" else extension}, + ); + } _ = file_info_win.printSegment(.{ .text = fbs.getWritten(), @@ -319,6 +397,8 @@ fn drawDirList( abs_file_path: vaxis.Window, file_information: vaxis.Window, ) !u16 { + const bottom_div: u16 = 1; + const current_dir_list_win = win.child(.{ .x_off = 0, .y_off = top_div + 1, @@ -389,7 +469,10 @@ fn drawUserInput( if (text_input.buf.realLength() > 0) { text_input.drawWithStyle( user_input_win, - if (std.mem.eql(u8, input, ":UnsupportedCommand")) config.styles.text_input_err else config.styles.text_input, + if (std.mem.eql(u8, input, ":UnsupportedCommand")) + config.styles.text_input_err + else + config.styles.text_input, ); } diff --git a/src/event_handlers.zig b/src/event_handlers.zig index f69eddb..738770e 100644 --- a/src/event_handlers.zig +++ b/src/event_handlers.zig @@ -178,6 +178,7 @@ pub fn handleNormalEvent( app.directories.entries.selectLast(app.last_known_height); }, .jump_top => app.directories.entries.selectFirst(), + .toggle_verbose_file_information => app.drawer.verbose = !app.drawer.verbose, } } else { switch (key.codepoint) { -- 2.51.2