mirror of
https://code.blicky.net/yorhel/ncdu.git
synced 2026-01-12 17:08:39 -09:00
Link to ncurses + some rudimentary TUI frameworky stuff
I tried playing with zbox (pure Zig termbox-like lib) for a bit, but I don't think I want to have to deal with the terminal support issues that will inevitably come with it. I already stumbled upon one myself: it doesn't properly put the terminal in a sensible state after cleanup in tmux. As much as I dislike ncurses, it /is/ ubiquitous and tends to kind of work.
This commit is contained in:
parent
3e27d37012
commit
826c2fc067
4 changed files with 230 additions and 22 deletions
|
|
@ -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();
|
||||
|
|
|
|||
22
src/browser.zig
Normal file
22
src/browser.zig
Normal file
|
|
@ -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.");
|
||||
}
|
||||
82
src/main.zig
82
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();
|
||||
|
|
|
|||
146
src/ui.zig
Normal file
146
src/ui.zig
Normal file
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in a new issue