diff --git a/ncdu.pod b/ncdu.pod
index 3903c99b7af4edaaca1706fc0dad67ab917ee58c..290b5c225c8ff2f6d15832fe77ac30300324bd5f 100644
--- a/ncdu.pod
+++ b/ncdu.pod
@@ -266,6 +266,27 @@ The default is I<dark-bg> unless the C<NO_COLOR> environment variable is set.
 =back
 
 
+=head1 CONFIGURATION
+
+Ncdu can be configured by placing command-line options in C</etc/ncdu.conf> or
+C<$HOME/.config/ncdu/config>. If both files exist, the system configuration
+will be loaded before the user configuration, allowing users to override
+options set in the system configuration. Options given on the command line will
+override options set in the configuration files.
+
+The configuration file format is simply one command line option per line. Lines
+starting with C<#> are ignored. Example configuration file:
+
+  # Always enable extended mode
+  -e
+
+  # Disable file deletion
+  --disable-delete
+
+  # Sort by apparent size by default
+  --sort apparent-size
+
+
 =head1 KEYS
 
 =over
diff --git a/src/browser.zig b/src/browser.zig
index 830fc5ed452d86c1f877b3327bebe1dc215c4d8e..de1ffa79d6710fe4fb4a70e50fe362c44da8e8db 100644
--- a/src/browser.zig
+++ b/src/browser.zig
@@ -727,7 +727,7 @@ pub fn draw() void {
     if (main.config.imported) {
         ui.move(0, saturateSub(ui.cols, 10));
         ui.addstr("[imported]");
-    } else if (!main.config.can_delete) {
+    } else if (!main.config.can_delete.?) {
         ui.move(0, saturateSub(ui.cols, 10));
         ui.addstr("[readonly]");
     }
@@ -838,7 +838,7 @@ pub fn keyInput(ch: i32) void {
         '?' => state = .help,
         'i' => if (dir_items.items.len > 0) info.set(dir_items.items[cursor_idx], .info),
         'r' => {
-            if (!main.config.can_refresh)
+            if (!main.config.can_refresh.?)
                 message = "Directory refresh feature disabled."
             else {
                 main.state = .refresh;
@@ -846,14 +846,14 @@ pub fn keyInput(ch: i32) void {
             }
         },
         'b' => {
-            if (!main.config.can_shell)
+            if (!main.config.can_shell.?)
                 message = "Shell feature disabled."
             else
                 main.state = .shell;
         },
         'd' => {
             if (dir_items.items.len == 0) {
-            } else if (!main.config.can_delete)
+            } else if (!main.config.can_delete.?)
                 message = "Deletion feature disabled."
             else if (dir_items.items[cursor_idx]) |e| {
                 main.state = .delete;
diff --git a/src/main.zig b/src/main.zig
index 86237c4f05673966e775fa5e378fabb6f6000b13..27fd29245d63bc9a7aeaeb06020b264abc81624e 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -9,6 +9,7 @@ const scan = @import("scan.zig");
 const ui = @import("ui.zig");
 const browser = @import("browser.zig");
 const delete = @import("delete.zig");
+const util = @import("util.zig");
 const c = @cImport(@cInclude("locale.h"));
 
 // "Custom" allocator that wraps the libc allocator and calls ui.oom() on error.
@@ -49,7 +50,7 @@ pub const config = struct {
     pub var exclude_patterns: std.ArrayList([:0]const u8) = std.ArrayList([:0]const u8).init(allocator);
 
     pub var update_delay: u64 = 100*std.time.ns_per_ms;
-    pub var scan_ui: enum { none, line, full } = .full;
+    pub var scan_ui: ?enum { none, line, full } = .full;
     pub var si: bool = false;
     pub var nc_tty: bool = false;
     pub var ui_color: enum { off, dark, darkbg } = .off;
@@ -67,9 +68,9 @@ pub const config = struct {
     pub var sort_dirsfirst: bool = false;
 
     pub var imported: bool = false;
-    pub var can_delete: bool = true;
-    pub var can_shell: bool = true;
-    pub var can_refresh: bool = true;
+    pub var can_delete: ?bool = true;
+    pub var can_shell: ?bool = true;
+    pub var can_refresh: ?bool = true;
     pub var confirm_quit: bool = false;
     pub var confirm_delete: bool = true;
     pub var ignore_delete_errors: bool = false;
@@ -153,6 +154,176 @@ fn Args(T: anytype) type {
     };
 }
 
+const ArgsFile = struct {
+    f: std.fs.File,
+    path: []const u8,
+    buf: std.io.BufferedReader(4096, std.fs.File.Reader),
+    hasarg: bool = false,
+    ch: u8 = 0,
+
+    fn open(path: [:0]const u8) ?ArgsFile {
+        const f = std.fs.cwd().openFileZ(path, .{}) catch |e| switch (e) {
+            error.FileNotFound => return null,
+            else => ui.die("Error opening {s}: {s}\n", .{ path, ui.errorString(e) }),
+        };
+        var self = ArgsFile{
+            .f = f,
+            .path = path,
+            .buf = std.io.bufferedReader(f.reader()),
+        };
+        self.con();
+        return self;
+    }
+
+    fn con(self: *ArgsFile) void {
+        self.ch = self.buf.reader().readByte() catch |e| switch (e) {
+            error.EndOfStream => 0,
+            else => ui.die("Error reading from {s}: {s}\n", .{ self.path, ui.errorString(e) }),
+        };
+    }
+
+    // Return value /should/ be freed, but the rest of the argument parsing
+    // code won't bother with that. Leaking arguments isn't a big deal.
+    fn next(self: *ArgsFile) ?[:0]const u8 {
+        while (true) {
+            while (true) {
+                switch (self.ch) {
+                    0 => return null,
+                    '\n', ' ', '\t', '\r' => {},
+                    else => break,
+                }
+                self.con();
+            }
+            if (self.ch == '#') {
+                while (true) {
+                    self.con();
+                    if (self.ch == 0) return null;
+                    if (self.ch == '\n') break;
+                }
+            } else break;
+        }
+        var val = std.ArrayList(u8).init(allocator);
+        if (self.hasarg) {
+            while (self.ch != '\n' and self.ch != 0) {
+                val.append(self.ch) catch unreachable;
+                self.con();
+            }
+            self.hasarg = false;
+        } else {
+            while (true) {
+                switch (self.ch) {
+                    '=', ' ', '\t', '\r' => { self.hasarg = true; break; },
+                    '\n' => break,
+                    0 => return null,
+                    else => val.append(self.ch) catch unreachable,
+                }
+                self.con();
+            }
+        }
+        return util.arrayListBufZ(&val);
+    }
+};
+
+// TODO: Rewrite this (and Args(), I guess) to be non-generic, it rather bloats
+// the binary this way.
+fn argConfig(args: anytype, opt: anytype) bool {
+    if (opt.is("-q") or opt.is("--slow-ui-updates")) config.update_delay = 2*std.time.ns_per_s
+    else if (opt.is("--fast-ui-updates")) config.update_delay = 100*std.time.ns_per_ms
+    else if (opt.is("-x") or opt.is("--one-file-system")) config.same_fs = true
+    else if (opt.is("--cross-file-system")) config.same_fs = false
+    else if (opt.is("-e") or opt.is("--extended")) config.extended = true
+    else if (opt.is("--no-extended")) config.extended = false
+    else if (opt.is("-r") and !(config.can_delete orelse true)) config.can_shell = false
+    else if (opt.is("-r")) config.can_delete = false
+    else if (opt.is("--enable-shell")) config.can_shell = true
+    else if (opt.is("--disable-shell")) config.can_shell = false
+    else if (opt.is("--enable-delete")) config.can_delete = true
+    else if (opt.is("--disable-delete")) config.can_delete = false
+    else if (opt.is("--enable-refresh")) config.can_refresh = true
+    else if (opt.is("--disable-refresh")) config.can_refresh = false
+    else if (opt.is("--show-hidden")) config.show_hidden = true
+    else if (opt.is("--hide-hidden")) config.show_hidden = false
+    else if (opt.is("--show-itemcount")) config.show_items = true
+    else if (opt.is("--hide-itemcount")) config.show_items = false
+    else if (opt.is("--show-mtime")) config.show_mtime = true
+    else if (opt.is("--hide-mtime")) config.show_mtime = false
+    else if (opt.is("--show-graph")) config.show_graph = true
+    else if (opt.is("--hide-graph")) config.show_graph = false
+    else if (opt.is("--show-percent")) config.show_percent = true
+    else if (opt.is("--hide-percent")) config.show_percent = false
+    else if (opt.is("--group-directories-first")) config.sort_dirsfirst = true
+    else if (opt.is("--no-group-directories-first")) config.sort_dirsfirst = false
+    else if (opt.is("--sort")) {
+        var val: []const u8 = args.arg();
+        var ord: ?config.SortOrder = null;
+        if (std.mem.endsWith(u8, val, "-asc")) {
+            val = val[0..val.len-4];
+            ord = .asc;
+        } else if (std.mem.endsWith(u8, val, "-desc")) {
+            val = val[0..val.len-5];
+            ord = .desc;
+        }
+        if (std.mem.eql(u8, val, "name")) {
+            config.sort_col = .name;
+            config.sort_order = ord orelse .asc;
+        } else if (std.mem.eql(u8, val, "disk-usage")) {
+            config.sort_col = .blocks;
+            config.sort_order = ord orelse .desc;
+        } else if (std.mem.eql(u8, val, "apparent-size")) {
+            config.sort_col = .size;
+            config.sort_order = ord orelse .desc;
+        } else if (std.mem.eql(u8, val, "itemcount")) {
+            config.sort_col = .items;
+            config.sort_order = ord orelse .desc;
+        } else if (std.mem.eql(u8, val, "mtime")) {
+            config.sort_col = .mtime;
+            config.sort_order = ord orelse .asc;
+        } else ui.die("Unknown --sort option: {s}.\n", .{val});
+    } else if (opt.is("--shared-column")) {
+        const val = args.arg();
+        if (std.mem.eql(u8, val, "off")) config.show_shared = .off
+        else if (std.mem.eql(u8, val, "shared")) config.show_shared = .shared
+        else if (std.mem.eql(u8, val, "unique")) config.show_shared = .unique
+        else ui.die("Unknown --shared-column option: {s}.\n", .{val});
+    } else if (opt.is("--apparent-size")) config.show_blocks = false
+    else if (opt.is("--disk-usage")) config.show_blocks = true
+    else if (opt.is("-0")) config.scan_ui = .none
+    else if (opt.is("-1")) config.scan_ui = .line
+    else if (opt.is("-2")) config.scan_ui = .full
+    else if (opt.is("--si")) config.si = true
+    else if (opt.is("--no-si")) config.si = false
+    else if (opt.is("-L") or opt.is("--follow-symlinks")) config.follow_symlinks = true
+    else if (opt.is("--no-follow-symlinks")) config.follow_symlinks = false
+    else if (opt.is("--exclude")) config.exclude_patterns.append(args.arg()) catch unreachable
+    else if (opt.is("-X") or opt.is("--exclude-from")) {
+        const arg = args.arg();
+        readExcludeFile(arg) catch |e| ui.die("Error reading excludes from {s}: {s}.\n", .{ arg, ui.errorString(e) });
+    } else if (opt.is("--exclude-caches")) config.exclude_caches = true
+    else if (opt.is("--include-caches")) config.exclude_caches = false
+    else if (opt.is("--exclude-kernfs")) config.exclude_kernfs = true
+    else if (opt.is("--include-kernfs")) config.exclude_kernfs = false
+    else if (opt.is("--confirm-quit")) config.confirm_quit = true
+    else if (opt.is("--no-confirm-quit")) config.confirm_quit = false
+    else if (opt.is("--confirm-delete")) config.confirm_delete = true
+    else if (opt.is("--no-confirm-delete")) config.confirm_delete = false
+    else if (opt.is("--color")) {
+        const val = args.arg();
+        if (std.mem.eql(u8, val, "off")) config.ui_color = .off
+        else if (std.mem.eql(u8, val, "dark")) config.ui_color = .dark
+        else if (std.mem.eql(u8, val, "dark-bg")) config.ui_color = .darkbg
+        else ui.die("Unknown --color option: {s}.\n", .{val});
+    } else return false;
+    return true;
+}
+
+fn tryReadArgsFile(path: [:0]const u8) void {
+    var args = Args(ArgsFile).init(ArgsFile.open(path) orelse return);
+    while (args.next()) |opt| {
+        if (!argConfig(&args, opt))
+            ui.die("Uncrecognized option in config file '{s}': {s}.\n", .{path, opt.val});
+    }
+}
+
 fn version() noreturn {
     std.io.getStdOut().writer().writeAll("ncdu " ++ program_version ++ "\n") catch {};
     std.process.exit(0);
@@ -238,8 +409,8 @@ fn spawnShell() void {
 }
 
 
-fn readExcludeFile(path: []const u8) !void {
-    const f = try std.fs.cwd().openFile(path, .{});
+fn readExcludeFile(path: [:0]const u8) !void {
+    const f = try std.fs.cwd().openFileZ(path, .{});
     defer f.close();
     var rd = std.io.bufferedReader(f.reader()).reader();
     var buf = std.ArrayList(u8).init(allocator);
@@ -263,14 +434,22 @@ pub fn main() void {
     }
     if (std.os.getenvZ("NO_COLOR") == null) config.ui_color = .darkbg;
 
+    tryReadArgsFile("/etc/ncdu.conf");
+
+    if (std.os.getenvZ("XDG_CONFIG_HOME")) |p| {
+        var path = std.fs.path.joinZ(allocator, &.{p, "ncdu", "config"}) catch unreachable;
+        defer allocator.free(path);
+        tryReadArgsFile(path);
+    } else if (std.os.getenvZ("HOME")) |p| {
+        var path = std.fs.path.joinZ(allocator, &.{p, ".config", "ncdu", "config"}) catch unreachable;
+        defer allocator.free(path);
+        tryReadArgsFile(path);
+    }
+
     var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init());
     var scan_dir: ?[]const u8 = null;
     var import_file: ?[:0]const u8 = null;
     var export_file: ?[:0]const u8 = null;
-    var has_scan_ui = false;
-    var has_can_delete = false;
-    var has_can_shell = false;
-    var has_can_refresh = false;
     _ = args.next(); // program name
     while (args.next()) |opt| {
         if (!opt.opt) {
@@ -280,97 +459,13 @@ pub fn main() void {
             continue;
         }
         if (opt.is("-h") or opt.is("-?") or opt.is("--help")) help()
-        else if(opt.is("-v") or opt.is("-V") or opt.is("--version")) version()
-        else if(opt.is("-q") or opt.is("--slow-ui-updates")) config.update_delay = 2*std.time.ns_per_s
-        else if(opt.is("--fast-ui-updates")) config.update_delay = 100*std.time.ns_per_ms
-        else if(opt.is("-x") or opt.is("--one-file-system")) config.same_fs = true
-        else if(opt.is("--cross-file-system")) config.same_fs = false
-        else if(opt.is("-e") or opt.is("--extended")) config.extended = true
-        else if(opt.is("--no-extended")) config.extended = false
-        else if(opt.is("-r") and !config.can_delete) config.can_shell = false
-        else if(opt.is("-r")) config.can_delete = false
-        else if(opt.is("--enable-shell"))    { has_can_shell = true;   config.can_shell = true; }
-        else if(opt.is("--disable-shell"))   { has_can_shell = true;   config.can_shell = false; }
-        else if(opt.is("--enable-delete"))   { has_can_delete = true;  config.can_delete = true; }
-        else if(opt.is("--disable-delete"))  { has_can_delete = true;  config.can_delete = false; }
-        else if(opt.is("--enable-refresh"))  { has_can_refresh = true; config.can_refresh = true; }
-        else if(opt.is("--disable-refresh")) { has_can_refresh = true; config.can_refresh = false; }
-        else if(opt.is("--show-hidden")) config.show_hidden = true
-        else if(opt.is("--hide-hidden")) config.show_hidden = false
-        else if(opt.is("--show-itemcount")) config.show_items = true
-        else if(opt.is("--hide-itemcount")) config.show_items = false
-        else if(opt.is("--show-mtime")) config.show_mtime = true
-        else if(opt.is("--hide-mtime")) config.show_mtime = false
-        else if(opt.is("--show-graph")) config.show_graph = true
-        else if(opt.is("--hide-graph")) config.show_graph = false
-        else if(opt.is("--show-percent")) config.show_percent = true
-        else if(opt.is("--hide-percent")) config.show_percent = false
-        else if(opt.is("--group-directories-first")) config.sort_dirsfirst = true
-        else if(opt.is("--no-group-directories-first")) config.sort_dirsfirst = false
-        else if(opt.is("--sort")) {
-            var val: []const u8 = args.arg();
-            var ord: ?config.SortOrder = null;
-            if (std.mem.endsWith(u8, val, "-asc")) {
-                val = val[0..val.len-4];
-                ord = .asc;
-            } else if (std.mem.endsWith(u8, val, "-desc")) {
-                val = val[0..val.len-5];
-                ord = .desc;
-            }
-            if (std.mem.eql(u8, val, "name")) {
-                config.sort_col = .name;
-                config.sort_order = ord orelse .asc;
-            } else if (std.mem.eql(u8, val, "disk-usage")) {
-                config.sort_col = .blocks;
-                config.sort_order = ord orelse .desc;
-            } else if (std.mem.eql(u8, val, "apparent-size")) {
-                config.sort_col = .size;
-                config.sort_order = ord orelse .desc;
-            } else if (std.mem.eql(u8, val, "itemcount")) {
-                config.sort_col = .items;
-                config.sort_order = ord orelse .desc;
-            } else if (std.mem.eql(u8, val, "mtime")) {
-                config.sort_col = .mtime;
-                config.sort_order = ord orelse .asc;
-            } else ui.die("Unknown --sort option: {s}.\n", .{val});
-        } else if(opt.is("--shared-column")) {
-            const val = args.arg();
-            if (std.mem.eql(u8, val, "off")) config.show_shared = .off
-            else if (std.mem.eql(u8, val, "shared")) config.show_shared = .shared
-            else if (std.mem.eql(u8, val, "unique")) config.show_shared = .unique
-            else ui.die("Unknown --shared-column option: {s}.\n", .{val});
-        } else if(opt.is("--apparent-size")) config.show_blocks = false
-        else if(opt.is("--disk-usage")) config.show_blocks = 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") and export_file != null) ui.die("The -o flag can only be given once.\n", .{})
-        else if(opt.is("-o")) export_file = args.arg()
-        else if(opt.is("-f") and import_file != null) ui.die("The -f flag can only be given once.\n", .{})
-        else if(opt.is("-f")) import_file = args.arg()
-        else if(opt.is("--si")) config.si = true
-        else if(opt.is("--no-si")) config.si = false
-        else if(opt.is("-L") or opt.is("--follow-symlinks")) config.follow_symlinks = true
-        else if(opt.is("--no-follow-symlinks")) config.follow_symlinks = false
-        else if(opt.is("--exclude")) config.exclude_patterns.append(args.arg()) catch unreachable
-        else if(opt.is("-X") or opt.is("--exclude-from")) {
-            const arg = args.arg();
-            readExcludeFile(arg) catch |e| ui.die("Error reading excludes from {s}: {s}.\n", .{ arg, ui.errorString(e) });
-        } else if(opt.is("--exclude-caches")) config.exclude_caches = true
-        else if(opt.is("--include-caches")) config.exclude_caches = false
-        else if(opt.is("--exclude-kernfs")) config.exclude_kernfs = true
-        else if(opt.is("--include-kernfs")) config.exclude_kernfs = false
-        else if(opt.is("--confirm-quit")) config.confirm_quit = true
-        else if(opt.is("--no-confirm-quit")) config.confirm_quit = false
-        else if(opt.is("--confirm-delete")) config.confirm_delete = true
-        else if(opt.is("--no-confirm-delete")) config.confirm_delete = false
-        else if(opt.is("--color")) {
-            const val = args.arg();
-            if (std.mem.eql(u8, val, "off")) config.ui_color = .off
-            else if (std.mem.eql(u8, val, "dark")) config.ui_color = .dark
-            else if (std.mem.eql(u8, val, "dark-bg")) config.ui_color = .darkbg
-            else ui.die("Unknown --color option: {s}.\n", .{val});
-        } else ui.die("Unrecognized option '{s}'.\n", .{opt.val});
+        else if (opt.is("-v") or opt.is("-V") or opt.is("--version")) version()
+        else if (opt.is("-o") and export_file != null) ui.die("The -o flag can only be given once.\n", .{})
+        else if (opt.is("-o")) export_file = args.arg()
+        else if (opt.is("-f") and import_file != null) ui.die("The -f flag can only be given once.\n", .{})
+        else if (opt.is("-f")) import_file = args.arg()
+        else if (argConfig(&args, opt)) {}
+        else ui.die("Unrecognized option '{s}'.\n", .{opt.val});
     }
 
     if (std.builtin.os.tag != .linux and config.exclude_kernfs)
@@ -378,11 +473,11 @@ pub fn main() void {
 
     const out_tty = std.io.getStdOut().isTty();
     const in_tty = std.io.getStdIn().isTty();
-    if (!has_scan_ui) {
+    if (config.scan_ui == null) {
         if (export_file) |f| {
             if (!out_tty or std.mem.eql(u8, f, "-")) config.scan_ui = .none
             else config.scan_ui = .line;
-        }
+        } else config.scan_ui = .full;
     }
     if (!in_tty and import_file == null and export_file == null)
         ui.die("Standard input is not a TTY. Did you mean to import a file using '-f -'?\n", .{});
@@ -404,9 +499,9 @@ pub fn main() void {
            catch |e| ui.die("Error opening directory: {s}.\n", .{ui.errorString(e)});
     if (out_file != null) return;
 
-    if (config.imported and !has_can_shell) config.can_shell = false;
-    if (config.imported and !has_can_delete) config.can_delete = false;
-    if (config.imported and !has_can_refresh) config.can_refresh = false;
+    config.can_shell = config.can_shell orelse !config.imported;
+    config.can_delete = config.can_delete orelse !config.imported;
+    config.can_refresh = config.can_refresh orelse !config.imported;
 
     config.scan_ui = .full; // in case we're refreshing from the UI, always in full mode.
     ui.init();
diff --git a/src/scan.zig b/src/scan.zig
index 38b9a2e3c9f7db77915ba1a0f3f822ffd234c845..bfa515c9c91a8a0a93efd3846fe0bfc1b5818eb6 100644
--- a/src/scan.zig
+++ b/src/scan.zig
@@ -1059,9 +1059,9 @@ fn drawBox() void {
 }
 
 pub fn draw() void {
-    if (active_context.fatal_error != null and main.config.scan_ui != .full)
+    if (active_context.fatal_error != null and main.config.scan_ui.? != .full)
         ui.die("Error reading {s}: {s}\n", .{ active_context.last_error.?, ui.errorString(active_context.fatal_error.?) });
-    switch (main.config.scan_ui) {
+    switch (main.config.scan_ui.?) {
         .none => {},
         .line => {
             var buf: [256]u8 = undefined;