diff --git a/CHANGELOG.md b/CHANGELOG.md index f685c79..5312f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v0.9.8 (2025-04-04) +- fix: Ensure complete Git branch is displayed. +- refactor: Audit try usage to improve system resiliance. +- refactor: Removed need for enum based notifications. + ## v0.9.7 (2025-04-01) - feat: Added ability to copy folders. This is done by (y)anking the file, then (p)asting in the desired directory. diff --git a/PROJECT_BOARD.md b/PROJECT_BOARD.md index 7544779..3f0d44c 100644 --- a/PROJECT_BOARD.md +++ b/PROJECT_BOARD.md @@ -16,7 +16,7 @@ Key: - [x] Ability to unbind keys. ### Refactors -- [ ] Better error logging. +- [x] Better error logging. There are many places errors could be caught, logged, and handled instead of crashing. - [ ] Improve image reading. diff --git a/build.zig b/build.zig index 27a604b..2712253 100644 --- a/build.zig +++ b/build.zig @@ -2,7 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); ///Must match the `version` in `build.zig.zon`. -const version = std.SemanticVersion{ .major = 0, .minor = 9, .patch = 7 }; +const version = std.SemanticVersion{ .major = 0, .minor = 9, .patch = 8 }; const targets: []const std.Target.Query = &.{ .{ .cpu_arch = .aarch64, .os_tag = .macos }, diff --git a/build.zig.zon b/build.zig.zon index 1a49c70..0d70f85 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .jido, .fingerprint = 0xee45eabe36cafb57, - .version = "0.9.7", + .version = "0.9.8", .minimum_zig_version = "0.14.0", .dependencies = .{ diff --git a/src/app.zig b/src/app.zig index bce9bd2..d54cc53 100644 --- a/src/app.zig +++ b/src/app.zig @@ -162,8 +162,23 @@ pub fn deinit(self: *App) void { if (self.file_logger) |file_logger| file_logger.deinit(); } +pub fn inputToSlice(self: *App) []const u8 { + self.text_input.buf.cursor = self.text_input.buf.realLength(); + return self.text_input.sliceToCursor(&self.text_input_buf); +} + +pub fn repopulateDirectory(self: *App, fuzzy: []const u8) error{OutOfMemory}!void { + self.directories.clearEntries(); + self.directories.populateEntries(fuzzy) catch |err| { + const message = try std.fmt.allocPrint(self.alloc, "Failed to read directory entries - {}.", .{err}); + defer self.alloc.free(message); + self.notification.write(message, .err) catch {}; + if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + }; +} + pub fn run(self: *App) !void { - try self.directories.populateEntries(""); + try self.repopulateDirectory(""); var loop: vaxis.Loop(Event) = .{ .vaxis = &self.vx, @@ -188,19 +203,28 @@ pub fn run(self: *App) !void { if ((key.codepoint == 'r' and key.mods.ctrl)) { if (config.parse(self.alloc, self)) { - try self.notification.writeInfo(.ConfigReloaded); + self.notification.write("Reloaded configuration file.", .info) catch {}; } else |err| switch (err) { error.SyntaxError => { - try self.notification.writeErr(.ConfigSyntaxError); + self.notification.write("Encountered a syntax error while parsing the config file.", .err) catch { + std.log.err("Encountered a syntax error while parsing the config file.", .{}); + }; }, error.InvalidCharacter => { - try self.notification.writeErr(.InvalidKeybind); + self.notification.write("One or more overriden keybinds are invalid.", .err) catch { + std.log.err("One or more overriden keybinds are invalid.", .{}); + }; }, error.DuplicateKeybind => { // Error logged in function }, else => { - try self.notification.writeErr(.ConfigUnknownError); + const message = try std.fmt.allocPrint(self.alloc, "Encountend an unknown error while parsing the config file - {}", .{err}); + defer self.alloc.free(message); + + self.notification.write(message, .err) catch { + std.log.err("Encountend an unknown error while parsing the config file - {}", .{err}); + }; }, } } @@ -230,10 +254,31 @@ pub fn run(self: *App) !void { } if (config.empty_trash_on_exit) { - if (try config.trashDir()) |dir| { - var trash_dir = dir; - defer trash_dir.close(); - _ = try environment.deleteContents(trash_dir); + var trash_dir = dir: { + notfound: { + break :dir (config.trashDir() catch break :notfound) orelse break :notfound; + } + if (self.file_logger) |file_logger| file_logger.write("Failed to open trash directory.", .err) catch { + std.log.err("Failed to open trash directory.", .{}); + }; + return; + }; + defer trash_dir.close(); + + const failed = environment.deleteContents(trash_dir) catch |err| { + const message = try std.fmt.allocPrint(self.alloc, "Failed to empty trash - {}.", .{err}); + defer self.alloc.free(message); + if (self.file_logger) |file_logger| file_logger.write(message, .err) catch { + std.log.err("Failed to empty trash - {}.", .{err}); + }; + return; + }; + if (failed > 0) { + const message = try std.fmt.allocPrint(self.alloc, "Failed to empty {d} items from the trash.", .{failed}); + defer self.alloc.free(message); + if (self.file_logger) |file_logger| file_logger.write(message, .err) catch { + std.log.err("Failed to empty {d} items from the trash.", .{failed}); + }; } } } diff --git a/src/commands.zig b/src/commands.zig index 79c9fa6..47e893f 100644 --- a/src/commands.zig +++ b/src/commands.zig @@ -1,7 +1,7 @@ const std = @import("std"); const App = @import("app.zig"); const environment = @import("environment.zig"); -const _config = &@import("./config.zig").config; +const user_config = &@import("./config.zig").config; pub const CommandHistory = struct { const history_len = 10; @@ -45,71 +45,77 @@ pub const CommandHistory = struct { }; ///Navigate the user to the config dir. -pub fn config(app: *App) !void { +pub fn config(app: *App) error{OutOfMemory}!void { const dir = dir: { notfound: { - break :dir (_config.configDir() catch break :notfound) orelse break :notfound; + break :dir (user_config.configDir() catch break :notfound) orelse break :notfound; } - try app.notification.writeErr(.ConfigPathNotFound); + const message = try std.fmt.allocPrint(app.alloc, "Failed to navigate to config directory - unable to retrieve config directory.", .{}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; return; }; - app.directories.clearEntries(); + app.directories.dir.close(); app.directories.dir = dir; - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; + try app.repopulateDirectory(""); } ///Navigate the user to the trash dir. -pub fn trash(app: *App) !void { +pub fn trash(app: *App) error{OutOfMemory}!void { const dir = dir: { notfound: { - break :dir (_config.trashDir() catch break :notfound) orelse break :notfound; + break :dir (user_config.trashDir() catch break :notfound) orelse break :notfound; } - try app.notification.writeErr(.ConfigPathNotFound); + const message = try std.fmt.allocPrint(app.alloc, "Failed to navigate to trash directory - unable to retrieve trash directory.", .{}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; return; }; - app.directories.clearEntries(); + app.directories.dir.close(); app.directories.dir = dir; - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; + try app.repopulateDirectory(""); } ///Empty the trash. -pub fn emptyTrash(app: *App) !void { - const dir = dir: { +pub fn emptyTrash(app: *App) error{OutOfMemory}!void { + var message: ?[]const u8 = null; + defer if (message) |msg| app.alloc.free(msg); + + var dir = dir: { notfound: { - break :dir (_config.trashDir() catch break :notfound) orelse break :notfound; + break :dir (user_config.trashDir() catch break :notfound) orelse break :notfound; } - try app.notification.writeErr(.ConfigPathNotFound); + message = try std.fmt.allocPrint(app.alloc, "Failed to navigate to trash directory - unable to retrieve trash directory.", .{}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; return; }; + defer dir.close(); - var trash_dir = dir; - defer trash_dir.close(); - const failed = try environment.deleteContents(trash_dir); - if (failed > 0) try app.notification.writeErr(.FailedToDeleteSomeItems); - - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } + const failed = environment.deleteContents(dir) catch |err| lbl: { + message = try std.fmt.allocPrint(app.alloc, "Failed to empty trash - {}.", .{err}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + break :lbl 0; }; + if (failed > 0) { + message = try std.fmt.allocPrint(app.alloc, "Failed to empty {d} items from the trash.", .{failed}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + } + + try app.repopulateDirectory(""); } ///Change directory. -pub fn cd(app: *App, path: []const u8) !void { +pub fn cd(app: *App, path: []const u8) error{OutOfMemory}!void { + var message: ?[]const u8 = null; + defer if (message) |msg| app.alloc.free(msg); + var path_buf: [std.fs.max_path_bytes]u8 = undefined; const resolved_path = lbl: { const resolved_path = if (std.mem.startsWith(u8, path, "~")) path: { @@ -125,31 +131,22 @@ pub fn cd(app: *App, path: []const u8) !void { break :lbl app.directories.dir.realpath(resolved_path, &path_buf) catch path; }; - if (app.directories.dir.openDir(resolved_path, .{ .iterate = true })) |dir| { - app.directories.dir.close(); - app.directories.dir = dir; - - try app.notification.writeInfo(.ChangedDir); - - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } + const dir = app.directories.dir.openDir(resolved_path, .{ .iterate = true }) catch |err| { + message = switch (err) { + error.FileNotFound => try std.fmt.allocPrint(app.alloc, "Failed to navigate to '{s}' - directory does not exist.", .{resolved_path}), + error.NotDir => try std.fmt.allocPrint(app.alloc, "Failed to navigate to '{s}' - item is not a directory.", .{resolved_path}), + else => try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err}), }; - app.directories.history.reset(); - } else |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - error.FileNotFound => try app.notification.writeErr(.IncorrectPath), - error.NotDir => try app.notification.writeErr(.NotADir), - else => try app.notification.writeErr(.UnknownError), - } - } -} + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + app.directories.dir.close(); + app.directories.dir = dir; + + message = try std.fmt.allocPrint(app.alloc, "Navigated to directory '{s}'.", .{resolved_path}); + app.notification.write(message.?, .info) catch {}; -///Display help menu. -pub fn displayHelpMenu(app: *App) !void { - app.state = .help_menu; + try app.repopulateDirectory(""); + app.directories.history.reset(); } diff --git a/src/config.zig b/src/config.zig index 944355a..f06e67a 100644 --- a/src/config.zig +++ b/src/config.zig @@ -18,8 +18,6 @@ const Config = struct { 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 = .{}, keybinds: Keybinds = .{}, diff --git a/src/drawer.zig b/src/drawer.zig index bb512e8..d23cf4e 100644 --- a/src/drawer.zig +++ b/src/drawer.zig @@ -1,12 +1,12 @@ const std = @import("std"); const App = @import("./app.zig"); +const FileLogger = @import("./file_logger.zig"); const Notification = @import("./notification.zig"); const Directories = @import("./directories.zig"); const config = &@import("./config.zig").config; const vaxis = @import("vaxis"); const Git = @import("./git.zig"); const List = @import("./list.zig").List; -const inputToSlice = @import("./event_handlers.zig").inputToSlice; const zeit = @import("zeit"); const Drawer = @This(); @@ -24,7 +24,7 @@ file_name_buf: [std.fs.max_path_bytes + 2]u8 = undefined, // +2 to accomodate fo git_branch: [1024]u8 = undefined, verbose: bool = false, -pub fn draw(self: *Drawer, app: *App) !void { +pub fn draw(self: *Drawer, app: *App) error{ OutOfMemory, NoSpaceLeft }!void { const win = app.vx.window(); win.clear(); @@ -48,9 +48,9 @@ pub fn draw(self: *Drawer, app: *App) !void { return; } - const abs_file_path_bar = try self.drawAbsFilePath(app.alloc, &app.directories, win); + const abs_file_path_bar = try self.drawAbsFilePath(app, win); const file_info_bar = try self.drawFileInfo(app.alloc, &app.directories, win); - app.last_known_height = try drawDirList( + app.last_known_height = drawDirList( win, app.directories.entries, abs_file_path_bar, @@ -62,18 +62,18 @@ pub fn draw(self: *Drawer, app: *App) !void { try self.drawFilePreview(app, win, file_name_bar); } - const input = inputToSlice(app); - try drawUserInput(app.state, &app.text_input, input, win); + const input = app.inputToSlice(); + drawUserInput(app.state, &app.text_input, input, win); // Notification should be drawn last. - try drawNotification(&app.notification, win); + drawNotification(&app.notification, &app.file_logger, win); } fn drawFileName( self: *Drawer, directories: *Directories, win: vaxis.Window, -) !vaxis.Window { +) error{NoSpaceLeft}!vaxis.Window { const file_name_bar = win.child(.{ .x_off = win.width / 2, .y_off = 0, @@ -81,20 +81,13 @@ fn drawFileName( .height = top_div, }); - lbl: { - const entry = directories.getSelected() catch break :lbl; - if (entry) |e| { - const file_name = try std.fmt.bufPrint( - &self.file_name_buf, - "[{s}]", - .{e.name}, - ); - _ = file_name_bar.print(&.{vaxis.Segment{ - .text = file_name, - .style = config.styles.file_name, - }}, .{}); - } - } + const entry = lbl: { + const entry = directories.getSelected() catch return file_name_bar; + if (entry) |e| break :lbl e else return file_name_bar; + }; + + const file_name = try std.fmt.bufPrint(&self.file_name_buf, "[{s}]", .{entry.name}); + _ = file_name_bar.printSegment(.{ .text = file_name, .style = config.styles.file_name }, .{}); return file_name_bar; } @@ -104,7 +97,7 @@ fn drawFilePreview( app: *App, win: vaxis.Window, file_name_win: vaxis.Window, -) !void { +) error{ OutOfMemory, NoSpaceLeft }!void { const bottom_div: u16 = 1; const preview_win = win.child(.{ @@ -126,27 +119,43 @@ fn drawFilePreview( self.current_item_path = try std.fmt.bufPrint( &self.current_item_path_buf, "{s}/{s}", - .{ try app.directories.fullPath("."), entry.name }, + .{ app.directories.fullPath(".") catch { + const message = try std.fmt.allocPrint(app.alloc, "Can not display file - unable to retrieve directory path.", .{}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + + _ = preview_win.print(&.{ + .{ .text = "Can not display file - unable to retrieve directory path. No preview available." }, + }, .{}); + return; + }, entry.name }, ); switch (entry.kind) { .directory => { app.directories.clearChildEntries(); - if (app.directories.populateChildEntries(entry.name)) { - for (app.directories.child_entries.all(), 0..) |item, i| { - if (std.mem.startsWith(u8, item, ".") and config.show_hidden == false) { - continue; - } - if (i > preview_win.height) continue; - const w = preview_win.child(.{ .y_off = @intCast(i), .height = 1 }); - w.fill(vaxis.Cell{ .style = config.styles.list_item }); - _ = w.print(&.{.{ .text = item, .style = config.styles.list_item }}, .{}); - } - } else |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), + app.directories.populateChildEntries(entry.name) catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to populate child directory entries - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + + _ = preview_win.print(&.{ + .{ .text = "Failed to populate child directory entries. No preview available." }, + }, .{}); + + return; + }; + + for (app.directories.child_entries.all(), 0..) |item, i| { + if (std.mem.startsWith(u8, item, ".") and config.show_hidden == false) { + continue; } + if (i > preview_win.height) continue; + const w = preview_win.child(.{ .y_off = @intCast(i), .height = 1 }); + w.fill(vaxis.Cell{ .style = config.styles.list_item }); + _ = w.print(&.{.{ .text = item, .style = config.styles.list_item }}, .{}); } }, .file => file: { @@ -154,32 +163,37 @@ fn drawFilePreview( entry.name, .{ .mode = .read_only }, ) catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } + const message = try std.fmt.allocPrint(app.alloc, "Failed to open file - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; _ = preview_win.print(&.{ - .{ .text = "No preview available." }, + .{ .text = "Failed to open file. No preview available." }, }, .{}); break :file; }; defer file.close(); - const bytes = try file.readAll(&app.directories.file_contents); + const bytes = file.readAll(&app.directories.file_contents) catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to read file contents - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + + _ = preview_win.print(&.{ + .{ .text = "Failed to read file contents. No preview available." }, + }, .{}); + + break :file; + }; // Handle image. if (config.show_images == true) unsupported: { var match = false; inline for (@typeInfo(vaxis.zigimg.Image.Format).@"enum".fields) |field| { - const entry_ext = std.mem.trimLeft( - u8, - std.fs.path.extension(entry.name), - ".", - ); - if (std.mem.eql(u8, entry_ext, field.name)) { - match = true; - } + const entry_ext = std.mem.trimLeft(u8, std.fs.path.extension(entry.name), "."); + if (std.mem.eql(u8, entry_ext, field.name)) match = true; } if (!match) break :unsupported; @@ -204,7 +218,18 @@ fn drawFilePreview( } if (app.image) |img| { - try img.draw(preview_win, .{ .scale = .contain }); + img.draw(preview_win, .{ .scale = .contain }) catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + + _ = preview_win.print(&.{ + .{ .text = "Failed to draw image to screen. No preview available." }, + }, .{}); + + break :file; + }; } break :file; @@ -273,7 +298,7 @@ fn drawFileInfo( alloc: std.mem.Allocator, directories: *Directories, win: vaxis.Window, -) !vaxis.Window { +) error{NoSpaceLeft}!vaxis.Window { const bottom_div: u16 = if (self.verbose) 6 else 1; const file_info_win = win.child(.{ @@ -306,31 +331,31 @@ fn drawFileInfo( if (self.verbose) lbl: { var maybe_meta: ?std.fs.File.Metadata = null; if (entry.kind == .directory) { - maybe_meta = try directories.dir.metadata(); + maybe_meta = directories.dir.metadata() catch break :lbl; } else if (entry.kind == .file) { - var file = try directories.dir.openFile(entry.name, .{}); - maybe_meta = try file.metadata(); + var file = directories.dir.openFile(entry.name, .{}) catch break :lbl; + maybe_meta = file.metadata() catch break :lbl; } const meta = maybe_meta orelse break :lbl; - var env = try std.process.getEnvMap(alloc); + var env = std.process.getEnvMap(alloc) catch break :lbl; defer env.deinit(); - const local = try zeit.local(alloc, &env); + const local = zeit.local(alloc, &env) catch break :lbl; defer local.deinit(); - const ctime_instant = try zeit.instant(.{ + const ctime_instant = zeit.instant(.{ .source = .{ .unix_nano = meta.created().? }, .timezone = &local, - }); + }) catch break :lbl; const ctime = ctime_instant.time(); - try ctime.strftime(fbs.writer().any(), "Created: %Y-%m-%d %H:%M:%S\n"); + ctime.strftime(fbs.writer().any(), "Created: %Y-%m-%d %H:%M:%S\n") catch break :lbl; - const mtime_instant = try zeit.instant(.{ + const mtime_instant = zeit.instant(.{ .source = .{ .unix_nano = meta.modified() }, .timezone = &local, - }); + }) catch break :lbl; const mtime = mtime_instant.time(); - try mtime.strftime(fbs.writer().any(), "Last modified: %Y-%m-%d %H:%M:%S\n"); + mtime.strftime(fbs.writer().any(), "Last modified: %Y-%m-%d %H:%M:%S\n") catch break :lbl; } // File permissions. @@ -427,7 +452,7 @@ fn drawDirList( list: List(std.fs.Dir.Entry), abs_file_path: vaxis.Window, file_information: vaxis.Window, -) !u16 { +) u16 { const bottom_div: u16 = 1; const current_dir_list_win = win.child(.{ @@ -470,10 +495,9 @@ fn drawDirList( fn drawAbsFilePath( self: *Drawer, - alloc: std.mem.Allocator, - directories: *Directories, + app: *App, win: vaxis.Window, -) !vaxis.Window { +) error{ OutOfMemory, NoSpaceLeft }!vaxis.Window { const abs_file_path_bar = win.child(.{ .x_off = 0, .y_off = 0, @@ -481,8 +505,8 @@ fn drawAbsFilePath( .height = top_div, }); - const branch_alloc = try Git.getGitBranch(alloc, directories.dir); - defer if (branch_alloc) |b| alloc.free(b); + const branch_alloc = Git.getGitBranch(app.alloc, app.directories.dir) catch null; + defer if (branch_alloc) |b| app.alloc.free(b); const branch = if (branch_alloc) |b| try std.fmt.bufPrint( &self.git_branch, @@ -493,7 +517,13 @@ fn drawAbsFilePath( ""; _ = abs_file_path_bar.print(&.{ - vaxis.Segment{ .text = try directories.fullPath(".") }, + vaxis.Segment{ .text = app.directories.fullPath(".") catch { + const message = try std.fmt.allocPrint(app.alloc, "Can not display absolute file path - unable to retrieve full path.", .{}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + return abs_file_path_bar; + } }, vaxis.Segment{ .text = if (branch_alloc != null) " on " else "" }, vaxis.Segment{ .text = branch, .style = config.styles.git_branch }, }, .{}); @@ -506,7 +536,7 @@ fn drawUserInput( text_input: *vaxis.widgets.TextInput, input: []const u8, win: vaxis.Window, -) !void { +) void { const user_input_win = win.child(.{ .x_off = 0, .y_off = top_div, @@ -540,8 +570,9 @@ fn drawUserInput( fn drawNotification( notification: *Notification, + file_logger: *?FileLogger, win: vaxis.Window, -) !void { +) void { if (notification.len() == 0) return; if (notification.clearIfEnded()) return; @@ -552,7 +583,10 @@ fn drawNotification( const max_width = win.width / 4; const width = notification.len() + width_padding; const calculated_width = if (width > max_width) max_width else width; - const height = try std.math.divCeil(usize, notification.len(), calculated_width) + height_padding; + const height = (std.math.divCeil(usize, notification.len(), calculated_width) catch { + if (file_logger.*) |fl| fl.write("Unable to display notification - failed to calculate notification height.", .err) catch {}; + return; + }) + height_padding; const notification_win = win.child(.{ .x_off = @intCast(win.width - (calculated_width + screen_pos_padding)), diff --git a/src/environment.zig b/src/environment.zig index 55cb4f9..8849061 100644 --- a/src/environment.zig +++ b/src/environment.zig @@ -28,7 +28,7 @@ pub fn checkDuplicatePath( buf: []u8, dir: std.fs.Dir, relative_path: []const u8, -) std.fmt.BufPrintError!struct { +) error{NoSpaceLeft}!struct { path: []const u8, had_duplicate: bool, } { diff --git a/src/event_handlers.zig b/src/event_handlers.zig index 2a39d1e..5e1585b 100644 --- a/src/event_handlers.zig +++ b/src/event_handlers.zig @@ -7,11 +7,7 @@ const Key = vaxis.Key; const config = &@import("./config.zig").config; const commands = @import("./commands.zig"); const Keybinds = @import("./config.zig").Keybinds; - -pub fn inputToSlice(self: *App) []const u8 { - self.text_input.buf.cursor = self.text_input.buf.realLength(); - return self.text_input.sliceToCursor(&self.text_input_buf); -} +const events = @import("./events.zig"); pub fn handleNormalEvent( app: *App, @@ -37,128 +33,30 @@ pub fn handleNormalEvent( if (maybe_remap) |action| { switch (action) { - .toggle_hidden_files => { - config.show_hidden = !config.show_hidden; - - const prev_selected_name: []const u8, const prev_selected_err: bool = lbl: { - const selected = app.directories.getSelected() catch break :lbl .{ "", true }; - if (selected == null) break :lbl .{ "", true }; - - break :lbl .{ try app.alloc.dupe(u8, selected.?.name), false }; - }; - defer if (!prev_selected_err) app.alloc.free(prev_selected_name); - - app.directories.clearEntries(); - app.text_input.clearAndFree(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; - - for (app.directories.entries.all()) |entry| { - if (std.mem.eql(u8, entry.name, prev_selected_name)) return; - app.directories.entries.selected += 1; - } - - // If it didn't find entry, reset selected. - app.directories.entries.selected = 0; - }, - .delete => { - const entry = lbl: { - const entry = app.directories.getSelected() catch { - try app.notification.writeErr(.UnableToDelete); - return; - }; - if (entry) |e| break :lbl e else return; - }; - - var prev_path_buf: [std.fs.max_path_bytes]u8 = undefined; - const prev_path = try app.alloc.dupe(u8, try app.directories.dir.realpath(entry.name, &prev_path_buf)); - - var trash_dir = dir: { - notfound: { - break :dir (config.trashDir() catch break :notfound) orelse break :notfound; - } - app.alloc.free(prev_path); - try app.notification.writeErr(.ConfigPathNotFound); - return; - }; - defer trash_dir.close(); - var trash_dir_path_buf: [std.fs.max_path_bytes]u8 = undefined; - const trash_dir_path = try trash_dir.realpath(".", &trash_dir_path_buf); - - if (std.mem.eql(u8, prev_path, trash_dir_path)) { - try app.notification.writeErr(.CannotDeleteTrashDir); - app.alloc.free(prev_path); + .toggle_hidden_files => try events.toggleHiddenFiles(app), + .delete => try events.delete(app), + .rename => { + const entry = (app.directories.getSelected() catch { + app.notification.write("Can not rename item - no item selected.", .warn) catch {}; return; - } + }) orelse return; - var tmp_path_buf: [std.fs.max_path_bytes]u8 = undefined; - const tmp_path = try app.alloc.dupe(u8, try std.fmt.bufPrint(&tmp_path_buf, "{s}/{s}-{s}", .{ trash_dir_path, entry.name, zuid.new.v4() })); - - if (app.directories.dir.rename(entry.name, tmp_path)) { - if (app.actions.push(.{ - .delete = .{ .prev_path = prev_path, .new_path = tmp_path }, - })) |prev_elem| { - app.alloc.free(prev_elem.delete.prev_path); - app.alloc.free(prev_elem.delete.new_path); - } - - try app.notification.writeInfo(.Deleted); - app.directories.removeSelected(); - } else |err| { - switch (err) { - error.RenameAcrossMountPoints => try app.notification.writeErr(.UnableToDeleteAcrossMountPoints), - else => try app.notification.writeErr(.UnableToDelete), - } - app.alloc.free(prev_path); - app.alloc.free(tmp_path); - } - }, - .rename => { app.text_input.clearAndFree(); - app.state = .rename; - - const entry = lbl: { - const entry = app.directories.getSelected() catch { - app.state = .normal; - try app.notification.writeErr(.UnableToRename); - return; - }; - if (entry) |e| break :lbl e else { - app.state = .normal; - return; - } - }; - app.text_input.insertSliceAtCursor(entry.name) catch { - app.state = .normal; - try app.notification.writeErr(.UnableToRename); - return; - }; + // Try insert entry name into text input for a nicer experience. + // This failing shouldn't stop the user from entering a new name. + app.text_input.insertSliceAtCursor(entry.name) catch {}; + app.state = .rename; }, + .create_dir => { + try app.repopulateDirectory(""); app.text_input.clearAndFree(); - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; app.state = .new_dir; }, .create_file => { + try app.repopulateDirectory(""); app.text_input.clearAndFree(); - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; app.state = .new_file; }, .fuzzy_find => { @@ -177,326 +75,17 @@ pub fn handleNormalEvent( .jump_bottom => app.directories.entries.selectLast(), .jump_top => app.directories.entries.selectFirst(), .toggle_verbose_file_information => app.drawer.verbose = !app.drawer.verbose, - .force_delete => { - const entry = lbl: { - const entry = app.directories.getSelected() catch { - try app.notification.writeErr(.UnableToDelete); - return; - }; - if (entry) |e| break :lbl e else return; - }; - - app.directories.dir.deleteTree(entry.name) catch { - try app.notification.writeErr(.UnableToDelete); - return; - }; - app.directories.removeSelected(); - }, - .yank => { - if (app.yanked) |yanked| { - app.alloc.free(yanked.dir); - app.alloc.free(yanked.entry.name); - } - - app.yanked = lbl: { - const entry = (app.directories.getSelected() catch { - app.notification.write("Failed to yank item - no item selected.", .warn) catch {}; - break :lbl null; - }) orelse break :lbl null; - - switch (entry.kind) { - .file => { - break :lbl .{ - .dir = try app.alloc.dupe(u8, app.directories.fullPath(".") catch { - const message = try std.fmt.allocPrint( - app.alloc, - "Failed to yank '{s}' - unable to retrieve directory path.", - .{entry.name}, - ); - defer app.alloc.free(message); - app.notification.write(message, .err) catch {}; - if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; - break :lbl null; - }), - .entry = .{ - .kind = entry.kind, - .name = try app.alloc.dupe(u8, entry.name), - }, - }; - }, - else => { - const message = try std.fmt.allocPrint( - app.alloc, - "Failed to yank '{s}' - unsupported file type '{s}'.", - .{ entry.name, @tagName(entry.kind) }, - ); - defer app.alloc.free(message); - app.notification.write(message, .warn) catch {}; - break :lbl null; - }, - } - }; - if (app.yanked) |y| { - const message = try std.fmt.allocPrint(app.alloc, "Yanked '{s}'.", .{y.entry.name}); - defer app.alloc.free(message); - app.notification.write(message, .info) catch {}; - } - }, - .paste => { - const yanked = if (app.yanked) |y| y else return; - - var new_path_buf: [std.fs.max_path_bytes]u8 = undefined; - const new_path_res = environment.checkDuplicatePath(&new_path_buf, app.directories.dir, yanked.entry.name) catch { - const message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - path too long.", .{yanked.entry.name}); - defer app.alloc.free(message); - app.notification.write(message, .err) catch {}; - return; - }; - - switch (yanked.entry.kind) { - .file => { - var source_dir = std.fs.openDirAbsolute(yanked.dir, .{ .iterate = true }) catch { - const message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.dir }); - defer app.alloc.free(message); - app.notification.write(message, .err) catch {}; - if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; - return; - }; - defer source_dir.close(); - std.fs.Dir.copyFile( - source_dir, - yanked.entry.name, - app.directories.dir, - new_path_res.path, - .{}, - ) catch |err| switch (err) { - error.FileNotFound => { - const message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - the original file was deleted or moved.", .{yanked.entry.name}); - defer app.alloc.free(message); - app.notification.write(message, .err) catch {}; - if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; - return; - }, - else => { - const message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - {}.", .{ yanked.entry.name, err }); - defer app.alloc.free(message); - app.notification.write(message, .err) catch {}; - if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; - return; - }, - }; - - // Append action to undo history. - { - var new_path_abs_buf: [std.fs.max_path_bytes]u8 = undefined; - const new_path_abs = app.directories.dir.realpath(new_path_res.path, &new_path_abs_buf) catch { - const message = try std.fmt.allocPrint( - app.alloc, - "Failed to push copy action for '{s}' to undo history - unable to retrieve absolute directory path for '{s}'. This action will not be able to be undone via the `undo` keybind.", - .{ new_path_res.path, yanked.entry.name }, - ); - defer app.alloc.free(message); - app.notification.write(message, .err) catch {}; - if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; - return; - }; - - if (app.actions.push(.{ - .paste = try app.alloc.dupe(u8, new_path_abs), - })) |prev_elem| { - app.alloc.free(prev_elem.delete.prev_path); - app.alloc.free(prev_elem.delete.new_path); - } - } - - const message = try std.fmt.allocPrint(app.alloc, "Copied '{s}'.", .{yanked.entry.name}); - defer app.alloc.free(message); - app.notification.write(message, .info) catch {}; - }, - else => { - const message = try std.fmt.allocPrint( - app.alloc, - "Failed to copy '{s}' - unsupported file type '{s}'.", - .{ yanked.entry.name, @tagName(yanked.entry.kind) }, - ); - defer app.alloc.free(message); - app.notification.write(message, .warn) catch {}; - }, - } - - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => app.notification.writeErr(.PermissionDenied) catch {}, - else => app.notification.writeErr(.UnknownError) catch {}, - } - }; - }, + .force_delete => try events.forceDelete(app), + .yank => try events.yank(app), + .paste => try events.paste(app), } } else { switch (key.codepoint) { - '-', 'h', Key.left => { - app.text_input.clearAndFree(); - - if (app.directories.dir.openDir("../", .{ .iterate = true })) |dir| { - app.directories.dir.close(); - app.directories.dir = dir; - - app.directories.clearEntries(); - const fuzzy = inputToSlice(app); - app.directories.populateEntries(fuzzy) catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; - - if (app.directories.history.pop()) |history| { - if (history < app.directories.entries.len()) { - app.directories.entries.selected = history; - } - } - } else |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - } - }, - Key.enter, 'l', Key.right => { - const entry = lbl: { - const entry = app.directories.getSelected() catch return; - if (entry) |e| break :lbl e else return; - }; - - switch (entry.kind) { - .directory => { - app.text_input.clearAndFree(); - - if (app.directories.dir.openDir(entry.name, .{ .iterate = true })) |dir| { - app.directories.dir.close(); - app.directories.dir = dir; - - _ = app.directories.history.push(app.directories.entries.selected); - - app.directories.clearEntries(); - const fuzzy = inputToSlice(app); - app.directories.populateEntries(fuzzy) catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; - } else |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - } - }, - .file => { - if (environment.getEditor()) |editor| { - try app.vx.exitAltScreen(app.tty.anyWriter()); - try app.vx.resetState(app.tty.anyWriter()); - loop.stop(); - - environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch { - try app.notification.writeErr(.UnableToOpenFile); - }; - - try loop.start(); - try app.vx.enterAltScreen(app.tty.anyWriter()); - try app.vx.enableDetectedFeatures(app.tty.anyWriter()); - app.vx.queueRefresh(); - } else { - try app.notification.writeErr(.EditorNotSet); - } - }, - else => {}, - } - }, - 'j', Key.down => { - app.directories.entries.next(); - }, - 'k', Key.up => { - app.directories.entries.previous(); - }, - 'u' => { - if (app.actions.pop()) |action| { - const selected = app.directories.entries.selected; - - switch (action) { - .delete => |a| { - defer app.alloc.free(a.new_path); - defer app.alloc.free(a.prev_path); - - var new_path_buf: [std.fs.max_path_bytes]u8 = undefined; - const new_path_res = try environment.checkDuplicatePath(&new_path_buf, app.directories.dir, a.prev_path); - - if (app.directories.dir.rename(a.new_path, new_path_res.path)) { - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; - if (new_path_res.had_duplicate) { - try app.notification.writeWarn(.DuplicateFileOnUndo); - } else { - try app.notification.writeInfo(.RestoredDelete); - } - } else |_| { - try app.notification.writeErr(.UnableToUndo); - } - }, - .rename => |a| { - defer app.alloc.free(a.new_path); - defer app.alloc.free(a.prev_path); - - var new_path_buf: [std.fs.max_path_bytes]u8 = undefined; - const new_path_res = try environment.checkDuplicatePath(&new_path_buf, app.directories.dir, a.prev_path); - - if (app.directories.dir.rename(a.new_path, new_path_res.path)) { - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; - if (new_path_res.had_duplicate) { - try app.notification.writeWarn(.DuplicateFileOnUndo); - } else { - try app.notification.writeInfo(.RestoredRename); - } - } else |_| { - try app.notification.writeErr(.UnableToUndo); - } - }, - .paste => |path| { - defer app.alloc.free(path); - - app.directories.dir.deleteTree(path) catch { - try app.notification.writeErr(.UnableToUndo); - return; - }; - - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; - }, - } - - app.directories.entries.selected = selected; - } else { - try app.notification.writeInfo(.EmptyUndo); - } - }, + '-', 'h', Key.left => try events.traverseLeft(app), + Key.enter, 'l', Key.right => try events.traverseRight(app, loop), + 'j', Key.down => app.directories.entries.next(), + 'k', Key.up => app.directories.entries.previous(), + 'u' => try events.undo(app), else => {}, } } @@ -512,13 +101,8 @@ pub fn handleInputEvent(app: *App, event: App.Event) !void { Key.escape => { switch (app.state) { .fuzzy => { - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; + try app.repopulateDirectory(""); + app.text_input.clearAndFree(); }, .command => app.command_history.resetSelected(), else => {}, @@ -530,103 +114,18 @@ pub fn handleInputEvent(app: *App, event: App.Event) !void { Key.enter => { const selected = app.directories.entries.selected; switch (app.state) { - .new_dir => { - const dir = inputToSlice(app); - if (app.directories.dir.makeDir(dir)) { - try app.notification.writeInfo(.CreatedFolder); - - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; - } else |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - error.PathAlreadyExists => try app.notification.writeErr(.ItemAlreadyExists), - else => try app.notification.writeErr(.UnknownError), - } - } - app.text_input.clearAndFree(); - }, - .new_file => { - const file = inputToSlice(app); - if (environment.fileExists(app.directories.dir, file)) { - try app.notification.writeErr(.ItemAlreadyExists); - } else { - if (app.directories.dir.createFile(file, .{})) |f| { - f.close(); - - try app.notification.writeInfo(.CreatedFile); - - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; - } else |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - } - } - app.text_input.clearAndFree(); - }, - .rename => { - var dir_prefix_buf: [std.fs.max_path_bytes]u8 = undefined; - const dir_prefix = try app.directories.dir.realpath(".", &dir_prefix_buf); - - const entry = lbl: { - const entry = app.directories.getSelected() catch { - try app.notification.writeErr(.UnableToRename); - return; - }; - if (entry) |e| break :lbl e else return; - }; - const new_path = inputToSlice(app); - - if (environment.fileExists(app.directories.dir, new_path)) { - try app.notification.writeErr(.ItemAlreadyExists); - } else { - app.directories.dir.rename(entry.name, new_path) catch |err| switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - error.PathAlreadyExists => try app.notification.writeErr(.ItemAlreadyExists), - else => try app.notification.writeErr(.UnknownError), - }; - if (app.actions.push(.{ - .rename = .{ - .prev_path = try std.fs.path.join(app.alloc, &.{ dir_prefix, entry.name }), - .new_path = try std.fs.path.join(app.alloc, &.{ dir_prefix, new_path }), - }, - })) |prev_elem| { - app.alloc.free(prev_elem.rename.prev_path); - app.alloc.free(prev_elem.rename.new_path); - } - - try app.notification.writeInfo(.Renamed); - - app.directories.clearEntries(); - app.directories.populateEntries("") catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; - } - app.text_input.clearAndFree(); - }, + .new_dir => try events.createNewDir(app), + .new_file => try events.createNewFile(app), + .rename => try events.rename(app), .change_dir => { - const path = inputToSlice(app); + const path = app.inputToSlice(); try commands.cd(app, path); app.text_input.clearAndFree(); }, .command => { - const command = inputToSlice(app); + const command = app.inputToSlice(); + + // Push command to history if it's not empty. if (!std.mem.eql(u8, std.mem.trim(u8, command, " "), ":")) { if (app.command_history.push(try app.alloc.dupe(u8, command))) |deleted| { app.alloc.free(deleted); @@ -650,8 +149,7 @@ pub fn handleInputEvent(app: *App, event: App.Event) !void { } if (std.mem.startsWith(u8, command, ":cd ")) { - const path = std.mem.trim(u8, command, ":cd \n\r"); - try commands.cd(app, path); + try commands.cd(app, command[":cd ".len..]); break :supported; } @@ -661,7 +159,7 @@ pub fn handleInputEvent(app: *App, event: App.Event) !void { } if (std.mem.eql(u8, command, ":h")) { - try commands.displayHelpMenu(app); + app.state = .help_menu; break :supported; } @@ -674,8 +172,6 @@ pub fn handleInputEvent(app: *App, event: App.Event) !void { else => {}, } - // TODO(2025-03-29): Could there be a better way to check - // this? if (app.state != .help_menu) app.state = .normal; app.directories.entries.selected = selected; }, @@ -685,7 +181,12 @@ pub fn handleInputEvent(app: *App, event: App.Event) !void { if (app.state == .command) { if (app.command_history.next()) |command| { app.text_input.clearAndFree(); - try app.text_input.insertSliceAtCursor(command); + app.text_input.insertSliceAtCursor(command) catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to get previous command history - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + }; } } }, @@ -693,9 +194,19 @@ pub fn handleInputEvent(app: *App, event: App.Event) !void { if (app.state == .command) { app.text_input.clearAndFree(); if (app.command_history.previous()) |command| { - try app.text_input.insertSliceAtCursor(command); + app.text_input.insertSliceAtCursor(command) catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to get next command history - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + }; } else { - try app.text_input.insertSliceAtCursor(":"); + app.text_input.insertSliceAtCursor(":") catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to get next command history - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + }; } } }, @@ -704,20 +215,21 @@ pub fn handleInputEvent(app: *App, event: App.Event) !void { switch (app.state) { .fuzzy => { - app.directories.clearEntries(); - const fuzzy = inputToSlice(app); - app.directories.populateEntries(fuzzy) catch |err| { - switch (err) { - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), - else => try app.notification.writeErr(.UnknownError), - } - }; + const fuzzy = app.inputToSlice(); + try app.repopulateDirectory(fuzzy); }, .command => { - const command = inputToSlice(app); + const command = app.inputToSlice(); if (!std.mem.startsWith(u8, command, ":")) { app.text_input.clearAndFree(); - try app.text_input.insertSliceAtCursor(":"); + app.text_input.insertSliceAtCursor(":") catch |err| { + app.state = .normal; + + const message = try std.fmt.allocPrint(app.alloc, "An input error occurred while attempting to enter a command - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + }; } }, else => {}, @@ -734,12 +246,8 @@ pub fn handleHelpMenuEvent(app: *App, event: App.Event) !void { .key_press => |key| { switch (key.codepoint) { Key.escape, 'q' => app.state = .normal, - 'j', Key.down => { - app.help_menu.next(); - }, - 'k', Key.up => { - app.help_menu.previous(); - }, + 'j', Key.down => app.help_menu.next(), + 'k', Key.up => app.help_menu.previous(), else => {}, } }, diff --git a/src/events.zig b/src/events.zig new file mode 100644 index 0000000..ea18cf9 --- /dev/null +++ b/src/events.zig @@ -0,0 +1,488 @@ +const std = @import("std"); +const App = @import("./app.zig"); +const config = &@import("./config.zig").config; +const zuid = @import("zuid"); +const environment = @import("./environment.zig"); +const vaxis = @import("vaxis"); + +pub fn delete(app: *App) error{OutOfMemory}!void { + var message: ?[]const u8 = null; + defer if (message) |msg| app.alloc.free(msg); + + const entry = (app.directories.getSelected() catch { + app.notification.write("Can not to delete item - no item selected.", .warn) catch {}; + return; + }) orelse return; + + var prev_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const prev_path = app.directories.dir.realpath(entry.name, &prev_path_buf) catch { + message = try std.fmt.allocPrint(app.alloc, "Failed to delete '{s}' - unable to retrieve absolute path.", .{entry.name}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + const prev_path_alloc = try app.alloc.dupe(u8, prev_path); + + var trash_dir = dir: { + notfound: { + break :dir (config.trashDir() catch break :notfound) orelse break :notfound; + } + app.alloc.free(prev_path_alloc); + message = try std.fmt.allocPrint(app.alloc, "Failed to delete '{s}' - unable to retrieve trash directory.", .{entry.name}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + defer trash_dir.close(); + + var trash_dir_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const trash_dir_path = trash_dir.realpath(".", &trash_dir_path_buf) catch { + message = try std.fmt.allocPrint(app.alloc, "Failed to delete '{s}' - unable to retrieve absolute path for trash directory.", .{entry.name}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + if (std.mem.eql(u8, prev_path_alloc, trash_dir_path)) { + app.notification.write("Can not delete trash directory.", .warn) catch {}; + app.alloc.free(prev_path_alloc); + return; + } + + const tmp_path = try std.fmt.allocPrint(app.alloc, "{s}/{s}-{s}", .{ trash_dir_path, entry.name, zuid.new.v4() }); + if (app.directories.dir.rename(entry.name, tmp_path)) { + if (app.actions.push(.{ + .delete = .{ .prev_path = prev_path_alloc, .new_path = tmp_path }, + })) |prev_elem| { + app.alloc.free(prev_elem.delete.prev_path); + app.alloc.free(prev_elem.delete.new_path); + } + message = try std.fmt.allocPrint(app.alloc, "Deleted '{s}'.", .{entry.name}); + app.notification.write(message.?, .info) catch {}; + + app.directories.removeSelected(); + } else |err| { + app.alloc.free(prev_path_alloc); + app.alloc.free(tmp_path); + + message = try std.fmt.allocPrint(app.alloc, "Failed to delete '{s}' - {}.", .{ entry.name, err }); + app.notification.write(message.?, .err) catch {}; + } +} + +pub fn rename(app: *App) error{OutOfMemory}!void { + var message: ?[]const u8 = null; + defer if (message) |msg| app.alloc.free(msg); + + const entry = (app.directories.getSelected() catch { + app.notification.write("Can not to rename item - no item selected.", .warn) catch {}; + return; + }) orelse return; + + var dir_prefix_buf: [std.fs.max_path_bytes]u8 = undefined; + const dir_prefix = app.directories.dir.realpath(".", &dir_prefix_buf) catch { + message = try std.fmt.allocPrint(app.alloc, "Failed to rename '{s}' - unable to retrieve absolute path.", .{entry.name}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + const new_path = app.inputToSlice(); + + if (environment.fileExists(app.directories.dir, new_path)) { + message = try std.fmt.allocPrint(app.alloc, "Can not rename file - '{s}' already exists.", .{new_path}); + app.notification.write(message.?, .warn) catch {}; + } else { + app.directories.dir.rename(entry.name, new_path) catch |err| { + message = try std.fmt.allocPrint(app.alloc, "Failed to rename '{s}' - {}.", .{ new_path, err }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + if (app.actions.push(.{ + .rename = .{ + .prev_path = try std.fs.path.join(app.alloc, &.{ dir_prefix, entry.name }), + .new_path = try std.fs.path.join(app.alloc, &.{ dir_prefix, new_path }), + }, + })) |prev_elem| { + app.alloc.free(prev_elem.rename.prev_path); + app.alloc.free(prev_elem.rename.new_path); + } + + try app.repopulateDirectory(""); + app.text_input.clearAndFree(); + + message = try std.fmt.allocPrint(app.alloc, "Renamed '{s}' to '{s}'.", .{ entry.name, new_path }); + app.notification.write(message.?, .info) catch {}; + } + + app.text_input.clearAndFree(); +} + +pub fn forceDelete(app: *App) error{OutOfMemory}!void { + const entry = (app.directories.getSelected() catch { + app.notification.write("Can not force delete item - no item selected.", .warn) catch {}; + return; + }) orelse return; + + app.directories.dir.deleteTree(entry.name) catch |err| { + const error_message = try std.fmt.allocPrint(app.alloc, "Failed to force delete '{s}' - {}.", .{ entry.name, err }); + app.notification.write(error_message, .err) catch {}; + return; + }; + + app.directories.removeSelected(); +} + +pub fn toggleHiddenFiles(app: *App) error{OutOfMemory}!void { + config.show_hidden = !config.show_hidden; + + const prev_selected_name: []const u8, const prev_selected_err: bool = lbl: { + const selected = app.directories.getSelected() catch break :lbl .{ "", true }; + if (selected == null) break :lbl .{ "", true }; + + break :lbl .{ try app.alloc.dupe(u8, selected.?.name), false }; + }; + defer if (!prev_selected_err) app.alloc.free(prev_selected_name); + + try app.repopulateDirectory(""); + app.text_input.clearAndFree(); + + for (app.directories.entries.all()) |entry| { + if (std.mem.eql(u8, entry.name, prev_selected_name)) return; + app.directories.entries.selected += 1; + } + + // If it didn't find entry, reset selected. + app.directories.entries.selected = 0; +} + +pub fn yank(app: *App) error{OutOfMemory}!void { + var message: ?[]const u8 = null; + defer if (message) |msg| app.alloc.free(msg); + + if (app.yanked) |yanked| { + app.alloc.free(yanked.dir); + app.alloc.free(yanked.entry.name); + } + + app.yanked = lbl: { + const entry = (app.directories.getSelected() catch { + app.notification.write("Can not yank item - no item selected.", .warn) catch {}; + break :lbl null; + }) orelse break :lbl null; + + switch (entry.kind) { + .file => { + break :lbl .{ + .dir = try app.alloc.dupe(u8, app.directories.fullPath(".") catch { + message = try std.fmt.allocPrint( + app.alloc, + "Failed to yank '{s}' - unable to retrieve directory path.", + .{entry.name}, + ); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + break :lbl null; + }), + .entry = .{ + .kind = entry.kind, + .name = try app.alloc.dupe(u8, entry.name), + }, + }; + }, + else => { + message = try std.fmt.allocPrint(app.alloc, "Can not yank '{s}' - unsupported file type '{s}'.", .{ entry.name, @tagName(entry.kind) }); + app.notification.write(message.?, .warn) catch {}; + break :lbl null; + }, + } + }; + + if (app.yanked) |y| { + message = try std.fmt.allocPrint(app.alloc, "Yanked '{s}'.", .{y.entry.name}); + app.notification.write(message.?, .info) catch {}; + } +} + +pub fn paste(app: *App) error{OutOfMemory}!void { + var message: ?[]const u8 = null; + defer if (message) |msg| app.alloc.free(msg); + + const yanked = if (app.yanked) |y| y else return; + + var new_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const new_path_res = environment.checkDuplicatePath(&new_path_buf, app.directories.dir, yanked.entry.name) catch { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - path too long.", .{yanked.entry.name}); + app.notification.write(message.?, .err) catch {}; + return; + }; + + switch (yanked.entry.kind) { + .file => { + var source_dir = std.fs.openDirAbsolute(yanked.dir, .{ .iterate = true }) catch { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.dir }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + defer source_dir.close(); + + std.fs.Dir.copyFile( + source_dir, + yanked.entry.name, + app.directories.dir, + new_path_res.path, + .{}, + ) catch |err| switch (err) { + error.FileNotFound => { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - the original file was deleted or moved.", .{yanked.entry.name}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }, + else => { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - {}.", .{ yanked.entry.name, err }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }, + }; + + // Append action to undo history. + var new_path_abs_buf: [std.fs.max_path_bytes]u8 = undefined; + const new_path_abs = app.directories.dir.realpath(new_path_res.path, &new_path_abs_buf) catch { + message = try std.fmt.allocPrint( + app.alloc, + "Failed to push copy action for '{s}' to undo history - unable to retrieve absolute directory path for '{s}'. This action will not be able to be undone via the `undo` keybind.", + .{ new_path_res.path, yanked.entry.name }, + ); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + if (app.actions.push(.{ + .paste = try app.alloc.dupe(u8, new_path_abs), + })) |prev_elem| { + app.alloc.free(prev_elem.delete.prev_path); + app.alloc.free(prev_elem.delete.new_path); + } + + message = try std.fmt.allocPrint(app.alloc, "Copied '{s}'.", .{yanked.entry.name}); + app.notification.write(message.?, .info) catch {}; + }, + else => { + message = try std.fmt.allocPrint(app.alloc, "Can not copy '{s}' - unsupported file type '{s}'.", .{ yanked.entry.name, @tagName(yanked.entry.kind) }); + app.notification.write(message.?, .warn) catch {}; + return; + }, + } + + try app.repopulateDirectory(""); + app.text_input.clearAndFree(); +} + +pub fn traverseLeft(app: *App) error{OutOfMemory}!void { + app.text_input.clearAndFree(); + + const dir = app.directories.dir.openDir("../", .{ .iterate = true }) catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + return; + }; + + app.directories.dir.close(); + app.directories.dir = dir; + + try app.repopulateDirectory(""); + app.text_input.clearAndFree(); + + if (app.directories.history.pop()) |history| { + if (history < app.directories.entries.len()) { + app.directories.entries.selected = history; + } + } +} + +pub fn traverseRight(app: *App, loop: *vaxis.Loop(App.Event)) !void { + var message: ?[]const u8 = null; + defer if (message) |msg| app.alloc.free(msg); + + const entry = (app.directories.getSelected() catch { + app.notification.write("Can not rename item - no item selected.", .warn) catch {}; + return; + }) orelse return; + + switch (entry.kind) { + .directory => { + app.text_input.clearAndFree(); + + const dir = app.directories.dir.openDir(entry.name, .{ .iterate = true }) catch |err| { + message = try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + app.directories.dir.close(); + app.directories.dir = dir; + _ = app.directories.history.push(app.directories.entries.selected); + try app.repopulateDirectory(""); + app.text_input.clearAndFree(); + }, + .file => { + if (environment.getEditor()) |editor| { + try app.vx.exitAltScreen(app.tty.anyWriter()); + try app.vx.resetState(app.tty.anyWriter()); + loop.stop(); + + environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch |err| { + message = try std.fmt.allocPrint(app.alloc, "Failed to open file '{s}' - {}.", .{ entry.name, err }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + }; + + try loop.start(); + try app.vx.enterAltScreen(app.tty.anyWriter()); + try app.vx.enableDetectedFeatures(app.tty.anyWriter()); + app.vx.queueRefresh(); + } else { + app.notification.write("Can not open file - $EDITOR not set.", .warn) catch {}; + } + }, + else => {}, + } +} + +pub fn createNewDir(app: *App) error{OutOfMemory}!void { + var message: ?[]const u8 = null; + defer if (message) |msg| app.alloc.free(msg); + + const dir = app.inputToSlice(); + + app.directories.dir.makeDir(dir) catch |err| { + message = try std.fmt.allocPrint(app.alloc, "Failed to create directory '{s}' - {}", .{ dir, err }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + app.text_input.clearAndFree(); + return; + }; + + try app.repopulateDirectory(""); + app.text_input.clearAndFree(); + + message = try std.fmt.allocPrint(app.alloc, "Created new directory '{s}'.", .{dir}); + app.notification.write(message.?, .info) catch {}; +} + +pub fn createNewFile(app: *App) error{OutOfMemory}!void { + var message: ?[]const u8 = null; + defer if (message) |msg| app.alloc.free(msg); + + const file = app.inputToSlice(); + + if (environment.fileExists(app.directories.dir, file)) { + message = try std.fmt.allocPrint(app.alloc, "Can not create file - '{s}' already exists.", .{file}); + app.notification.write(message.?, .warn) catch {}; + } else { + _ = app.directories.dir.createFile(file, .{}) catch |err| { + message = try std.fmt.allocPrint(app.alloc, "Failed to create file '{s}' - {}", .{ file, err }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + app.text_input.clearAndFree(); + return; + }; + + try app.repopulateDirectory(""); + app.text_input.clearAndFree(); + + message = try std.fmt.allocPrint(app.alloc, "Created new file '{s}'.", .{file}); + app.notification.write(message.?, .info) catch {}; + } + + app.text_input.clearAndFree(); +} + +pub fn undo(app: *App) error{OutOfMemory}!void { + var message: ?[]const u8 = null; + defer if (message) |msg| app.alloc.free(msg); + + const action = app.actions.pop() orelse { + app.notification.write("There is nothing to undo.", .info) catch {}; + return; + }; + + const selected = app.directories.entries.selected; + + switch (action) { + .delete => |a| { + defer app.alloc.free(a.new_path); + defer app.alloc.free(a.prev_path); + + var new_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const new_path_res = environment.checkDuplicatePath(&new_path_buf, app.directories.dir, a.prev_path) catch { + message = try std.fmt.allocPrint(app.alloc, "Failed to undo delete '{s}' - path too long.", .{a.prev_path}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + app.directories.dir.rename(a.new_path, new_path_res.path) catch |err| { + message = try std.fmt.allocPrint(app.alloc, "Failed to undo delete for '{s}' - {}.", .{ a.prev_path, err }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + try app.repopulateDirectory(""); + app.text_input.clearAndFree(); + + message = try std.fmt.allocPrint(app.alloc, "Restored '{s}' as '{s}'.", .{ a.prev_path, new_path_res.path }); + app.notification.write(message.?, .info) catch {}; + }, + .rename => |a| { + defer app.alloc.free(a.new_path); + defer app.alloc.free(a.prev_path); + + var new_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const new_path_res = environment.checkDuplicatePath(&new_path_buf, app.directories.dir, a.prev_path) catch { + message = try std.fmt.allocPrint(app.alloc, "Failed to undo rename '{s}' - path too long.", .{a.prev_path}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + app.directories.dir.rename(a.new_path, new_path_res.path) catch |err| { + message = try std.fmt.allocPrint(app.alloc, "Failed to undo rename for '{s}' - {}.", .{ a.new_path, err }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + try app.repopulateDirectory(""); + app.text_input.clearAndFree(); + + message = try std.fmt.allocPrint(app.alloc, "Reverted renaming of '{s}', now '{s}'.", .{ a.new_path, new_path_res.path }); + app.notification.write(message.?, .info) catch {}; + }, + .paste => |path| { + defer app.alloc.free(path); + + app.directories.dir.deleteTree(path) catch |err| { + message = try std.fmt.allocPrint(app.alloc, "Failed to delete '{s}' - {}.", .{ path, err }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + try app.repopulateDirectory(""); + app.text_input.clearAndFree(); + }, + } + + app.directories.entries.selected = selected; +} diff --git a/src/git.zig b/src/git.zig index 9efd8c8..d19ced8 100644 --- a/src/git.zig +++ b/src/git.zig @@ -2,15 +2,14 @@ const std = @import("std"); /// Callers owns memory returned. pub fn getGitBranch(alloc: std.mem.Allocator, dir: std.fs.Dir) !?[]const u8 { - var file = dir.openFile(".git/HEAD", .{}) catch return null; + var file = try dir.openFile(".git/HEAD", .{}); defer file.close(); var buf: [1024]u8 = undefined; - const bytes = file.readAll(&buf) catch return null; + const bytes = try file.readAll(&buf); + if (bytes == 0) return null; - var it = std.mem.splitBackwardsSequence(u8, buf[0..bytes], "/"); - const branch = it.next() orelse return null; - if (std.mem.eql(u8, branch, "")) return null; + const preamble = "ref: refs/heads/"; - return try alloc.dupe(u8, branch); + return try alloc.dupe(u8, buf[preamble.len..]); } diff --git a/src/main.zig b/src/main.zig index adef24e..f3cf470 100644 --- a/src/main.zig +++ b/src/main.zig @@ -53,16 +53,25 @@ pub fn main() !void { config.parse(alloc, &app) catch |err| switch (err) { error.SyntaxError => { - try app.notification.writeErr(.ConfigSyntaxError); + app.notification.write("Encountered a syntax error while parsing the config file.", .err) catch { + std.log.err("Encountered a syntax error while parsing the config file.", .{}); + }; }, error.InvalidCharacter => { - try app.notification.writeErr(.InvalidKeybind); + app.notification.write("One or more overriden keybinds are invalid.", .err) catch { + std.log.err("One or more overriden keybinds are invalid.", .{}); + }; }, error.DuplicateKeybind => { // Error logged in function }, else => { - try app.notification.writeErr(.ConfigUnknownError); + const message = try std.fmt.allocPrint(alloc, "Encountend an unknown error while parsing the config file - {}", .{err}); + defer alloc.free(message); + + app.notification.write(message, .err) catch { + std.log.err("Encountend an unknown error while parsing the config file - {}", .{err}); + }; }, }; diff --git a/src/notification.zig b/src/notification.zig index ed711c4..4099a54 100644 --- a/src/notification.zig +++ b/src/notification.zig @@ -12,41 +12,6 @@ const Style = enum { warn, }; -const Error = enum { - PermissionDenied, - UnknownError, - UnableToUndo, - UnableToOpenFile, - UnableToDelete, - FailedToDeleteSomeItems, - UnableToDeleteAcrossMountPoints, - UnsupportedImageFormat, - EditorNotSet, - ItemAlreadyExists, - UnableToRename, - IncorrectPath, - ConfigSyntaxError, - ConfigUnknownError, - ConfigPathNotFound, - CannotDeleteTrashDir, - InvalidKeybind, - NotADir, -}; - -const Info = enum { - CreatedFile, - CreatedFolder, - Deleted, - Renamed, - RestoredDelete, - RestoredRename, - EmptyUndo, - ChangedDir, - ConfigReloaded, -}; - -const Warn = enum { DeprecatedConfigPath, DuplicateFileOnUndo }; - var buf: [1024]u8 = undefined; style: Style = Style.info, @@ -61,50 +26,6 @@ pub fn write(self: *Self, text: []const u8, style: Style) !void { self.style = style; } -pub fn writeErr(self: *Self, err: Error) !void { - try switch (err) { - .PermissionDenied => self.write("Permission denied.", .err), - .UnknownError => self.write("An unknown error occurred.", .err), - .UnableToOpenFile => self.write("Unable to open file.", .err), - .UnableToDelete => self.write("Unable to delete item.", .err), - .FailedToDeleteSomeItems => self.write("Failed to delete some items..", .err), - .UnableToDeleteAcrossMountPoints => self.write("Unable to move item to /tmp. Failed to delete.", .err), - .UnableToUndo => self.write("Unable to undo previous action.", .err), - .ItemAlreadyExists => self.write("Item already exists.", .err), - .UnableToRename => self.write("Unable to rename item.", .err), - .IncorrectPath => self.write("Unable to find path.", .err), - .NotADir => self.write("Path is not a directory.", .err), - .EditorNotSet => self.write("$EDITOR is not set.", .err), - .UnsupportedImageFormat => self.write("Unsupported image format.", .err), - .ConfigSyntaxError => self.write("Could not read config due to a syntax error.", .err), - .ConfigUnknownError => self.write("Could not read config due to an unknown error.", .err), - .ConfigPathNotFound => self.write("Could not read config due to unset env variables. Please set either $HOME or $XDG_CONFIG_HOME.", .err), - .CannotDeleteTrashDir => self.write("Cannot delete trash directory.", .err), - .InvalidKeybind => self.write("Config has keybind(s) with invalid key(s).", .err), - }; -} - -pub fn writeInfo(self: *Self, info: Info) !void { - try switch (info) { - .CreatedFile => self.write("Successfully created file.", .info), - .CreatedFolder => self.write("Successfully created folder.", .info), - .Deleted => self.write("Successfully deleted item.", .info), - .Renamed => self.write("Successfully renamed item.", .info), - .RestoredDelete => self.write("Successfully restored deleted item.", .info), - .RestoredRename => self.write("Successfully restored renamed item.", .info), - .EmptyUndo => self.write("Nothing to undo.", .info), - .ChangedDir => self.write("Successfully changed directory.", .info), - .ConfigReloaded => self.write("Successfully reloaded config.", .info), - }; -} - -pub fn writeWarn(self: *Self, warning: Warn) !void { - try switch (warning) { - .DeprecatedConfigPath => self.write("You are using a deprecated config path. Please move your config to either `$XDG_CONFIG_HOME/jido` or `$HOME/.jido`", .warn), - .DuplicateFileOnUndo => self.write("A file with the same name already exists. A unique identifier has been appending to the duplicated item.", .warn), - }; -} - pub fn reset(self: *Self) void { self.fbs.reset(); self.style = Style.info;