diff --git a/README.md b/README.md
index 5d1989b49599058a29a7373d7d2746b377ca4509..1407b0c6854efb7dfbaf81d6b7ba49075c3cef13 100644
--- a/README.md
+++ b/README.md
@@ -30,11 +30,11 @@ Missing features:
 
 - Export/import
 - Most directory listing settings
-- Scaning UI
 - Lots of informational UI windows
 - Directory refresh
 - File deletion
 - Opening a shell
+- OOM handling
 
 ### Improvements compared to the C version
 
@@ -54,6 +54,7 @@ Already implemented:
   (Implemented in the data model, but not displayed in the UI yet)
 - Faster --exclude-kernfs thanks to `statfs()` caching.
 - Improved handling of Unicode and special characters.
+- Remembers item position when switching directories.
 
 Potentially to be implemented:
 
diff --git a/src/browser.zig b/src/browser.zig
index 571018db1ae89c2fad388709e48ab54752e5b80d..697187f245c152a5e84f68337d0d49a4fbd340d6 100644
--- a/src/browser.zig
+++ b/src/browser.zig
@@ -257,7 +257,7 @@ pub fn key(ch: i32) !void {
     defer current_view.save();
 
     switch (ch) {
-        'q' => main.state = .quit,
+        'q' => ui.quit(), // TODO: Confirm quit
 
         // Selection
         'j', ui.c.KEY_DOWN => {
diff --git a/src/main.zig b/src/main.zig
index f409fc5ed9f2babf8dc5b1ae81b58c72dead3062..44f06f40ce2a5507eef3e114a1144fa2293aec83 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -21,6 +21,7 @@ pub const Config = struct {
     exclude_patterns: std.ArrayList([:0]const u8) = std.ArrayList([:0]const u8).init(allocator),
 
     update_delay: u64 = 100*std.time.ns_per_ms,
+    scan_ui: enum { none, line, full } = .full,
     si: bool = false,
     nc_tty: bool = false,
     ui_color: enum { off, dark } = .off,
@@ -39,7 +40,7 @@ pub const Config = struct {
 
 pub var config = Config{};
 
-pub var state: enum { browse, quit } = .browse;
+pub var state: enum { scan, browse } = .browse;
 
 // 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.:
@@ -173,6 +174,9 @@ pub fn main() anyerror!void {
 
     var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init());
     var scan_dir: ?[]const u8 = null;
+    var import_file: ?[]const u8 = null;
+    var export_file: ?[]const u8 = null;
+    var has_scan_ui = false;
     _ = args.next(); // program name
     while (args.next()) |opt| {
         if (!opt.opt) {
@@ -188,6 +192,11 @@ pub fn main() anyerror!void {
         else if(opt.is("-e")) config.extended = true
         else if(opt.is("-r") and config.read_only) config.can_shell = false
         else if(opt.is("-r")) config.read_only = true
+        else if(opt.is("-0")) { has_scan_ui = true; config.scan_ui = .none; }
+        else if(opt.is("-1")) { has_scan_ui = true; config.scan_ui = .line; }
+        else if(opt.is("-2")) { has_scan_ui = true; config.scan_ui = .full; }
+        else if(opt.is("-o")) export_file = args.arg()
+        else if(opt.is("-f")) import_file = args.arg()
         else if(opt.is("--si")) config.si = true
         else if(opt.is("-L") or opt.is("--follow-symlinks")) config.follow_symlinks = true
         else if(opt.is("--exclude")) try config.exclude_patterns.append(args.arg())
@@ -203,23 +212,34 @@ pub fn main() anyerror!void {
             else if (std.mem.eql(u8, val, "dark")) config.ui_color = .dark
             else ui.die("Unknown --color option: {s}.\n", .{val});
         } else ui.die("Unrecognized option '{s}'.\n", .{opt.val});
-        // TODO: -o, -f, -0, -1, -2
     }
 
     if (std.builtin.os.tag != .linux and config.exclude_kernfs)
         ui.die("The --exclude-kernfs tag is currently only supported on Linux.\n", .{});
 
+    const is_out_tty = std.io.getStdOut().isTty();
+    if (!has_scan_ui) {
+        if (export_file) |f| {
+            if (!is_out_tty or std.mem.eql(u8, f, "-")) config.scan_ui = .none
+            else config.scan_ui = .line;
+        }
+    }
+    if (!is_out_tty and (export_file == null or config.scan_ui != .none))
+        ui.die("Standard output is not a TTY, can't initialize ncurses UI.\n", .{});
+
     event_delay_timer = try std.time.Timer.start();
+    defer ui.deinit();
 
+    state = .scan;
     try scan.scanRoot(scan_dir orelse ".");
-    try browser.loadDir();
 
+    config.scan_ui = .full; // in case we're refreshing from the UI, always in full mode.
     ui.init();
-    defer ui.deinit();
+    state = .browse;
+    try browser.loadDir();
 
     // TODO: Handle OOM errors
-    // TODO: Confirm quit
-    while (state != .quit) try handleEvent(true, false);
+    while (true) try handleEvent(true, false);
 }
 
 var event_delay_timer: std.time.Timer = undefined;
@@ -228,16 +248,26 @@ var event_delay_timer: std.time.Timer = undefined;
 // In non-blocking mode, screen drawing is rate-limited to keep this function fast.
 pub fn handleEvent(block: bool, force_draw: bool) !void {
     if (block or force_draw or event_delay_timer.read() > config.update_delay) {
-        _ = ui.c.erase();
-        try browser.draw();
-        _ = ui.c.refresh();
+        if (ui.inited) _ = ui.c.erase();
+        switch (state) {
+            .scan => try scan.draw(),
+            .browse => try browser.draw(),
+        }
+        if (ui.inited) _ = ui.c.refresh();
         event_delay_timer.reset();
     }
+    if (!ui.inited) {
+        std.debug.assert(!block);
+        return;
+    }
 
     var ch = ui.getch(block);
     if (ch == 0) return;
     if (ch == -1) return handleEvent(block, true);
-    try browser.key(ch);
+    switch (state) {
+        .scan => try scan.key(ch),
+        .browse => try browser.key(ch),
+    }
 }
 
 
diff --git a/src/scan.zig b/src/scan.zig
index d640150b6dd4f0b03357f434a43444de7ba565f3..04af0372b48d8a9d0ab12ab01a7064d882703b8c 100644
--- a/src/scan.zig
+++ b/src/scan.zig
@@ -1,6 +1,8 @@
 const std = @import("std");
 const main = @import("main.zig");
 const model = @import("model.zig");
+const ui = @import("ui.zig");
+usingnamespace @import("util.zig");
 const c_statfs = @cImport(@cInclude("sys/vfs.h"));
 const c_fnmatch = @cImport(@cInclude("fnmatch.h"));
 
@@ -98,11 +100,14 @@ const Context = struct {
     parents: model.Parents = .{},
     path: std.ArrayList(u8) = std.ArrayList(u8).init(main.allocator),
     path_indices: std.ArrayList(usize) = std.ArrayList(usize).init(main.allocator),
+    items_seen: u32 = 1,
 
     // 0-terminated name of the top entry, points into 'path', invalid after popPath().
     // This is a workaround to Zig's directory iterator not returning a [:0]const u8.
     name: [:0]const u8 = undefined,
 
+    last_error: ?[:0]u8 = null,
+
     const Self = @This();
 
     fn pushPath(self: *Self, name: []const u8) !void {
@@ -120,8 +125,27 @@ const Context = struct {
         self.path.items.len = self.path_indices.items[self.path_indices.items.len-1];
         self.path_indices.items.len -= 1;
     }
+
+    fn pathZ(self: *Self) [:0]const u8 {
+        self.path.append(0) catch unreachable;
+        defer self.path.items.len -= 1;
+        return self.path.items[0..self.path.items.len-1:0];
+    }
+
+    // Insert the current path as an error entry
+    fn setError(self: *Self) !void {
+        var e = try model.Entry.create(.file, false, self.name);
+        e.insert(&self.parents) catch unreachable;
+        e.set_err(&self.parents);
+
+        if (self.last_error) |p| main.allocator.free(p);
+        self.last_error = try main.allocator.dupeZ(u8, self.path.items);
+    }
 };
 
+// Context that is currently being used for scanning.
+var active_context: ?*Context = null;
+
 // Read and index entries of the given dir. The entry for the directory is already assumed to be in 'ctx.parents'.
 // (TODO: shouldn't error on OOM but instead call a function that waits or something)
 fn scanDir(ctx: *Context, dir: std.fs.Dir) std.mem.Allocator.Error!void {
@@ -131,16 +155,16 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir) std.mem.Allocator.Error!void {
             ctx.parents.top().entry.set_err(&ctx.parents);
             return;
         } orelse break;
+        ctx.items_seen += 1;
 
         try ctx.pushPath(entry.name);
+        try main.handleEvent(false, false);
         defer ctx.popPath();
 
         // XXX: This algorithm is extremely slow, can be optimized with some clever pattern parsing.
         const excluded = blk: {
             for (main.config.exclude_patterns.items) |pat| {
-                ctx.path.append(0) catch unreachable;
-                var path = ctx.path.items[0..ctx.path.items.len-1:0];
-                ctx.path.items.len -= 1;
+                var path = ctx.pathZ();
                 while (path.len > 0) {
                     if (c_fnmatch.fnmatch(pat, path, 0) == 0) break :blk true;
                     if (std.mem.indexOfScalar(u8, path, '/')) |idx| path = path[idx+1..:0]
@@ -157,16 +181,12 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir) std.mem.Allocator.Error!void {
         }
 
         var stat = Stat.read(dir, ctx.name, false) catch {
-            var e = try model.Entry.create(.file, false, entry.name);
-            e.insert(&ctx.parents) catch unreachable;
-            e.set_err(&ctx.parents);
+            try ctx.setError();
             continue;
         };
 
         if (main.config.same_fs and stat.dev != model.getDev(ctx.parents.top().dev)) {
-            var e = try model.Entry.create(.file, false, entry.name);
-            e.file().?.other_fs = true;
-            e.insert(&ctx.parents) catch unreachable;
+            try ctx.setError();
             continue;
         }
 
@@ -184,9 +204,7 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir) std.mem.Allocator.Error!void {
 
         var edir =
             if (stat.dir) dir.openDirZ(ctx.name, .{ .access_sub_paths = true, .iterate = true, .no_follow = true }) catch {
-                var e = try model.Entry.create(.file, false, entry.name);
-                e.insert(&ctx.parents) catch unreachable;
-                e.set_err(&ctx.parents);
+                try ctx.setError();
                 continue;
             } else null;
         defer if (edir != null) edir.?.close();
@@ -248,5 +266,94 @@ pub fn scanRoot(path: []const u8) !void {
     var ctx = Context{};
     try ctx.pushPath(full_path);
     const dir = try std.fs.cwd().openDirZ(model.root.entry.name(), .{ .access_sub_paths = true, .iterate = true });
+
+    active_context = &ctx;
+    defer active_context = null;
     try scanDir(&ctx, dir);
 }
+
+var animation_pos: u32 = 0;
+
+fn drawBox() !void {
+    ui.init();
+    const ctx = active_context.?;
+    const width = saturateSub(ui.cols, 5);
+    const box = ui.Box.create(10, width, "Scanning...");
+    box.move(2, 2);
+    ui.addstr("Total items: ");
+    ui.addnum(.default, ctx.items_seen);
+
+    if (width > 48 and true) { // TODO: When not exporting to file
+        box.move(2, 30);
+        ui.addstr("size: ");
+        ui.addsize(.default, blocksToSize(model.root.entry.blocks));
+    }
+
+    box.move(3, 2);
+    ui.addstr("Current item: ");
+    ui.addstr(try ui.shorten(try ui.toUtf8(ctx.pathZ()), saturateSub(width, 18)));
+
+    if (ctx.last_error) |path| {
+        box.move(5, 2);
+        ui.style(.bold);
+        ui.addstr("Warning: ");
+        ui.style(.default);
+        ui.addstr("error scanning ");
+        ui.addstr(try ui.shorten(try ui.toUtf8(path), saturateSub(width, 28)));
+        box.move(6, 3);
+        ui.addstr("some directory sizes may not be correct.");
+    }
+
+    box.move(8, saturateSub(width, 18));
+    ui.addstr("Press ");
+    ui.style(.key);
+    ui.addch('q');
+    ui.style(.default);
+    ui.addstr(" to abort");
+
+    if (main.config.update_delay < std.time.ns_per_s and width > 40) {
+        const txt = "Scanning...";
+        animation_pos += 1;
+        if (animation_pos >= txt.len*2) animation_pos = 0;
+        if (animation_pos < txt.len) {
+            var i: u32 = 0;
+            box.move(8, 2);
+            while (i <= animation_pos) : (i += 1) ui.addch(txt[i]);
+        } else {
+            var i: u32 = txt.len-1;
+            while (i > animation_pos-txt.len) : (i -= 1) {
+                box.move(8, 2+i);
+                ui.addch(txt[i]);
+            }
+        }
+    }
+}
+
+pub fn draw() !void {
+    switch (main.config.scan_ui) {
+        .none => {},
+        .line => {
+            var buf: [256]u8 = undefined;
+            var line: []const u8 = undefined;
+            if (false) { // TODO: When exporting to file; no total size known
+                line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <63} {d:>9} files\x1b8",
+                    .{ ui.shorten(active_context.?.pathZ(), 63), active_context.?.items_seen }
+                ) catch return;
+            } else {
+                const r = ui.FmtSize.fmt(blocksToSize(model.root.entry.blocks));
+                line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <51} {d:>9} files / {s}{s}\x1b8",
+                    .{ ui.shorten(active_context.?.pathZ(), 51), active_context.?.items_seen, r.num(), r.unit }
+                ) catch return;
+            }
+            _ = std.io.getStdErr().write(line) catch {};
+        },
+        .full => try drawBox(),
+    }
+}
+
+pub fn key(ch: i32) !void {
+    switch (ch) {
+        'q' => ui.quit(), // TODO: Confirm quit
+        else => {},
+    }
+}
diff --git a/src/ui.zig b/src/ui.zig
index 02f7e6a0c5c68caf13e6cc97b371836e975ecd4f..30b4657ed1687464b37811ee124f8b2517713153 100644
--- a/src/ui.zig
+++ b/src/ui.zig
@@ -2,6 +2,7 @@
 
 const std = @import("std");
 const main = @import("main.zig");
+usingnamespace @import("util.zig");
 
 pub const c = @cImport({
     @cInclude("stdio.h");
@@ -12,7 +13,7 @@ pub const c = @cImport({
     @cInclude("locale.h");
 });
 
-var inited: bool = false;
+pub var inited: bool = false;
 
 pub var rows: u32 = undefined;
 pub var cols: u32 = undefined;
@@ -23,6 +24,11 @@ pub fn die(comptime fmt: []const u8, args: anytype) noreturn {
     std.process.exit(1);
 }
 
+pub fn quit() noreturn {
+    deinit();
+    std.process.exit(0);
+}
+
 var to_utf8_buf = std.ArrayList(u8).init(main.allocator);
 
 fn toUtf8BadChar(ch: u8) bool {
@@ -141,13 +147,6 @@ extern fn ncdu_acs_lrcorner() c.chtype;
 extern fn ncdu_acs_hline()    c.chtype;
 extern fn ncdu_acs_vline()    c.chtype;
 
-pub fn acs_ulcorner() c.chtype { return ncdu_acs_ulcorner(); }
-pub fn acs_llcorner() c.chtype { return ncdu_acs_llcorner(); }
-pub fn acs_urcorner() c.chtype { return ncdu_acs_urcorner(); }
-pub fn acs_lrcorner() c.chtype { return ncdu_acs_lrcorner(); }
-pub fn acs_hline()    c.chtype { return ncdu_acs_hline()   ; }
-pub fn acs_vline()    c.chtype { return ncdu_acs_vline()   ; }
-
 const StyleAttr = struct { fg: i16, bg: i16, attr: u32 };
 const StyleDef = struct {
     name: []const u8,
@@ -165,6 +164,9 @@ const styles = [_]StyleDef{
     .{  .name = "default",
         .off  = .{ .fg = -1,              .bg = -1,             .attr = 0 },
         .dark = .{ .fg = -1,              .bg = -1,             .attr = 0 } },
+    .{  .name = "bold",
+        .off  = .{ .fg = -1,              .bg = -1,             .attr = c.A_BOLD },
+        .dark = .{ .fg = -1,              .bg = -1,             .attr = c.A_BOLD } },
     .{  .name = "box_title",
         .off  = .{ .fg = -1,              .bg = -1,             .attr = c.A_BOLD },
         .dark = .{ .fg = c.COLOR_BLUE,    .bg = -1,             .attr = c.A_BOLD } },
@@ -266,6 +268,9 @@ fn updateSize() void {
 
 pub fn init() void {
     if (inited) return;
+    // Send a "clear from cursor to end of screen" instruction, to clear a
+    // potential line left behind from scanning in -1 mode.
+    _ = std.io.getStdErr().write("\x1b[J") catch {};
     if (main.config.nc_tty) {
         var tty = c.fopen("/dev/tty", "r+");
         if (tty == null) die("Error opening /dev/tty: {s}.\n", .{ c.strerror(std.c.getErrno(-1)) });
@@ -317,37 +322,51 @@ pub fn addch(ch: c.chtype) void {
     _ = c.addch(ch);
 }
 
-// Print a human-readable size string, formatted into the given bavkground.
-// Takes 8 columns in SI mode, 9 otherwise.
-//   "###.# XB"
-//   "###.# XiB"
-pub fn addsize(bg: Bg, v: u64) void {
-    var f = @intToFloat(f32, v);
-    var unit: [:0]const u8 = undefined;
-    if (main.config.si) {
-        if(f < 1000.0)    { unit = "  B"; }
-        else if(f < 1e6)  { unit = " KB"; f /= 1e3;  }
-        else if(f < 1e9)  { unit = " MB"; f /= 1e6;  }
-        else if(f < 1e12) { unit = " GB"; f /= 1e9;  }
-        else if(f < 1e15) { unit = " TB"; f /= 1e12; }
-        else if(f < 1e18) { unit = " PB"; f /= 1e15; }
-        else              { unit = " EB"; f /= 1e18; }
+// Format an integer to a human-readable size string.
+//   num() = "###.#"
+//   unit = " XB" or " XiB"
+// Concatenated, these take 8 columns in SI mode or 9 otherwise.
+pub const FmtSize = struct {
+    buf: [8:0]u8,
+    unit: [:0]const u8,
+
+    pub fn fmt(v: u64) @This() {
+        var r: @This() = undefined;
+        var f = @intToFloat(f32, v);
+        if (main.config.si) {
+            if(f < 1000.0)    { r.unit = "  B"; }
+            else if(f < 1e6)  { r.unit = " KB"; f /= 1e3;  }
+            else if(f < 1e9)  { r.unit = " MB"; f /= 1e6;  }
+            else if(f < 1e12) { r.unit = " GB"; f /= 1e9;  }
+            else if(f < 1e15) { r.unit = " TB"; f /= 1e12; }
+            else if(f < 1e18) { r.unit = " PB"; f /= 1e15; }
+            else              { r.unit = " EB"; f /= 1e18; }
+        }
+        else {
+            if(f < 1000.0)       { r.unit = "   B"; }
+            else if(f < 1023e3)  { r.unit = " KiB"; f /= 1024.0; }
+            else if(f < 1023e6)  { r.unit = " MiB"; f /= 1048576.0; }
+            else if(f < 1023e9)  { r.unit = " GiB"; f /= 1073741824.0; }
+            else if(f < 1023e12) { r.unit = " TiB"; f /= 1099511627776.0; }
+            else if(f < 1023e15) { r.unit = " PiB"; f /= 1125899906842624.0; }
+            else                 { r.unit = " EiB"; f /= 1152921504606846976.0; }
+        }
+        _ = std.fmt.bufPrintZ(&r.buf, "{d:>5.1}", .{f}) catch unreachable;
+        return r;
     }
-    else {
-        if(f < 1000.0)       { unit = "   B"; }
-        else if(f < 1023e3)  { unit = " KiB"; f /= 1024.0; }
-        else if(f < 1023e6)  { unit = " MiB"; f /= 1048576.0; }
-        else if(f < 1023e9)  { unit = " GiB"; f /= 1073741824.0; }
-        else if(f < 1023e12) { unit = " TiB"; f /= 1099511627776.0; }
-        else if(f < 1023e15) { unit = " PiB"; f /= 1125899906842624.0; }
-        else                 { unit = " EiB"; f /= 1152921504606846976.0; }
+
+    pub fn num(self: *const @This()) [:0]const u8 {
+        return std.mem.spanZ(&self.buf);
     }
-    var buf: [8:0]u8 = undefined;
-    _ = std.fmt.bufPrintZ(&buf, "{d:>5.1}", .{f}) catch unreachable;
+};
+
+// Print a formatted human-readable size string onto the given background.
+pub fn addsize(bg: Bg, v: u64) void {
+    const r = FmtSize.fmt(v);
     bg.fg(.num);
-    addstr(&buf);
+    addstr(r.num());
     bg.fg(.default);
-    addstr(unit);
+    addstr(r.unit);
 }
 
 // Print a full decimal number with thousand separators.
@@ -378,6 +397,52 @@ pub fn hline(ch: c.chtype, len: u32) void {
     _ = c.hline(ch, @intCast(i32, len));
 }
 
+// Draws a bordered box in the center of the screen.
+pub const Box = struct {
+    start_row: u32,
+    start_col: u32,
+
+    const Self = @This();
+
+    pub fn create(height: u32, width: u32, title: [:0]const u8) Self {
+        const s = Self{
+            .start_row = saturateSub(rows>>1, height>>1),
+            .start_col = saturateSub(cols>>1, width>>1),
+        };
+        style(.default);
+        if (width < 6 or height < 3) return s;
+
+        const ulcorner = ncdu_acs_ulcorner();
+        const llcorner = ncdu_acs_llcorner();
+        const urcorner = ncdu_acs_urcorner();
+        const lrcorner = ncdu_acs_lrcorner();
+        const acs_hline = ncdu_acs_hline();
+        const acs_vline = ncdu_acs_vline();
+
+        var i: u32 = 0;
+        while (i < height) : (i += 1) {
+            s.move(i, 0);
+            addch(if (i == 0) ulcorner else if (i == height-1) llcorner else acs_hline);
+            hline(if (i == 0 or i == height-1) acs_vline else ' ', width-2);
+            s.move(i, width-1);
+            addch(if (i == 0) urcorner else if (i == height-1) lrcorner else acs_hline);
+        }
+
+        s.move(0, 3);
+        style(.box_title);
+        addch(' ');
+        addstr(title);
+        addch(' ');
+        style(.default);
+        return s;
+    }
+
+    // Move the global cursor to the given coordinates inside the box.
+    pub fn move(s: Self, row: u32, col: u32) void {
+        ui.move(s.start_row + row, s.start_col + col);
+    }
+};
+
 // Returns 0 if no key was pressed in non-blocking mode.
 // Returns -1 if it was KEY_RESIZE, requiring a redraw of the screen.
 pub fn getch(block: bool) i32 {