mirror of
https://code.blicky.net/yorhel/ncdu.git
synced 2026-01-13 01:08:41 -09:00
Managed ArrayLists are deprecated in 0.15. "ArrayList" in 0.15 is the same as "ArrayListUnmanaged" in 0.14. The latter alias is still available in 0.15, so let's stick with that for now. When dropping support for 0.14, we can do s/ArrayListUnmanaged/ArrayList/.
301 lines
9.6 KiB
Zig
301 lines
9.6 KiB
Zig
// SPDX-FileCopyrightText: Yorhel <projects@yorhel.nl>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
const std = @import("std");
|
|
const main = @import("main.zig");
|
|
const model = @import("model.zig");
|
|
const ui = @import("ui.zig");
|
|
const browser = @import("browser.zig");
|
|
const scan = @import("scan.zig");
|
|
const sink = @import("sink.zig");
|
|
const mem_sink = @import("mem_sink.zig");
|
|
const util = @import("util.zig");
|
|
const c = @import("c.zig").c;
|
|
|
|
var parent: *model.Dir = undefined;
|
|
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;
|
|
|
|
pub fn setup(p: *model.Dir, e: *model.Entry, n: ?*model.Entry) void {
|
|
parent = 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, .{ .no_follow = true, .iterate = false }) catch |e| return err(e);
|
|
var it = &d.sub.ptr;
|
|
parent = d;
|
|
defer parent = parent.parent.?;
|
|
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.ptr;
|
|
}
|
|
fd.close();
|
|
dir.deleteDirZ(path) catch |e|
|
|
return if (e != error.DirNotEmpty or d.sub.ptr == null) err(e) else false;
|
|
} else
|
|
dir.deleteFileZ(path) catch |e| return err(e);
|
|
ptr.*.?.zeroStats(parent);
|
|
ptr.* = ptr.*.?.next.ptr;
|
|
return false;
|
|
}
|
|
|
|
// Returns true if the item has been deleted successfully.
|
|
fn deleteCmd(path: [:0]const u8, ptr: *align(1) ?*model.Entry) bool {
|
|
{
|
|
var env = std.process.getEnvMap(main.allocator) catch unreachable;
|
|
defer env.deinit();
|
|
env.put("NCDU_DELETE_PATH", path) catch unreachable;
|
|
|
|
// Since we're passing the path as an environment variable and go through
|
|
// the shell anyway, we can refer to the variable and avoid error-prone
|
|
// shell escaping.
|
|
const cmd = std.fmt.allocPrint(main.allocator, "{s} \"$NCDU_DELETE_PATH\"", .{main.config.delete_command}) catch unreachable;
|
|
defer main.allocator.free(cmd);
|
|
ui.runCmd(&.{"/bin/sh", "-c", cmd}, null, &env, true);
|
|
}
|
|
|
|
const stat = scan.statAt(std.fs.cwd(), path, false, null) catch {
|
|
// Stat failed. Would be nice to display an error if it's not
|
|
// 'FileNotFound', but w/e, let's just assume the item has been
|
|
// deleted as expected.
|
|
ptr.*.?.zeroStats(parent);
|
|
ptr.* = ptr.*.?.next.ptr;
|
|
return true;
|
|
};
|
|
|
|
// If either old or new entry is not a dir, remove & re-add entry in the in-memory tree.
|
|
if (ptr.*.?.pack.etype != .dir or stat.etype != .dir) {
|
|
ptr.*.?.zeroStats(parent);
|
|
const e = model.Entry.create(main.allocator, stat.etype, main.config.extended and !stat.ext.isEmpty(), ptr.*.?.name());
|
|
e.next.ptr = ptr.*.?.next.ptr;
|
|
mem_sink.statToEntry(&stat, e, parent);
|
|
ptr.* = e;
|
|
|
|
var it : ?*model.Dir = parent;
|
|
while (it) |p| : (it = p.parent) {
|
|
if (stat.etype != .link) {
|
|
p.entry.pack.blocks +|= e.pack.blocks;
|
|
p.entry.size +|= e.size;
|
|
}
|
|
p.items +|= 1;
|
|
}
|
|
}
|
|
|
|
// If new entry is a dir, recursively scan.
|
|
if (ptr.*.?.dir()) |d| {
|
|
main.state = .refresh;
|
|
sink.global.sink = .mem;
|
|
mem_sink.global.root = d;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Returns the item that should be selected in the browser.
|
|
pub fn delete() ?*model.Entry {
|
|
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 = &parent.sub.ptr;
|
|
while (it.*) |n| : (it = &n.next.ptr)
|
|
if (it.* == entry)
|
|
break;
|
|
|
|
var path: std.ArrayListUnmanaged(u8) = .empty;
|
|
defer path.deinit(main.allocator);
|
|
parent.fmtPath(main.allocator, true, &path);
|
|
if (path.items.len == 0 or path.items[path.items.len-1] != '/')
|
|
path.append(main.allocator, '/') catch unreachable;
|
|
path.appendSlice(main.allocator, entry.name()) catch unreachable;
|
|
|
|
if (main.config.delete_command.len == 0) {
|
|
_ = deleteItem(std.fs.cwd(), util.arrayListBufZ(&path, main.allocator), it);
|
|
model.inodes.addAllStats();
|
|
return if (it.* == e) e else next_sel;
|
|
} else {
|
|
const isdel = deleteCmd(util.arrayListBufZ(&path, main.allocator), it);
|
|
model.inodes.addAllStats();
|
|
return if (isdel) next_sel else it.*;
|
|
}
|
|
}
|
|
|
|
fn drawConfirm() void {
|
|
browser.draw();
|
|
const box = ui.Box.create(6, 60, "Confirm delete");
|
|
box.move(1, 2);
|
|
if (main.config.delete_command.len == 0) {
|
|
ui.addstr("Are you sure you want to delete \"");
|
|
ui.addstr(ui.shorten(ui.toUtf8(entry.name()), 21));
|
|
ui.addch('"');
|
|
if (entry.pack.etype != .dir)
|
|
ui.addch('?')
|
|
else {
|
|
box.move(2, 18);
|
|
ui.addstr("and all of its contents?");
|
|
}
|
|
} else {
|
|
ui.addstr("Are you sure you want to run \"");
|
|
ui.addstr(ui.shorten(ui.toUtf8(main.config.delete_command), 25));
|
|
ui.addch('"');
|
|
box.move(2, 4);
|
|
ui.addstr("on \"");
|
|
ui.addstr(ui.shorten(ui.toUtf8(entry.name()), 50));
|
|
ui.addch('"');
|
|
}
|
|
|
|
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");
|
|
box.move(4, switch (confirm) {
|
|
.yes => 15,
|
|
.no => 25,
|
|
.ignore => 31
|
|
});
|
|
}
|
|
|
|
fn drawProgress() void {
|
|
var path: std.ArrayListUnmanaged(u8) = .empty;
|
|
defer path.deinit(main.allocator);
|
|
parent.fmtPath(main.allocator, false, &path);
|
|
path.append(main.allocator, '/') catch unreachable;
|
|
path.appendSlice(main.allocator, 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(util.arrayListBufZ(&path, main.allocator)), 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.ArrayListUnmanaged(u8) = .empty;
|
|
defer path.deinit(main.allocator);
|
|
parent.fmtPath(main.allocator, false, &path);
|
|
path.append(main.allocator, '/') catch unreachable;
|
|
path.appendSlice(main.allocator, 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(util.arrayListBufZ(&path, main.allocator)), 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', c.KEY_LEFT => confirm = switch (confirm) {
|
|
.ignore => .no,
|
|
else => .yes,
|
|
},
|
|
'l', 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', c.KEY_LEFT => error_option = switch (error_option) {
|
|
.all => .ignore,
|
|
else => .abort,
|
|
},
|
|
'l', 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 => {}
|
|
},
|
|
}
|
|
}
|