Skip to content
Snippets Groups Projects
Commit 3a21dea2 authored by Yorhel's avatar Yorhel
Browse files

Implement file deletion + a bunch of bug fixes

parent 448fa9e7
No related branches found
No related tags found
No related merge requests found
......@@ -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.
......
......@@ -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;
}
},
......
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 => {}
},
}
}
......@@ -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;
......
......@@ -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;
}
......
......@@ -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 :(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment