Add configuration file support

This commit is contained in:
Yorhel 2021-10-06 11:05:56 +02:00
parent 88c8f13c35
commit 4b1da95835
4 changed files with 228 additions and 112 deletions

View file

@ -266,6 +266,27 @@ The default is I<dark-bg> unless the C<NO_COLOR> environment variable is set.
=back =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 =head1 KEYS
=over =over

View file

@ -727,7 +727,7 @@ pub fn draw() void {
if (main.config.imported) { if (main.config.imported) {
ui.move(0, saturateSub(ui.cols, 10)); ui.move(0, saturateSub(ui.cols, 10));
ui.addstr("[imported]"); ui.addstr("[imported]");
} else if (!main.config.can_delete) { } else if (!main.config.can_delete.?) {
ui.move(0, saturateSub(ui.cols, 10)); ui.move(0, saturateSub(ui.cols, 10));
ui.addstr("[readonly]"); ui.addstr("[readonly]");
} }
@ -838,7 +838,7 @@ pub fn keyInput(ch: i32) void {
'?' => state = .help, '?' => state = .help,
'i' => if (dir_items.items.len > 0) info.set(dir_items.items[cursor_idx], .info), 'i' => if (dir_items.items.len > 0) info.set(dir_items.items[cursor_idx], .info),
'r' => { 'r' => {
if (!main.config.can_refresh) if (!main.config.can_refresh.?)
message = "Directory refresh feature disabled." message = "Directory refresh feature disabled."
else { else {
main.state = .refresh; main.state = .refresh;
@ -846,14 +846,14 @@ pub fn keyInput(ch: i32) void {
} }
}, },
'b' => { 'b' => {
if (!main.config.can_shell) if (!main.config.can_shell.?)
message = "Shell feature disabled." message = "Shell feature disabled."
else else
main.state = .shell; main.state = .shell;
}, },
'd' => { 'd' => {
if (dir_items.items.len == 0) { if (dir_items.items.len == 0) {
} else if (!main.config.can_delete) } else if (!main.config.can_delete.?)
message = "Deletion feature disabled." message = "Deletion feature disabled."
else if (dir_items.items[cursor_idx]) |e| { else if (dir_items.items[cursor_idx]) |e| {
main.state = .delete; main.state = .delete;

View file

@ -9,6 +9,7 @@ const scan = @import("scan.zig");
const ui = @import("ui.zig"); const ui = @import("ui.zig");
const browser = @import("browser.zig"); const browser = @import("browser.zig");
const delete = @import("delete.zig"); const delete = @import("delete.zig");
const util = @import("util.zig");
const c = @cImport(@cInclude("locale.h")); const c = @cImport(@cInclude("locale.h"));
// "Custom" allocator that wraps the libc allocator and calls ui.oom() on error. // "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 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 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 si: bool = false;
pub var nc_tty: bool = false; pub var nc_tty: bool = false;
pub var ui_color: enum { off, dark, darkbg } = .off; 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 sort_dirsfirst: bool = false;
pub var imported: bool = false; pub var imported: bool = false;
pub var can_delete: bool = true; pub var can_delete: ?bool = true;
pub var can_shell: bool = true; pub var can_shell: ?bool = true;
pub var can_refresh: bool = true; pub var can_refresh: ?bool = true;
pub var confirm_quit: bool = false; pub var confirm_quit: bool = false;
pub var confirm_delete: bool = true; pub var confirm_delete: bool = true;
pub var ignore_delete_errors: bool = false; 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 { fn version() noreturn {
std.io.getStdOut().writer().writeAll("ncdu " ++ program_version ++ "\n") catch {}; std.io.getStdOut().writer().writeAll("ncdu " ++ program_version ++ "\n") catch {};
std.process.exit(0); std.process.exit(0);
@ -238,8 +409,8 @@ fn spawnShell() void {
} }
fn readExcludeFile(path: []const u8) !void { fn readExcludeFile(path: [:0]const u8) !void {
const f = try std.fs.cwd().openFile(path, .{}); const f = try std.fs.cwd().openFileZ(path, .{});
defer f.close(); defer f.close();
var rd = std.io.bufferedReader(f.reader()).reader(); var rd = std.io.bufferedReader(f.reader()).reader();
var buf = std.ArrayList(u8).init(allocator); 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; 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 args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init());
var scan_dir: ?[]const u8 = null; var scan_dir: ?[]const u8 = null;
var import_file: ?[:0]const u8 = null; var import_file: ?[:0]const u8 = null;
var export_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 _ = args.next(); // program name
while (args.next()) |opt| { while (args.next()) |opt| {
if (!opt.opt) { if (!opt.opt) {
@ -280,97 +459,13 @@ pub fn main() void {
continue; continue;
} }
if (opt.is("-h") or opt.is("-?") or opt.is("--help")) help() 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("-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("-o") and export_file != null) ui.die("The -o flag can only be given once.\n", .{})
else if(opt.is("--fast-ui-updates")) config.update_delay = 100*std.time.ns_per_ms else if (opt.is("-o")) export_file = args.arg()
else if(opt.is("-x") or opt.is("--one-file-system")) config.same_fs = true else if (opt.is("-f") and import_file != null) ui.die("The -f flag can only be given once.\n", .{})
else if(opt.is("--cross-file-system")) config.same_fs = false else if (opt.is("-f")) import_file = args.arg()
else if(opt.is("-e") or opt.is("--extended")) config.extended = true else if (argConfig(&args, opt)) {}
else if(opt.is("--no-extended")) config.extended = false else ui.die("Unrecognized option '{s}'.\n", .{opt.val});
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});
} }
if (std.builtin.os.tag != .linux and config.exclude_kernfs) 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 out_tty = std.io.getStdOut().isTty();
const in_tty = std.io.getStdIn().isTty(); const in_tty = std.io.getStdIn().isTty();
if (!has_scan_ui) { if (config.scan_ui == null) {
if (export_file) |f| { if (export_file) |f| {
if (!out_tty or std.mem.eql(u8, f, "-")) config.scan_ui = .none if (!out_tty or std.mem.eql(u8, f, "-")) config.scan_ui = .none
else config.scan_ui = .line; else config.scan_ui = .line;
} } else config.scan_ui = .full;
} }
if (!in_tty and import_file == null and export_file == null) 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", .{}); 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)}); catch |e| ui.die("Error opening directory: {s}.\n", .{ui.errorString(e)});
if (out_file != null) return; if (out_file != null) return;
if (config.imported and !has_can_shell) config.can_shell = false; config.can_shell = config.can_shell orelse !config.imported;
if (config.imported and !has_can_delete) config.can_delete = false; config.can_delete = config.can_delete orelse !config.imported;
if (config.imported and !has_can_refresh) config.can_refresh = false; 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. config.scan_ui = .full; // in case we're refreshing from the UI, always in full mode.
ui.init(); ui.init();

View file

@ -1059,9 +1059,9 @@ fn drawBox() void {
} }
pub fn draw() 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.?) }); 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 => {}, .none => {},
.line => { .line => {
var buf: [256]u8 = undefined; var buf: [256]u8 = undefined;