// SPDX-FileCopyrightText: Yorhel // 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 => {} }, } }