summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2021-07-16 16:13:25 +0200
committerYorhel <git@yorhel.nl>2021-07-16 16:18:13 +0200
commit3a21dea2cd549a99cfeae50335b025f31c4066e9 (patch)
tree879758692071ec87a6d18eeafe0c94af94f5418e
parent448fa9e7a66a3270650ec61b48fd2683b63049fd (diff)
Implement file deletion + a bunch of bug fixes
-rw-r--r--README.md5
-rw-r--r--src/browser.zig59
-rw-r--r--src/delete.zig224
-rw-r--r--src/main.zig16
-rw-r--r--src/scan.zig1
-rw-r--r--src/ui.zig4
6 files changed, 276 insertions, 33 deletions
diff --git a/README.md b/README.md
index 5552cb1..ed6cd8d 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,6 @@ backported to the C version, depending on how viable a proper Zig release is.
Missing features:
- Help window
-- File deletion
### Improvements compared to the C version
@@ -76,11 +75,9 @@ Aside from this implementation being unfinished:
Not sure if these count as improvements or regressions, so I'll just list these
separately:
-- The browsing UI is not visible during refresh.
+- The browsing UI is not visible during refresh or file deletion.
- Some columns in the file browser are hidden automatically if the terminal is
not wide enough to display them.
-- Browsing keys other than changing the currently selected item don't work
- anymore while the info window is being displayed.
- The file's path is not displayed in the item window anymore (it's redundant).
- The item window's height is dynamic based on its contents.
diff --git a/src/browser.zig b/src/browser.zig
index 0c44bde..0e31586 100644
--- a/src/browser.zig
+++ b/src/browser.zig
@@ -2,6 +2,7 @@ const std = @import("std");
const main = @import("main.zig");
const model = @import("model.zig");
const scan = @import("scan.zig");
+const delete = @import("delete.zig");
const ui = @import("ui.zig");
const c = @cImport(@cInclude("time.h"));
usingnamespace @import("util.zig");
@@ -44,12 +45,12 @@ const View = struct {
}
// Should be called after dir_parents or dir_items has changed, will load the last saved view and find the proper cursor_idx.
- fn load(self: *@This()) void {
+ fn load(self: *@This(), sel: ?*const model.Entry) void {
if (opened_dir_views.get(@ptrToInt(dir_parents.top()))) |v| self.* = v
else self.* = @This(){};
cursor_idx = 0;
for (dir_items.items) |e, i| {
- if (self.cursor_hash == hashEntry(e)) {
+ if (if (sel != null) e == sel else self.cursor_hash == hashEntry(e)) {
cursor_idx = i;
break;
}
@@ -110,19 +111,19 @@ fn sortLt(_: void, ap: ?*model.Entry, bp: ?*model.Entry) bool {
// - config.sort_* changes
// - dir_items changes (i.e. from loadDir())
// - files in this dir have changed in a way that affects their ordering
-fn sortDir() void {
+fn sortDir(next_sel: ?*const model.Entry) void {
// No need to sort the first item if that's the parent dir reference,
// excluding that allows sortLt() to ignore null values.
const lst = dir_items.items[(if (dir_items.items.len > 0 and dir_items.items[0] == null) @as(usize, 1) else 0)..];
std.sort.sort(?*model.Entry, lst, @as(void, undefined), sortLt);
- current_view.load();
+ current_view.load(next_sel);
}
// Must be called when:
// - dir_parents changes (i.e. we change directory)
// - config.show_hidden changes
// - files in this dir have been added or removed
-pub fn loadDir() void {
+pub fn loadDir(next_sel: ?*const model.Entry) void {
dir_items.shrinkRetainingCapacity(0);
dir_max_size = 1;
dir_max_blocks = 1;
@@ -145,7 +146,7 @@ pub fn loadDir() void {
}
it = e.next;
}
- sortDir();
+ sortDir(next_sel);
}
const Row = struct {
@@ -531,18 +532,10 @@ const info = struct {
if (keyInputSelection(ch, &links_idx, links.?.paths.items.len, 5))
return true;
if (ch == 10) { // Enter - go to selected entry
- // XXX: This jump can be a little bit jarring as, usually,
- // browsing to parent directory will cause the previously
- // opened dir to be selected. This jump doesn't update the View
- // state of parent dirs, so that won't be the case anymore.
const p = links.?.paths.items[links_idx];
dir_parents.stack.shrinkRetainingCapacity(0);
dir_parents.stack.appendSlice(p.path.stack.items) catch unreachable;
- loadDir();
- for (dir_items.items) |e, i| {
- if (e == &p.node.entry)
- cursor_idx = i;
- }
+ loadDir(&p.node.entry);
set(null, .info);
}
}
@@ -630,7 +623,7 @@ pub fn draw() void {
const box = ui.Box.create(6, 60, "Message");
box.move(2, 2);
ui.addstr(m);
- box.move(4, 34);
+ box.move(4, 33);
ui.addstr("Press any key to continue");
}
if (sel_row > 0) ui.move(sel_row, 0);
@@ -641,7 +634,7 @@ fn sortToggle(col: main.config.SortCol, default_order: main.config.SortOrder) vo
else if (main.config.sort_order == .asc) main.config.sort_order = .desc
else main.config.sort_order = .asc;
main.config.sort_col = col;
- sortDir();
+ sortDir(null);
}
fn keyInputSelection(ch: i32, idx: *usize, len: usize, page: u32) bool {
@@ -677,7 +670,7 @@ pub fn keyInput(ch: i32) void {
switch (ch) {
'q' => if (main.config.confirm_quit) { state = .quit; } else ui.quit(),
- 'i' => info.set(dir_items.items[cursor_idx], .info),
+ 'i' => if (dir_items.items.len > 0) info.set(dir_items.items[cursor_idx], .info),
'r' => {
if (main.config.imported)
message = "Directory imported from file, refreshing is disabled."
@@ -694,6 +687,21 @@ pub fn keyInput(ch: i32) void {
else
main.state = .shell;
},
+ 'd' => {
+ if (dir_items.items.len == 0) {
+ } else if (main.config.imported)
+ message = "Deletion feature not available for imported directories."
+ else if (main.config.read_only)
+ message = "Deletion feature disabled in read-only mode."
+ else if (dir_items.items[cursor_idx]) |e| {
+ main.state = .delete;
+ const next =
+ if (cursor_idx+1 < dir_items.items.len) dir_items.items[cursor_idx+1]
+ else if (cursor_idx == 0) null
+ else dir_items.items[cursor_idx-1];
+ delete.setup(dir_parents.copy(), e, next);
+ }
+ },
// Sort & filter settings
'n' => sortToggle(.name, .asc),
@@ -702,22 +710,22 @@ pub fn keyInput(ch: i32) void {
'M' => if (main.config.extended) sortToggle(.mtime, .desc),
'e' => {
main.config.show_hidden = !main.config.show_hidden;
- loadDir();
+ loadDir(null);
state = .main;
},
't' => {
main.config.sort_dirsfirst = !main.config.sort_dirsfirst;
- sortDir();
+ sortDir(null);
},
'a' => {
main.config.show_blocks = !main.config.show_blocks;
if (main.config.show_blocks and main.config.sort_col == .size) {
main.config.sort_col = .blocks;
- sortDir();
+ sortDir(null);
}
if (!main.config.show_blocks and main.config.sort_col == .blocks) {
main.config.sort_col = .size;
- sortDir();
+ sortDir(null);
}
},
@@ -727,19 +735,20 @@ pub fn keyInput(ch: i32) void {
} else if (dir_items.items[cursor_idx]) |e| {
if (e.dir()) |d| {
dir_parents.push(d);
- loadDir();
+ loadDir(null);
state = .main;
}
} else if (!dir_parents.isRoot()) {
dir_parents.pop();
- loadDir();
+ loadDir(null);
state = .main;
}
},
'h', '<', ui.c.KEY_BACKSPACE, ui.c.KEY_LEFT => {
if (!dir_parents.isRoot()) {
+ const e = dir_parents.top();
dir_parents.pop();
- loadDir();
+ loadDir(&e.entry);
state = .main;
}
},
diff --git a/src/delete.zig b/src/delete.zig
new file mode 100644
index 0000000..36ebe4a
--- /dev/null
+++ b/src/delete.zig
@@ -0,0 +1,224 @@
+const std = @import("std");
+const main = @import("main.zig");
+const model = @import("model.zig");
+const ui = @import("ui.zig");
+const browser = @import("browser.zig");
+usingnamespace @import("util.zig");
+
+var parents: model.Parents = .{};
+var entry: *model.Entry = undefined;
+var next_sel: ?*model.Entry = undefined; // Which item to select if deletion succeeds
+var state: enum { confirm, busy, err } = .confirm;
+var confirm: enum { yes, no, ignore } = .no;
+var error_option: enum { abort, ignore, all } = .abort;
+var error_code: anyerror = undefined;
+
+// ownership of p is passed to this function
+pub fn setup(p: model.Parents, e: *model.Entry, n: ?*model.Entry) void {
+ parents = p;
+ entry = e;
+ next_sel = n;
+ state = if (main.config.confirm_delete) .confirm else .busy;
+ confirm = .no;
+}
+
+
+// Returns true to abort scanning.
+fn err(e: anyerror) bool {
+ if (main.config.ignore_delete_errors)
+ return false;
+ error_code = e;
+ state = .err;
+
+ while (main.state == .delete and state == .err)
+ main.handleEvent(true, false);
+
+ return main.state != .delete;
+}
+
+fn deleteItem(dir: std.fs.Dir, path: [:0]const u8, ptr: *align(1) ?*model.Entry) bool {
+ entry = ptr.*.?;
+ main.handleEvent(false, false);
+ if (main.state != .delete)
+ return true;
+
+ if (entry.dir()) |d| {
+ var fd = dir.openDirZ(path, .{ .access_sub_paths = true, .iterate = false })
+ catch |e| return err(e);
+ var it = &d.sub;
+ parents.push(d);
+ defer parents.pop();
+ while (it.*) |n| {
+ if (deleteItem(fd, n.name(), it)) {
+ fd.close();
+ return true;
+ }
+ if (it.* == n) // item deletion failed, make sure to still advance to next
+ it = &n.next;
+ }
+ fd.close();
+ dir.deleteDirZ(path) catch |e|
+ return if (e != error.DirNotEmpty or d.sub == null) err(e) else false;
+ } else
+ dir.deleteFileZ(path) catch |e| return err(e);
+ ptr.*.?.delStats(&parents);
+ ptr.* = ptr.*.?.next;
+ return false;
+}
+
+// Returns the item that should be selected in the browser.
+pub fn delete() ?*model.Entry {
+ defer parents.deinit();
+ while (main.state == .delete and state == .confirm)
+ main.handleEvent(true, false);
+ if (main.state != .delete)
+ return entry;
+
+ // Find the pointer to this entry
+ const e = entry;
+ var it = &parents.top().sub;
+ while (it.*) |n| : (it = &n.next)
+ if (it.* == entry)
+ break;
+
+ var path = std.ArrayList(u8).init(main.allocator);
+ defer path.deinit();
+ parents.fmtPath(true, &path);
+ if (path.items.len == 0 or path.items[path.items.len-1] != '/')
+ path.append('/') catch unreachable;
+ path.appendSlice(entry.name()) catch unreachable;
+
+ _ = deleteItem(std.fs.cwd(), arrayListBufZ(&path), it);
+ return if (it.* == e) e else next_sel;
+}
+
+fn drawConfirm() void {
+ browser.draw();
+ const box = ui.Box.create(6, 60, "Confirm delete");
+ box.move(1, 2);
+ ui.addstr("Are you sure you want to delete \"");
+ ui.addstr(ui.shorten(ui.toUtf8(entry.name()), 21));
+ ui.addch('"');
+ if (entry.etype != .dir)
+ ui.addch('?')
+ else {
+ box.move(2, 18);
+ ui.addstr("and all of its contents?");
+ }
+
+ box.move(4, 15);
+ ui.style(if (confirm == .yes) .sel else .default);
+ ui.addstr("yes");
+
+ box.move(4, 25);
+ ui.style(if (confirm == .no) .sel else .default);
+ ui.addstr("no");
+
+ box.move(4, 31);
+ ui.style(if (confirm == .ignore) .sel else .default);
+ ui.addstr("don't ask me again");
+}
+
+fn drawProgress() void {
+ var path = std.ArrayList(u8).init(main.allocator);
+ defer path.deinit();
+ parents.fmtPath(false, &path);
+ path.append('/') catch unreachable;
+ path.appendSlice(entry.name()) catch unreachable;
+
+ // TODO: Item counts and progress bar would be nice.
+
+ const box = ui.Box.create(6, 60, "Deleting...");
+ box.move(2, 2);
+ ui.addstr(ui.shorten(ui.toUtf8(arrayListBufZ(&path)), 56));
+ box.move(4, 41);
+ ui.addstr("Press ");
+ ui.style(.key);
+ ui.addch('q');
+ ui.style(.default);
+ ui.addstr(" to abort");
+}
+
+fn drawErr() void {
+ var path = std.ArrayList(u8).init(main.allocator);
+ defer path.deinit();
+ parents.fmtPath(false, &path);
+ path.append('/') catch unreachable;
+ path.appendSlice(entry.name()) catch unreachable;
+
+ const box = ui.Box.create(6, 60, "Error");
+ box.move(1, 2);
+ ui.addstr("Error deleting ");
+ ui.addstr(ui.shorten(ui.toUtf8(arrayListBufZ(&path)), 41));
+ box.move(2, 4);
+ ui.addstr(ui.errorString(error_code));
+
+ box.move(4, 14);
+ ui.style(if (error_option == .abort) .sel else .default);
+ ui.addstr("abort");
+
+ box.move(4, 23);
+ ui.style(if (error_option == .ignore) .sel else .default);
+ ui.addstr("ignore");
+
+ box.move(4, 33);
+ ui.style(if (error_option == .all) .sel else .default);
+ ui.addstr("ignore all");
+}
+
+pub fn draw() void {
+ switch (state) {
+ .confirm => drawConfirm(),
+ .busy => drawProgress(),
+ .err => drawErr(),
+ }
+}
+
+pub fn keyInput(ch: i32) void {
+ switch (state) {
+ .confirm => switch (ch) {
+ 'h', ui.c.KEY_LEFT => confirm = switch (confirm) {
+ .ignore => .no,
+ else => .yes,
+ },
+ 'l', ui.c.KEY_RIGHT => confirm = switch (confirm) {
+ .yes => .no,
+ else => .ignore,
+ },
+ 'q' => main.state = .browse,
+ '\n' => switch (confirm) {
+ .yes => state = .busy,
+ .no => main.state = .browse,
+ .ignore => {
+ main.config.confirm_delete = false;
+ state = .busy;
+ },
+ },
+ else => {}
+ },
+ .busy => {
+ if (ch == 'q')
+ main.state = .browse;
+ },
+ .err => switch (ch) {
+ 'h', ui.c.KEY_LEFT => error_option = switch (error_option) {
+ .all => .ignore,
+ else => .abort,
+ },
+ 'l', ui.c.KEY_RIGHT => error_option = switch (error_option) {
+ .abort => .ignore,
+ else => .all,
+ },
+ 'q' => main.state = .browse,
+ '\n' => switch (error_option) {
+ .abort => main.state = .browse,
+ .ignore => state = .busy,
+ .all => {
+ main.config.ignore_delete_errors = true;
+ state = .busy;
+ },
+ },
+ else => {}
+ },
+ }
+}
diff --git a/src/main.zig b/src/main.zig
index 08f9980..70c6466 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -5,6 +5,7 @@ const model = @import("model.zig");
const scan = @import("scan.zig");
const ui = @import("ui.zig");
const browser = @import("browser.zig");
+const delete = @import("delete.zig");
const c = @cImport(@cInclude("locale.h"));
// "Custom" allocator that wraps the libc allocator and calls ui.oom() on error.
@@ -65,9 +66,11 @@ pub const config = struct {
pub var imported: bool = false;
pub var can_shell: bool = true;
pub var confirm_quit: bool = false;
+ pub var confirm_delete: bool = true;
+ pub var ignore_delete_errors: bool = false;
};
-pub var state: enum { scan, browse, refresh, shell } = .scan;
+pub var state: enum { scan, browse, refresh, shell, delete } = .scan;
// Simple generic argument parser, supports getopt_long() style arguments.
// T can be any type that has a 'fn next(T) ?[:0]const u8' method, e.g.:
@@ -332,19 +335,24 @@ pub fn main() void {
config.scan_ui = .full; // in case we're refreshing from the UI, always in full mode.
ui.init();
state = .browse;
- browser.loadDir();
+ browser.loadDir(null);
while (true) {
switch (state) {
.refresh => {
scan.scan();
state = .browse;
- browser.loadDir();
+ browser.loadDir(null);
},
.shell => {
spawnShell();
state = .browse;
},
+ .delete => {
+ const next = delete.delete();
+ state = .browse;
+ browser.loadDir(next);
+ },
else => handleEvent(true, false)
}
}
@@ -360,6 +368,7 @@ pub fn handleEvent(block: bool, force_draw: bool) void {
switch (state) {
.scan, .refresh => scan.draw(),
.browse => browser.draw(),
+ .delete => delete.draw(),
.shell => unreachable,
}
if (ui.inited) _ = ui.c.refresh();
@@ -378,6 +387,7 @@ pub fn handleEvent(block: bool, force_draw: bool) void {
switch (state) {
.scan, .refresh => scan.keyInput(ch),
.browse => browser.keyInput(ch),
+ .delete => delete.keyInput(ch),
.shell => unreachable,
}
firstblock = false;
diff --git a/src/scan.zig b/src/scan.zig
index 4ac7c5f..60fcaca 100644
--- a/src/scan.zig
+++ b/src/scan.zig
@@ -384,6 +384,7 @@ const Context = struct {
else if (self.wr) |wr|
self.writeSpecial(wr.writer(), t) catch |e| writeErr(e);
+ self.stat.dir = false; // So that popPath() doesn't consider this as leaving a dir.
self.items_seen += 1;
}
diff --git a/src/ui.zig b/src/ui.zig
index 7910ee5..cc1784b 100644
--- a/src/ui.zig
+++ b/src/ui.zig
@@ -53,11 +53,12 @@ pub fn oom() void {
pub fn errorString(e: anyerror) [:0]const u8 {
return switch (e) {
error.AccessDenied => "Access denied",
+ error.DirNotEmpty => "Directory not empty",
error.DiskQuota => "Disk quota exceeded",
+ error.FileBusy => "File is busy",
error.FileNotFound => "No such file or directory",
error.FileSystem => "I/O error", // This one is shit, Zig uses this for both EIO and ELOOP in execve().
error.FileTooBig => "File too big",
- error.FileBusy => "File is busy",
error.InputOutput => "I/O error",
error.InvalidExe => "Invalid executable",
error.IsDir => "Is a directory",
@@ -66,6 +67,7 @@ pub fn errorString(e: anyerror) [:0]const u8 {
error.NotDir => "Not a directory",
error.OutOfMemory, error.SystemResources => "Out of memory",
error.ProcessFdQuotaExceeded => "Process file descriptor limit exceeded",
+ error.ReadOnlyFilesystem => "Read-only filesystem",
error.SymlinkLoop => "Symlink loop",
error.SystemFdQuotaExceeded => "System file descriptor limit exceeded",
else => "Unknown error", // rather useless :(