mirror of
https://code.blicky.net/yorhel/ncdu.git
synced 2026-01-13 01:08:41 -09:00
Implement file deletion + a bunch of bug fixes
This commit is contained in:
parent
448fa9e7a6
commit
3a21dea2cd
6 changed files with 276 additions and 33 deletions
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
|
|
|||
224
src/delete.zig
Normal file
224
src/delete.zig
Normal file
|
|
@ -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 => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
16
src/main.zig
16
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;
|
||||
|
|
|
|||
|
|
@ -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 :(
|
||||
|
|
|
|||
Loading…
Reference in a new issue