diff --git a/README.md b/README.md
index 18da30bea5da90cff4092cb244b350726d0c2725..a3a64431b747d0cb0c44921dc95eb581cdb85cf2 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:
 
 - File import
-- Most directory listing settings
 - Lots of informational UI windows
 - Directory refresh
 - File deletion
diff --git a/src/browser.zig b/src/browser.zig
index 25db5e81de7d8e8d7f9b811f13d254b28b647c0c..4f450f8cb4a25113920db6dc08613309e4b5bf27 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 ui = @import("ui.zig");
+const c = @cImport(@cInclude("time.h"));
 usingnamespace @import("util.zig");
 
 // Currently opened directory and its parents.
@@ -11,6 +12,9 @@ var dir_parents = model.Parents{};
 // (first item may be null to indicate the "parent directory" item)
 var dir_items = std.ArrayList(?*model.Entry).init(main.allocator);
 
+var dir_max_blocks: u64 = 0;
+var dir_max_size: u64 = 0;
+
 // Index into dir_items that is currently selected.
 var cursor_idx: usize = 0;
 
@@ -97,7 +101,7 @@ fn sortLt(_: void, ap: ?*model.Entry, bp: ?*model.Entry) bool {
     const an = a.name();
     const bn = b.name();
     return if (main.config.sort_order == .asc) std.mem.lessThan(u8, an, bn)
-           else std.mem.lessThan(u8, bn, an) or std.mem.eql(u8, an, bn);
+           else std.mem.lessThan(u8, bn, an);
 }
 
 // Should be called when:
