mirror of
https://code.blicky.net/yorhel/ncdu.git
synced 2026-01-13 01:08:41 -09:00
More UI stuff: nice string handling/shortening + Zig bug workaround
libc locale-dependent APIs are pure madness, but I can't avoid them as long as I use ncurses. libtickit seems like a much saner alternative (at first glance), but no popular application seems to use it. :(
This commit is contained in:
parent
a28a0788c3
commit
a54c10bffb
5 changed files with 205 additions and 19 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ pub fn main() anyerror!void {
|
|||
|
||||
ui.init();
|
||||
defer ui.deinit();
|
||||
browser.draw();
|
||||
try browser.draw();
|
||||
|
||||
_ = ui.c.getch();
|
||||
|
||||
|
|
|
|||
23
src/ncurses_refs.c
Normal file
23
src/ncurses_refs.c
Normal file
|
|
@ -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 ; }
|
||||
163
src/ui.zig
163
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));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue