diff --git a/README.md b/README.md
index 5552cb1366f4caf1b02378e2488c1a8e4900a4bb..ed6cd8d99cb51ba06b9568410b7ac10a287b24f3 100644
--- a/README.md
+++ b/README.md
@@ -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.
 
diff --git a/src/browser.zig b/src/browser.zig
index 0c44bdea6c9210804a38d708099874d0f6375602..0e315867b9d391ea25c1aec930cca72e9cf64a7d 100644
--- a/src/browser.zig
+++ b/src/browser.zig
@@ -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;
             }
         },
diff --git a/src/delete.zig b/src/delete.zig
new file mode 100644
index 0000000000000000000000000000000000000000..36ebe4ac9acfb5ac53ff54f43dc4f9343be4af3d
--- /dev/null
+++ b/src/delete.zig
@@ -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 => {}
+        },
+    }
+}
diff --git a/src/main.zig b/src/main.zig
index 08f9980dd9b3eb5bab6ac273a5819a6d8eb534d2..70c6466235b8cc44b0c165156cdb28b49c1f3c2c 100644
--- a/src/main.zig
+++ b/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;
diff --git a/src/scan.zig b/src/scan.zig
index 4ac7c5f2cdf90fe885c0964e618d577c36babc88..60fcacaf62b4fa1d90aee50e5b9528716de63f4f 100644
--- a/src/scan.zig
+++ b/src/scan.zig
@@ -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;
     }
 
diff --git a/src/ui.zig b/src/ui.zig
index 7910ee52076c31d4ca93dc6aa35e17439cb434ee..cc1784bb6492fce7207871797c418431f401fd7e 100644
--- a/src/ui.zig
+++ b/src/ui.zig
@@ -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 :(