ncdu-zig/src/delete.zig

302 lines
9.6 KiB
Zig
Raw Normal View History

// SPDX-FileCopyrightText: Yorhel <projects@yorhel.nl>
2021-07-18 01:36:05 -08:00
// 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");
2021-07-19 05:28:11 -08:00
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 => {}
},
}
}