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;