diff --git a/build.zig b/build.zig index 1f693d334974a281b0bc04207cad78f38ec69baf..9b91dcc1e3fc0d5f82dee5217193d0d3a1be1c7e 100644 --- a/build.zig +++ b/build.zig @@ -7,6 +7,8 @@ pub fn build(b: *std.build.Builder) void { const exe = b.addExecutable("ncdu", "src/main.zig"); exe.setTarget(target); exe.setBuildMode(mode); + exe.linkLibC(); + exe.linkSystemLibrary("ncurses"); exe.install(); const run_cmd = exe.run(); diff --git a/src/browser.zig b/src/browser.zig new file mode 100644 index 0000000000000000000000000000000000000000..567d3472d41795a3c755ce523b1303c06ca24ba0 --- /dev/null +++ b/src/browser.zig @@ -0,0 +1,22 @@ +const std = @import("std"); +const main = @import("main.zig"); +const ui = @import("ui.zig"); + +pub fn draw() void { + ui.style(.hd); + _ = ui.c.mvhline(0, 0, ' ', ui.cols); + _ = ui.c.mvaddstr(0, 0, "ncdu " ++ main.program_version ++ " ~ Use the arrow keys to navigate, press "); + ui.style(.key_hd); + _ = ui.c.addch('?'); + ui.style(.hd); + _ = ui.c.addstr(" for help"); + // TODO: [imported]/[readonly] indicators + + ui.style(.default); + _ = ui.c.mvhline(1, 0, ' ', ui.cols); + // TODO: path + + ui.style(.hd); + _ = ui.c.mvhline(ui.rows-1, 0, ' ', ui.cols); + _ = ui.c.mvaddstr(ui.rows-1, 1, "No items to display."); +} diff --git a/src/main.zig b/src/main.zig index 32c53d988ac70e84716859c504401e5ae16e9544..452d78e9747bd3352986f6aa5b4af06e55113a88 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,9 +1,13 @@ +pub const program_version = "2.0"; + const std = @import("std"); const model = @import("model.zig"); const scan = @import("scan.zig"); +const ui = @import("ui.zig"); +const browser = @import("browser.zig"); +const c = @cImport(@cInclude("locale.h")); -var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; -pub const allocator = &general_purpose_allocator.allocator; +pub const allocator = std.heap.c_allocator; pub const Config = struct { same_fs: bool = true, @@ -15,7 +19,9 @@ pub const Config = struct { update_delay: u32 = 100, si: bool = false, - // TODO: color scheme + nc_tty: bool = false, + ui_color: enum { off, dark } = .off, + thousands_sep: []const u8 = ".", read_only: bool = false, can_shell: bool = true, @@ -24,11 +30,6 @@ pub const Config = struct { pub var config = Config{}; -fn die(comptime fmt: []const u8, args: anytype) noreturn { - _ = std.io.getStdErr().writer().print(fmt, args) catch {}; - std.process.exit(1); -} - // Simple generic argument parser, supports getopt_long() style arguments. // T can be any type that has a 'fn next(T) ?[]const u8' method, e.g.: // var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init()); @@ -55,7 +56,7 @@ fn Args(T: anytype) type { return Self{ .it = it }; } - pub fn shortopt(self: *Self, s: []const u8) Option { + fn shortopt(self: *Self, s: []const u8) Option { self.shortbuf[0] = '-'; self.shortbuf[1] = s[0]; self.short = if (s.len > 1) s[1..] else null; @@ -67,18 +68,18 @@ fn Args(T: anytype) type { /// 'opt' indicates whether it's an option or positional argument, /// 'val' will be either -x, --something or the argument. pub fn next(self: *Self) ?Option { - if (self.last_arg != null) die("Option '{s}' does not expect an argument.\n", .{ self.last.? }); + if (self.last_arg != null) ui.die("Option '{s}' does not expect an argument.\n", .{ self.last.? }); if (self.short) |s| return self.shortopt(s); const val = self.it.next() orelse return null; if (self.argsep or val.len == 0 or val[0] != '-') return Option{ .opt = false, .val = val }; - if (val.len == 1) die("Invalid option '-'.\n", .{}); + if (val.len == 1) ui.die("Invalid option '-'.\n", .{}); if (val.len == 2 and val[1] == '-') { self.argsep = true; return self.next(); } if (val[1] == '-') { if (std.mem.indexOfScalar(u8, val, '=')) |sep| { - if (sep == 2) die("Invalid option '{s}'.\n", .{val}); + if (sep == 2) ui.die("Invalid option '{s}'.\n", .{val}); self.last_arg = val[sep+1.. :0]; self.last = val[0..sep]; return Option{ .opt = true, .val = self.last.? }; @@ -100,7 +101,7 @@ fn Args(T: anytype) type { return a; } if (self.it.next()) |o| return o; - die("Option '{s}' requires an argument.\n", .{ self.last.? }); + ui.die("Option '{s}' requires an argument.\n", .{ self.last.? }); } }; } @@ -146,25 +147,53 @@ fn writeTree(out: anytype, e: *model.Entry, indent: u32) @TypeOf(out).Error!void } fn version() noreturn { - // TODO: don't hardcode this version here. - _ = std.io.getStdOut().writer().writeAll("ncdu 2.0\n") catch {}; + std.io.getStdOut().writer().writeAll("ncdu " ++ program_version ++ "\n") catch {}; std.process.exit(0); } fn help() noreturn { - // TODO - _ = std.io.getStdOut().writer().writeAll("ncdu 2.0\n") catch {}; + std.io.getStdOut().writer().writeAll( + "ncdu <options> <directory>\n\n" + ++ " -h,--help This help message\n" + ++ " -q Quiet mode, refresh interval 2 seconds\n" + ++ " -v,-V,--version Print version\n" + ++ " -x Same filesystem\n" + ++ " -e Enable extended information\n" + ++ " -r Read only\n" + ++ " -o FILE Export scanned directory to FILE\n" + ++ " -f FILE Import scanned directory from FILE\n" + ++ " -0,-1,-2 UI to use when scanning (0=none,2=full ncurses)\n" + ++ " --si Use base 10 (SI) prefixes instead of base 2\n" + ++ " --exclude PATTERN Exclude files that match PATTERN\n" + ++ " -X, --exclude-from FILE Exclude files that match any pattern in FILE\n" + ++ " -L, --follow-symlinks Follow symbolic links (excluding directories)\n" + ++ " --exclude-caches Exclude directories containing CACHEDIR.TAG\n" + ++ " --exclude-kernfs Exclude Linux pseudo filesystems (procfs,sysfs,cgroup,...)\n" + ++ " --confirm-quit Confirm quitting ncdu\n" + ++ " --color SCHEME Set color scheme (off/dark)\n" + ) catch {}; std.process.exit(0); } pub fn main() anyerror!void { + // Grab thousands_sep from the current C locale. + // (We can safely remove this when not linking against libc, it's a somewhat obscure feature) + _ = c.setlocale(c.LC_ALL, ""); + if (c.localeconv()) |locale| { + if (locale.*.thousands_sep) |sep| { + const span = std.mem.spanZ(sep); + if (span.len > 0) + config.thousands_sep = span; + } + } + var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init()); var scan_dir: ?[]const u8 = null; _ = args.next(); // program name while (args.next()) |opt| { if (!opt.opt) { // XXX: ncdu 1.x doesn't error, it just silently ignores all but the last argument. - if (scan_dir != null) die("Multiple directories given, see ncdu -h for help.\n", .{}); + if (scan_dir != null) ui.die("Multiple directories given, see ncdu -h for help.\n", .{}); scan_dir = opt.val; continue; } @@ -180,14 +209,23 @@ pub fn main() anyerror!void { else if(opt.is("--exclude-caches")) config.exclude_caches = true else if(opt.is("--exclude-kernfs")) config.exclude_kernfs = true else if(opt.is("--confirm-quit")) config.confirm_quit = true - else die("Unrecognized option '{s}'.\n", .{opt.val}); - // TODO: -o, -f, -0, -1, -2, --exclude, -X, --exclude-from, --color + 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 ui.die("Unknown --color option: {s}.\n", .{val}); + } else ui.die("Unrecognized option '{s}'.\n", .{opt.val}); + // TODO: -o, -f, -0, -1, -2, --exclude, -X, --exclude-from } - std.log.info("align={}, Entry={}, Dir={}, Link={}, File={}.", - .{@alignOf(model.Dir), @sizeOf(model.Entry), @sizeOf(model.Dir), @sizeOf(model.Link), @sizeOf(model.File)}); try scan.scanRoot(scan_dir orelse "."); + ui.init(); + defer ui.deinit(); + browser.draw(); + + _ = ui.c.getch(); + //var out = std.io.bufferedWriter(std.io.getStdOut().writer()); //try writeTree(out.writer(), &model.root.entry, 0); //try out.flush(); diff --git a/src/ui.zig b/src/ui.zig new file mode 100644 index 0000000000000000000000000000000000000000..88410e095bdae7d1fe58f4cfcd2c4bfa7dc348c7 --- /dev/null +++ b/src/ui.zig @@ -0,0 +1,146 @@ +// Ncurses wrappers and TUI helper functions. + +const std = @import("std"); +const main = @import("main.zig"); + +pub const c = @cImport({ + @cInclude("stdio.h"); + @cInclude("string.h"); + @cInclude("unistd.h"); + @cInclude("curses.h"); +}); + +var inited: bool = false; + +pub var rows: i32 = undefined; +pub var cols: i32 = undefined; + +pub fn die(comptime fmt: []const u8, args: anytype) noreturn { + deinit(); + _ = std.io.getStdErr().writer().print(fmt, args) catch {}; + std.process.exit(1); +} + +const StyleAttr = struct { fg: i16, bg: i16, attr: u32 }; +const StyleDef = struct { + name: []const u8, + off: StyleAttr, + dark: StyleAttr, + fn style(self: *const @This()) StyleAttr { + return switch (main.config.ui_color) { + .off => self.off, + .dark => self.dark, + }; + } +}; + +const styles = [_]StyleDef{ + .{ .name = "default", + .off = .{ .fg = -1, .bg = -1, .attr = 0 }, + .dark = .{ .fg = -1, .bg = -1, .attr = 0 } }, + .{ .name = "box_title", + .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD }, + .dark = .{ .fg = c.COLOR_BLUE, .bg = -1, .attr = c.A_BOLD } }, + .{ .name = "hd", // header + footer + .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE }, + .dark = .{ .fg = c.COLOR_BLACK, .bg = c.COLOR_CYAN, .attr = 0 } }, + .{ .name = "sel", + .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE }, + .dark = .{ .fg = c.COLOR_WHITE, .bg = c.COLOR_GREEN, .attr = c.A_BOLD } }, + .{ .name = "num", + .off = .{ .fg = -1, .bg = -1, .attr = 0 }, + .dark = .{ .fg = c.COLOR_YELLOW, .bg = -1, .attr = c.A_BOLD } }, + .{ .name = "num_hd", + .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE }, + .dark = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_CYAN, .attr = c.A_BOLD } }, + .{ .name = "num_sel", + .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE }, + .dark = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_GREEN, .attr = c.A_BOLD } }, + .{ .name = "key", + .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD }, + .dark = .{ .fg = c.COLOR_YELLOW, .bg = -1, .attr = c.A_BOLD } }, + .{ .name = "key_hd", + .off = .{ .fg = -1, .bg = -1, .attr = c.A_BOLD|c.A_REVERSE }, + .dark = .{ .fg = c.COLOR_YELLOW, .bg = c.COLOR_CYAN, .attr = c.A_BOLD } }, + .{ .name = "dir", + .off = .{ .fg = -1, .bg = -1, .attr = 0 }, + .dark = .{ .fg = c.COLOR_BLUE, .bg = -1, .attr = c.A_BOLD } }, + .{ .name = "dir_sel", + .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE }, + .dark = .{ .fg = c.COLOR_BLUE, .bg = c.COLOR_GREEN, .attr = c.A_BOLD } }, + .{ .name = "flag", + .off = .{ .fg = -1, .bg = -1, .attr = 0 }, + .dark = .{ .fg = c.COLOR_RED, .bg = -1, .attr = 0 } }, + .{ .name = "flag_sel", + .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE }, + .dark = .{ .fg = c.COLOR_RED, .bg = c.COLOR_GREEN, .attr = 0 } }, + .{ .name = "graph", + .off = .{ .fg = -1, .bg = -1, .attr = 0 }, + .dark = .{ .fg = c.COLOR_MAGENTA, .bg = -1, .attr = 0 } }, + .{ .name = "graph_sel", + .off = .{ .fg = -1, .bg = -1, .attr = c.A_REVERSE }, + .dark = .{ .fg = c.COLOR_MAGENTA, .bg = c.COLOR_GREEN, .attr = 0 } }, +}; + +const Style = lbl: { + var fields: [styles.len]std.builtin.TypeInfo.EnumField = undefined; + var decls = [_]std.builtin.TypeInfo.Declaration{}; + inline for (styles) |s, i| { + fields[i] = .{ + .name = s.name, + .value = i, + }; + } + break :lbl @Type(.{ + .Enum = .{ + .layout = .Auto, + .tag_type = u8, + .fields = &fields, + .decls = &decls, + .is_exhaustive = true, + } + }); +}; + +pub fn style(s: Style) void { + _ = c.attr_set(styles[@enumToInt(s)].style().attr, @enumToInt(s)+1, null); +} + +fn updateSize() void { + // getmax[yx] macros are marked as "legacy", but Zig can't deal with the "proper" getmaxyx macro. + rows = c.getmaxy(c.stdscr); + cols = c.getmaxx(c.stdscr); +} + +pub fn init() void { + if (inited) return; + 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)) }); + var term = c.newterm(null, tty, tty); + if (term == null) die("Error initializing ncurses.\n", .{}); + _ = c.set_term(term); + } else { + if (c.isatty(0) != 1) die("Standard input is not a TTY. Did you mean to import a file using '-f -'?\n", .{}); + if (c.initscr() == null) die("Error initializing ncurses.\n", .{}); + } + updateSize(); + _ = c.cbreak(); + _ = c.noecho(); + _ = c.curs_set(0); + _ = c.keypad(c.stdscr, true); + + _ = c.start_color(); + _ = c.use_default_colors(); + for (styles) |s, i| _ = c.init_pair(@intCast(i16, i+1), s.style().fg, s.style().bg); + + inited = true; +} + +pub fn deinit() void { + if (!inited) return; + _ = c.erase(); + _ = c.refresh(); + _ = c.endwin(); + inited = false; +}