diff --git a/build.zig b/build.zig index 9b91dcc1e3fc0d5f82dee5217193d0d3a1be1c7e..5519afc0f11c7e71eac69d3f74005f6046d27eb5 100644 --- a/build.zig +++ b/build.zig @@ -7,8 +7,9 @@ pub fn build(b: *std.build.Builder) void { const exe = b.addExecutable("ncdu", "src/main.zig"); exe.setTarget(target); exe.setBuildMode(mode); + exe.addCSourceFile("src/ncurses_refs.c", &[_][]const u8{}); exe.linkLibC(); - exe.linkSystemLibrary("ncurses"); + exe.linkSystemLibrary("ncursesw"); exe.install(); const run_cmd = exe.run(); @@ -19,4 +20,10 @@ pub fn build(b: *std.build.Builder) void { const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); + + const tst = b.addTest("src/main.zig"); + tst.linkLibC(); + tst.linkSystemLibrary("ncursesw"); + const tst_step = b.step("test", "Run tests"); + tst_step.dependOn(&tst.step); } diff --git a/src/browser.zig b/src/browser.zig index 567d3472d41795a3c755ce523b1303c06ca24ba0..9024f75cc0ace42c9a0f26d259cd255759657b2f 100644 --- a/src/browser.zig +++ b/src/browser.zig @@ -1,22 +1,31 @@ const std = @import("std"); const main = @import("main.zig"); +const model = @import("model.zig"); const ui = @import("ui.zig"); -pub fn draw() void { +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.move(0,0); + ui.hline(' ', ui.cols); + ui.move(0,0); + ui.addstr("ncdu " ++ main.program_version ++ " ~ Use the arrow keys to navigate, press "); ui.style(.key_hd); - _ = ui.c.addch('?'); + ui.addch('?'); ui.style(.hd); - _ = ui.c.addstr(" for help"); + ui.addstr(" for help"); // TODO: [imported]/[readonly] indicators ui.style(.default); - _ = ui.c.mvhline(1, 0, ' ', ui.cols); - // TODO: path + ui.move(1,0); + ui.hline('-', ui.cols); + ui.move(1,3); + ui.addch(' '); + ui.addstr(try ui.shorten(try ui.toUtf8(model.root.entry.name()), std.math.sub(u32, ui.cols, 5) catch 4)); + ui.addch(' '); ui.style(.hd); - _ = ui.c.mvhline(ui.rows-1, 0, ' ', ui.cols); - _ = ui.c.mvaddstr(ui.rows-1, 1, "No items to display."); + ui.move(ui.rows-1, 0); + ui.hline(' ', ui.cols); + ui.move(ui.rows-1, 1); + ui.addstr("No items to display."); } diff --git a/src/main.zig b/src/main.zig index 24d1461f48a3a1b2c7870faf4d99e0e706bb78c4..cb6043cd78267712b75c7f0eea3a5856b53dd8df 100644 --- a/src/main.zig +++ b/src/main.zig @@ -242,7 +242,7 @@ pub fn main() anyerror!void { ui.init(); defer ui.deinit(); - browser.draw(); + try browser.draw(); _ = ui.c.getch(); diff --git a/src/ncurses_refs.c b/src/ncurses_refs.c new file mode 100644 index 0000000000000000000000000000000000000000..ed1a4c36cd4357c254f3c8ee282aefa20f57cb56 --- /dev/null +++ b/src/ncurses_refs.c @@ -0,0 +1,23 @@ +#include <curses.h> + +/* Zig @cImport() has problems with the ACS_* macros. Two, in fact: + * + * 1. Naively using the ACS_* macros results in: + * + * error: cannot store runtime value in compile time variable + * return acs_map[NCURSES_CAST(u8, c)]; + * ^ + * That error doesn't make much sense to me, but it might be + * related to https://github.com/ziglang/zig/issues/5344? + * + * 2. The 'acs_map' extern variable isn't being linked correctly? + * Haven't investigated this one deeply enough yet, but attempting + * to dereference acs_map from within Zig leads to a segfault; + * its pointer value doesn't make any sense. + */ +chtype ncdu_acs_ulcorner() { return ACS_ULCORNER; } +chtype ncdu_acs_llcorner() { return ACS_LLCORNER; } +chtype ncdu_acs_urcorner() { return ACS_URCORNER; } +chtype ncdu_acs_lrcorner() { return ACS_LRCORNER; } +chtype ncdu_acs_hline() { return ACS_VLINE ; } +chtype ncdu_acs_vline() { return ACS_HLINE ; } diff --git a/src/ui.zig b/src/ui.zig index dff364294bf272a21ac2c2d2541d4a063dab9d1b..14608bd880d778191115946c4a83a811a2ca8f35 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -7,12 +7,15 @@ pub const c = @cImport({ @cInclude("stdio.h"); @cInclude("string.h"); @cInclude("curses.h"); + @cDefine("_X_OPEN_SOURCE", "1"); + @cInclude("wchar.h"); + @cInclude("locale.h"); }); var inited: bool = false; -pub var rows: i32 = undefined; -pub var cols: i32 = undefined; +pub var rows: u32 = undefined; +pub var cols: u32 = undefined; pub fn die(comptime fmt: []const u8, args: anytype) noreturn { deinit(); @@ -20,6 +23,131 @@ pub fn die(comptime fmt: []const u8, args: anytype) noreturn { std.process.exit(1); } +var to_utf8_buf = std.ArrayList(u8).init(main.allocator); + +fn toUtf8BadChar(ch: u8) bool { + return switch (ch) { + 0...0x1F, 0x7F => true, + else => false + }; +} + +// Utility function to convert a string to valid (mostly) printable UTF-8. +// Invalid codepoints will be encoded as '\x##' strings. +// Returns the given string if it's already valid, otherwise points to an +// internal buffer that will be invalidated on the next call. +// (Doesn't check for non-printable Unicode characters) +// (This program assumes that the console locale is UTF-8, but file names may not be) +pub fn toUtf8(in: [:0]const u8) ![:0]const u8 { + const hasBadChar = blk: { + for (in) |ch| if (toUtf8BadChar(ch)) break :blk true; + break :blk false; + }; + if (!hasBadChar and std.unicode.utf8ValidateSlice(in)) return in; + var i: usize = 0; + to_utf8_buf.shrinkRetainingCapacity(0); + while (i < in.len) { + if (std.unicode.utf8ByteSequenceLength(in[i])) |cp_len| { + if (!toUtf8BadChar(in[i]) and i + cp_len <= in.len) { + if (std.unicode.utf8Decode(in[i .. i + cp_len])) |_| { + try to_utf8_buf.appendSlice(in[i .. i + cp_len]); + i += cp_len; + continue; + } else |_| {} + } + } else |_| {} + try to_utf8_buf.writer().print("\\x{X:0>2}", .{in[i]}); + i += 1; + } + return try to_utf8_buf.toOwnedSliceSentinel(0); +} + +var shorten_buf = std.ArrayList(u8).init(main.allocator); + +// Shorten the given string to fit in the given number of columns. +// If the string is too long, only the prefix and suffix will be printed, with '...' in between. +// Input is assumed to be valid UTF-8. +// Return value points to the input string or to an internal buffer that is +// invalidated on a subsequent call. +pub fn shorten(in: [:0]const u8, max_width: u32) ![:0] const u8 { + if (max_width < 4) return "..."; + var total_width: u32 = 0; + var prefix_width: u32 = 0; + var prefix_end: u32 = 0; + var it = std.unicode.Utf8View.initUnchecked(in).iterator(); + while (it.nextCodepoint()) |cp| { + // XXX: libc assumption: wchar_t is a Unicode point. True for most modern libcs? + // (The "proper" way is to use mbtowc(), but I'd rather port the musl wcwidth implementation to Zig so that I *know* it'll be Unicode. + // On the other hand, ncurses also use wcwidth() so that would cause duplicated code. Ugh) + const cp_width_ = c.wcwidth(cp); + const cp_width = @intCast(u32, if (cp_width_ < 0) 1 else cp_width_); + const cp_len = std.unicode.utf8CodepointSequenceLength(cp) catch unreachable; + total_width += cp_width; + if (prefix_width + cp_width <= @divFloor(max_width-1, 2)-1) { + prefix_width += cp_width; + prefix_end += cp_len; + continue; + } + } + if (total_width <= max_width) return in; + + shorten_buf.shrinkRetainingCapacity(0); + try shorten_buf.appendSlice(in[0..prefix_end]); + try shorten_buf.appendSlice("..."); + + var start_width: u32 = prefix_width; + var start_len: u32 = prefix_end; + it = std.unicode.Utf8View.initUnchecked(in[prefix_end..]).iterator(); + while (it.nextCodepoint()) |cp| { + const cp_width_ = c.wcwidth(cp); + const cp_width = @intCast(u32, if (cp_width_ < 0) 1 else cp_width_); + const cp_len = std.unicode.utf8CodepointSequenceLength(cp) catch unreachable; + start_width += cp_width; + start_len += cp_len; + if (total_width - start_width <= max_width - prefix_width - 3) { + try shorten_buf.appendSlice(in[start_len..]); + break; + } + } + return try shorten_buf.toOwnedSliceSentinel(0); +} + +fn shortenTest(in: [:0]const u8, max_width: u32, out: [:0]const u8) void { + std.testing.expectEqualStrings(out, shorten(in, max_width) catch unreachable); +} + +test "shorten" { + _ = c.setlocale(c.LC_ALL, ""); // libc wcwidth() may not recognize Unicode without this + const t = shortenTest; + t("abcde", 3, "..."); + t("abcde", 5, "abcde"); + t("abcde", 4, "...e"); + t("abcdefgh", 6, "a...gh"); + t("abcdefgh", 7, "ab...gh"); + t("ABCDEFGH", 16, "ABCDEFGH"); + t("ABCDEFGH", 7, "A...H"); + t("ABCDEFGH", 8, "A...H"); + t("ABCDEFGH", 9, "A...GH"); + t("AaBCDEFGH", 8, "A...H"); // could optimize this, but w/e + t("ABCDEFGaH", 8, "A...aH"); + t("ABCDEFGH", 15, "ABC...FGH"); +} + +// ncurses_refs.c +extern fn ncdu_acs_ulcorner() c.chtype; +extern fn ncdu_acs_llcorner() c.chtype; +extern fn ncdu_acs_urcorner() c.chtype; +extern fn ncdu_acs_lrcorner() c.chtype; +extern fn ncdu_acs_hline() c.chtype; +extern fn ncdu_acs_vline() c.chtype; + +pub fn acs_ulcorner() c.chtype { return ncdu_acs_ulcorner(); } +pub fn acs_llcorner() c.chtype { return ncdu_acs_llcorner(); } +pub fn acs_urcorner() c.chtype { return ncdu_acs_urcorner(); } +pub fn acs_lrcorner() c.chtype { return ncdu_acs_lrcorner(); } +pub fn acs_hline() c.chtype { return ncdu_acs_hline() ; } +pub fn acs_vline() c.chtype { return ncdu_acs_vline() ; } + const StyleAttr = struct { fg: i16, bg: i16, attr: u32 }; const StyleDef = struct { name: []const u8, @@ -101,14 +229,10 @@ const Style = lbl: { }); }; -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); + rows = @intCast(u32, c.getmaxy(c.stdscr)); + cols = @intCast(u32, c.getmaxx(c.stdscr)); } pub fn init() void { @@ -143,3 +267,26 @@ pub fn deinit() void { _ = c.endwin(); inited = false; } + +pub fn style(s: Style) void { + _ = c.attr_set(styles[@enumToInt(s)].style().attr, @enumToInt(s)+1, null); +} + +pub fn move(y: u32, x: u32) void { + _ = c.move(@intCast(i32, y), @intCast(i32, x)); +} + +// Wraps to the next line if the text overflows, not sure how to disable that. +// (Well, addchstr() does that, but not entirely sure I want to go that way. +// Does that even work with UTF-8? Or do I really need to go wchar madness?) +pub fn addstr(s: [:0]const u8) void { + _ = c.addstr(s); +} + +pub fn addch(ch: c.chtype) void { + _ = c.addch(ch); +} + +pub fn hline(ch: c.chtype, len: u32) void { + _ = c.hline(ch, @intCast(i32, len)); +}