@@ -118,10 +122,15 @@ fn sortDir() void {
 // - files in this dir have been added or removed
 pub fn loadDir() !void {
     dir_items.shrinkRetainingCapacity(0);
+    dir_max_size = 1;
+    dir_max_blocks = 1;
+
     if (dir_parents.top() != model.root)
         try dir_items.append(null);
     var it = dir_parents.top().sub;
     while (it) |e| {
+        if (e.blocks > dir_max_blocks) dir_max_blocks = e.blocks;
+        if (e.size > dir_max_size) dir_max_size = e.size;
         if (main.config.show_hidden) // fast path
             try dir_items.append(e)
         else {
@@ -143,7 +152,7 @@ const Row = struct {
 
     const Self = @This();
 
-    fn flag(self: *Self) !void {
+    fn flag(self: *Self) void {
         defer self.col += 2;
         const item = self.item orelse return;
         const ch: u7 = ch: {
@@ -165,7 +174,7 @@ const Row = struct {
         ui.addch(ch);
     }
 
-    fn size(self: *Self) !void {
+    fn size(self: *Self) void {
         defer self.col += if (main.config.si) @as(u32, 9) else 10;
         const item = self.item orelse return;
         ui.move(self.row, self.col);
@@ -173,14 +182,99 @@ const Row = struct {
         // TODO: shared sizes
     }
 
-    fn name(self: *Self) !void {
+    fn graph(self: *Self) void {
+        if (main.config.show_graph == .off) return;
+
+        const bar_size = std.math.max(ui.cols/7, 10);
+        defer self.col += switch (main.config.show_graph) {
+            .off => unreachable,
+            .graph => bar_size + 3,
+            .percent => 9,
+            .both => bar_size + 10,
+        };
+        const item = self.item orelse return;
+
         ui.move(self.row, self.col);
         self.bg.fg(.default);
+        ui.addch('[');
+        if (main.config.show_graph == .both or main.config.show_graph == .percent) {
+            self.bg.fg(.num);
+            ui.addprint("{d:>5.1}", .{ 100*
+                if (main.config.show_blocks) @intToFloat(f32, item.blocks) / @intToFloat(f32, std.math.max(1, dir_parents.top().entry.blocks))
+                else                         @intToFloat(f32, item.size)   / @intToFloat(f32, std.math.max(1, dir_parents.top().entry.size))
+            });
+            self.bg.fg(.default);
+            ui.addch('%');
+        }
+        if (main.config.show_graph == .both) ui.addch(' ');
+        if (main.config.show_graph == .both or main.config.show_graph == .graph) {
+            const perblock = std.math.divCeil(u64, if (main.config.show_blocks) dir_max_blocks else dir_max_size, bar_size) catch unreachable;
+            const num = if (main.config.show_blocks) item.blocks else item.size;
+            var i: u32 = 0;
+            self.bg.fg(.graph);
+            while (i < bar_size) : (i += 1) ui.addch(if (i*perblock <= num) '#' else ' ');
+        }
+        self.bg.fg(.default);
+        ui.addch(']');
+    }
+
+    fn items(self: *Self) void {
+        if (!main.config.show_items) return;
+        defer self.col += 7;
+        const d = if (self.item) |d| d.dir() orelse return else return;
+        const n = d.total_items;
+        ui.move(self.row, self.col);
+        self.bg.fg(.num);
+        if (n < 1000)
+            ui.addprint("  {d:>4}", .{n})
+        else if (n < 100_000)
+            ui.addprint("{d:>6.3}", .{ @intToFloat(f32, n) / 1000 })
+        else if (n < 1000_000) {
+            ui.addprint("{d:>5.1}", .{ @intToFloat(f32, n) / 1000 });
+            self.bg.fg(.default);
+            ui.addch('k');
+        } else if (n < 1000_000_000) {
+            ui.addprint("{d:>5.1}", .{ @intToFloat(f32, n) / 1000_000 });
+            self.bg.fg(.default);
+            ui.addch('M');
+        } else {
+            self.bg.fg(.default);
+            ui.addstr("  > ");
+            self.bg.fg(.num);
+            ui.addch('1');
+            self.bg.fg(.default);
+            ui.addch('G');
+        }
+    }
+
+    fn mtime(self: *Self) void {
+        if (!main.config.show_mtime) return;
+        defer self.col += 27;
+        ui.move(self.row, self.col+1);
+        const ext = (if (self.item) |e| e.ext() else @as(?*model.Ext, null)) orelse dir_parents.top().entry.ext();
+        if (ext) |e| {
+            const t = castClamp(c.time_t, e.mtime);
+            var buf: [32:0]u8 = undefined;
+            const len = c.strftime(&buf, buf.len, "%Y-%m-%d %H:%M:%S %z", c.localtime(&t));
+            if (len > 0) {
+                self.bg.fg(.num);
+                ui.addstr(buf[0..len:0]);
+            } else
+                ui.addstr("            invalid mtime");
+        } else
+            ui.addstr("                 no mtime");
+    }
+
+    fn name(self: *Self) !void {
+        ui.move(self.row, self.col);
         if (self.item) |i| {
+            self.bg.fg(if (i.etype == .dir) .dir else .default);
             ui.addch(if (i.etype == .dir) '/' else ' ');
             ui.addstr(try ui.shorten(try ui.toUtf8(i.name()), saturateSub(ui.cols, self.col + 1)));
-        } else
+        } else {
+            self.bg.fg(.dir);
             ui.addstr("/..");
+        }
     }
 
     fn draw(self: *Self) !void {
@@ -189,8 +283,11 @@ const Row = struct {
             ui.move(self.row, 0);
             ui.hline(' ', ui.cols);
         }
-        try self.flag();
-        try self.size();
+        self.flag();
+        self.size();
+        self.graph();
+        self.items();
+        self.mtime();
         try self.name();
     }
 };
@@ -232,7 +329,14 @@ pub fn draw() !void {
     ui.hline('-', ui.cols);
     ui.move(1,3);
     ui.addch(' ');
-    ui.addstr(try ui.shorten(try ui.toUtf8(model.root.entry.name()), saturateSub(ui.cols, 5)));
+    ui.style(.dir);
+
+    var pathbuf = std.ArrayList(u8).init(main.allocator);
+    try dir_parents.path(pathbuf.writer());
+    ui.addstr(try ui.shorten(try ui.toUtf8(try arrayListBufZ(&pathbuf)), saturateSub(ui.cols, 5)));
+    pathbuf.deinit();
+
+    ui.style(.default);
     ui.addch(' ');
 
     const numrows = saturateSub(ui.rows, 3);
@@ -240,6 +344,7 @@ pub fn draw() !void {
     if (cursor_idx >= current_view.top + numrows) current_view.top = cursor_idx - numrows + 1;
 
     var i: u32 = 0;
+    var sel_row: u32 = 0;
     while (i < numrows) : (i += 1) {
         if (i+current_view.top >= dir_items.items.len) break;
         var row = Row{
@@ -247,6 +352,7 @@ pub fn draw() !void {
             .item = dir_items.items[i+current_view.top],
             .bg = if (i+current_view.top == cursor_idx) .sel else .default,
         };
+        if (row.bg == .sel) sel_row = i+2;
         try row.draw();
     }
 
@@ -262,6 +368,7 @@ pub fn draw() !void {
     ui.addnum(.hd, dir_parents.top().total_items);
 
     if (need_confirm_quit) drawQuit();
+    if (sel_row > 0) ui.move(sel_row, 0);
 }
 
 fn sortToggle(col: main.SortCol, default_order: main.SortOrder) void {
@@ -343,6 +450,16 @@ pub fn key(ch: i32) !void {
             }
         },
 
+        // Display settings
+        'c' => main.config.show_items = !main.config.show_items,
+        'm' => if (main.config.extended) { main.config.show_mtime = !main.config.show_mtime; },
+        'g' => main.config.show_graph = switch (main.config.show_graph) {
+            .off => .graph,
+            .graph => .percent,
+            .percent => .both,
+            .both => .off,
+        },
+
         else => {}
     }
 }
diff --git a/src/main.zig b/src/main.zig
index b12d31e8589db6e39643c5e00f5336285a60b3fa..b34a59236c6e4c4f46884bb7145ddcde552f687f 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,4 +1,4 @@
-pub const program_version = "2.0";
+pub const program_version = "2.0-dev";
 
 const std = @import("std");
 const model = @import("model.zig");
@@ -29,6 +29,9 @@ pub const Config = struct {
 
     show_hidden: bool = true,
     show_blocks: bool = true,
+    show_items: bool = false,
+    show_mtime: bool = false,
+    show_graph: enum { off, graph, percent, both } = .graph,
     sort_col: SortCol = .blocks,
     sort_order: SortOrder = .desc,
     sort_dirsfirst: bool = false,
@@ -267,12 +270,16 @@ pub fn handleEvent(block: bool, force_draw: bool) !void {
         return;
     }
 
-    var ch = ui.getch(block);
-    if (ch == 0) return;
-    if (ch == -1) return handleEvent(block, true);
-    switch (state) {
-        .scan => try scan.key(ch),
-        .browse => try browser.key(ch),
+    var firstblock = block;
+    while (true) {
+        var ch = ui.getch(firstblock);
+        if (ch == 0) return;
+        if (ch == -1) return handleEvent(firstblock, true);
+        switch (state) {
+            .scan => try scan.key(ch),
+            .browse => try browser.key(ch),
+        }
+        firstblock = false;
     }
 }
 
diff --git a/src/model.zig b/src/model.zig
index 48340dd4b86c13083a39519da3ab93a6187c8833..c5ea1ceab0d4ad122ca48daa04cf3ce418295728 100644
--- a/src/model.zig
+++ b/src/model.zig
@@ -106,11 +106,14 @@ pub const Entry = packed struct {
         // Means we should count it for other-dev parent dirs, too.
         var new_hl = false;
 
-        // TODO: Saturating add/substract
         var it = parents.iter();
         while(it.next()) |p| {
             var add_total = false;
 
+            if (self.ext()) |e|
+                if (p.entry.ext()) |pe|
+                    if (e.mtime > pe.mtime) { pe.mtime = e.mtime; };
+
             // Hardlink in a subdirectory with a different device, only count it the first time.
             if (self.link() != null and dev != p.dev) {
                 add_total = new_hl;
@@ -204,6 +207,12 @@ pub const Ext = packed struct {
     mode: u16,
 };
 
+comptime {
+    std.debug.assert(@bitOffsetOf(Dir, "name") % 8 == 0);
+    std.debug.assert(@bitOffsetOf(Link, "name") % 8 == 0);
+    std.debug.assert(@bitOffsetOf(File, "name") % 8 == 0);
+}
+
 
 // Hardlink handling:
 //
@@ -350,11 +359,6 @@ pub const Parents = struct {
     }
 };
 
-test "name offsets" {
-    std.testing.expectEqual(@bitOffsetOf(Dir, "name") % 8, 0);
-    std.testing.expectEqual(@bitOffsetOf(Link, "name") % 8, 0);
-    std.testing.expectEqual(@bitOffsetOf(File, "name") % 8, 0);
-}
 
 test "entry" {
     var e = Entry.create(.file, false, "hello") catch unreachable;
diff --git a/src/scan.zig b/src/scan.zig
index 96978ed66f35c6b76089389a0b29ca07d5b39efa..0af71a78ef5d238d9071ae5563e08ceadcc47a29 100644
--- a/src/scan.zig
+++ b/src/scan.zig
@@ -19,26 +19,6 @@ const Stat = struct {
     symlink: bool,
     ext: model.Ext,
 
-    // Cast any integer type to the target type, clamping the value to the supported maximum if necessary.
-    fn castClamp(comptime T: type, x: anytype) T {
-        // (adapted from std.math.cast)
-        if (std.math.maxInt(@TypeOf(x)) > std.math.maxInt(T) and x > std.math.maxInt(T)) {
-            return std.math.maxInt(T);
-        } else if (std.math.minInt(@TypeOf(x)) < std.math.minInt(T) and x < std.math.minInt(T)) {
-            return std.math.minInt(T);
-        } else {
-            return @intCast(T, x);
-        }
-    }
-
-    // Cast any integer type to the target type, truncating if necessary.
-    fn castTruncate(comptime T: type, x: anytype) T {
-        const Ti = @typeInfo(T).Int;
-        const Xi = @typeInfo(@TypeOf(x)).Int;
-        const nx = if (Xi.signedness != Ti.signedness) @bitCast(std.meta.Int(Ti.signedness, Xi.bits), x) else x;
-        return if (Xi.bits > Ti.bits) @truncate(T, nx) else nx;
-    }
-
     fn clamp(comptime T: type, comptime field: anytype, x: anytype) std.meta.fieldInfo(T, field).field_type {
         return castClamp(std.meta.fieldInfo(T, field).field_type, x);
     }
@@ -176,9 +156,7 @@ const Context = struct {
     }
 
     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];
+        return arrayListBufZ(&self.path) catch unreachable;
     }
 
     // Set a flag to indicate that there was an error listing file entries in the current directory.
@@ -266,6 +244,13 @@ const Context = struct {
         if (self.parents) |p| p.pop();
         if (self.wr) |w| try w.writeByte(']');
     }
+
+    fn deinit(self: *Self) void {
+        if (self.last_error) |p| main.allocator.free(p);
+        if (self.parents) |p| p.deinit();
+        self.path.deinit();
+        self.path_indices.deinit();
+    }
 };
 
 // Context that is currently being used for scanning.
@@ -360,8 +345,10 @@ fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) (std.fs.File.Writer.Err
 
 pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void {
     const full_path = std.fs.realpathAlloc(main.allocator, path) catch path;
+    defer main.allocator.free(full_path);
 
     var ctx = Context{};
+    defer ctx.deinit();
     try ctx.pushPath(full_path);
     active_context = &ctx;
     defer active_context = null;
@@ -388,7 +375,8 @@ pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void {
         if (model.root.entry.ext()) |ext| ext.* = ctx.stat.ext;
     }
 
-    const dir = try std.fs.cwd().openDirZ(ctx.pathZ(), .{ .access_sub_paths = true, .iterate = true });
+    var dir = try std.fs.cwd().openDirZ(ctx.pathZ(), .{ .access_sub_paths = true, .iterate = true });
+    defer dir.close();
     try scanDir(&ctx, dir, ctx.stat.dev);
     if (out != null) {
         try ctx.leaveDir();
diff --git a/src/ui.zig b/src/ui.zig
index d8721883ea53a5850c0d46bab9d5a7153177f4c2..c56e7be5f38a83561e7c85c9c4303aacdc86ed1a 100644
--- a/src/ui.zig
+++ b/src/ui.zig
@@ -65,7 +65,7 @@ pub fn toUtf8(in: [:0]const u8) ![:0]const u8 {
         try to_utf8_buf.writer().print("\\x{X:0>2}", .{in[i]});
         i += 1;
     }
-    return try to_utf8_buf.toOwnedSliceSentinel(0);
+    return try arrayListBufZ(&to_utf8_buf);
 }
 
 var shorten_buf = std.ArrayList(u8).init(main.allocator);
@@ -115,7 +115,7 @@ pub fn shorten(in: [:0]const u8, max_width: u32) ![:0] const u8 {
             break;
         }
     }
-    return try shorten_buf.toOwnedSliceSentinel(0);
+    return try arrayListBufZ(&shorten_buf);
 }
 
 fn shortenTest(in: [:0]const u8, max_width: u32, out: [:0]const u8) void {
@@ -325,6 +325,13 @@ pub fn addstr(s: [:0]const u8) void {
     _ = c.addstr(s);
 }
 
+// Not to be used for strings that may end up >256 bytes.
+pub fn addprint(comptime fmt: []const u8, args: anytype) void {
+    var buf: [256:0]u8 = undefined;
+    const s = std.fmt.bufPrintZ(&buf, fmt, args) catch unreachable;
+    addstr(s);
+}
+
 pub fn addch(ch: c.chtype) void {
     _ = c.addch(ch);
 }
diff --git a/src/util.zig b/src/util.zig
index 3b6986ad8666b362fbe7525c84af0484c7a619d3..9b73de57351232738841fb262e43f41415548464 100644
--- a/src/util.zig
+++ b/src/util.zig
@@ -10,7 +10,36 @@ pub fn saturateSub(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
     return std.math.sub(@TypeOf(a), a, b) catch std.math.minInt(@TypeOf(a));
 }
 
+// Cast any integer type to the target type, clamping the value to the supported maximum if necessary.
+pub fn castClamp(comptime T: type, x: anytype) T {
+    // (adapted from std.math.cast)
+    if (std.math.maxInt(@TypeOf(x)) > std.math.maxInt(T) and x > std.math.maxInt(T)) {
+        return std.math.maxInt(T);
+    } else if (std.math.minInt(@TypeOf(x)) < std.math.minInt(T) and x < std.math.minInt(T)) {
+        return std.math.minInt(T);
+    } else {
+        return @intCast(T, x);
+    }
+}
+
+// Cast any integer type to the target type, truncating if necessary.
+pub fn castTruncate(comptime T: type, x: anytype) T {
+    const Ti = @typeInfo(T).Int;
+    const Xi = @typeInfo(@TypeOf(x)).Int;
+    const nx = if (Xi.signedness != Ti.signedness) @bitCast(std.meta.Int(Ti.signedness, Xi.bits), x) else x;
+    return if (Xi.bits > Ti.bits) @truncate(T, nx) else nx;
+}
+
 // Multiplies by 512, saturating.
 pub fn blocksToSize(b: u64) u64 {
     return if (b & 0xFF80000000000000 > 0) std.math.maxInt(u64) else b << 9;
 }
+
+// Ensure the given arraylist buffer gets zero-terminated and returns a slice
+// into the buffer. The returned buffer is invalidated whenever the arraylist
+// is freed or written to.
+pub fn arrayListBufZ(buf: *std.ArrayList(u8)) ![:0]const u8 {
+    try buf.append(0);
+    defer buf.items.len -= 1;
+    return buf.items[0..buf.items.len-1:0];
+}