Something went wrong. Try again.
htop for your network: a real-time IP traffic monitor for the terminal, ported to Zig from iptop
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331// SPDX-FileCopyrightText: © 2026 Francesc Muñoz// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! How byte counts, rates and gauges are written down. Everything here is a//! formatter usable with `{f}` or a function of plain numbers, so the panels//! can print into whatever buffer the frame is being built in.
const std = @import("std");const Writer = std.Io.Writer;
const units = [_][]const u8{ "B", "KB", "MB", "GB", "TB" };const compact_units = [_][]const u8{ "B", "K", "M", "G", "T" };
const Scaled = struct { value: f64, unit: usize };
fn scale(n: f64) Scaled { var value = n; var unit: usize = 0; while (value >= 1024 and unit < units.len - 1) { value /= 1024; unit += 1; } return .{ .value = value, .unit = unit };}
/// Three significant figures or so: `512 B`, `1.50 KB`, `10.0 MB`, `150 GB`./// Whole bytes are never given a fraction.fn writeScaled(w: *Writer, s: Scaled) Writer.Error!void { if (s.value >= 100 or s.unit == 0) { try w.print("{d:.0}", .{s.value}); } else if (s.value >= 10) { try w.print("{d:.1}", .{s.value}); } else { try w.print("{d:.2}", .{s.value}); }}
/// A byte count with a space before its unit: `1.50 KB`.pub const Bytes = struct { n: f64,
pub fn format(self: Bytes, w: *Writer) Writer.Error!void { const s = scale(self.n); try writeScaled(w, s); try w.print(" {s}", .{units[s.unit]}); }};
/// A byte count per second: `2.00 KB/s`.pub const Rate = struct { n: f64,
pub fn format(self: Rate, w: *Writer) Writer.Error!void { try w.print("{f}/s", .{Bytes{ .n = self.n }}); }};
/// A rate split into its number and its unit, for a caller that typesets the/// two differently.pub const SplitRate = struct { value: Value, unit: []const u8,
pub const Value = struct { scaled: Scaled, pub fn format(self: Value, w: *Writer) Writer.Error!void { try writeScaled(w, self.scaled); } };};
pub fn splitRate(n: f64) SplitRate { const s = scale(n); const rate_units = [_][]const u8{ "B/s", "KB/s", "MB/s", "GB/s", "TB/s" }; return .{ .value = .{ .scaled = s }, .unit = rate_units[s.unit] };}
/// The terse form for narrow columns: `0`, `999B`, `2.0K`, `150K`, `3.0M`./// Anything under a byte is `0`, so a bar is never drawn beside a number that/// reads as nothing.pub const Compact = struct { n: f64,
pub fn format(self: Compact, w: *Writer) Writer.Error!void { if (self.n < 1) return w.writeAll("0"); var value = self.n; var unit: usize = 0; while (value >= 1024 and unit < compact_units.len - 1) { value /= 1024; unit += 1; } if (value >= 10 or unit == 0) { try w.print("{d:.0}", .{@round(value)}); } else { try w.print("{d:.1}", .{value}); } try w.writeAll(compact_units[unit]); }};
/// Left-aligned partial blocks, by eighths: index 0 is no block at all.pub const fill_eighths = [_][]const u8{ "", "▏", "▎", "▍", "▌", "▋", "▊", "▉" };
/// A gauge `width` cells wide: `full` solid blocks, then the partial block/// `fill_eighths[partial]` if `partial` is not zero, then `track` cells of/// track. The three always add up to `width`.pub const Gauge = struct { full: usize, partial: u3, track: usize,
pub fn fillLen(self: Gauge) usize { return self.full + @intFromBool(self.partial != 0); }};
fn eighthsFor(value: f64, max: f64, cells: usize) usize { if (max <= 0 or value <= 0) return 0; const ratio = @min(1.0, value / max); const cells_f: f64 = @floatFromInt(cells); // A value that rounds to nothing still gets one eighth, so that a host // doing a few bytes a second is visibly not idle. return @max(1, @as(usize, @intFromFloat(@round(ratio * cells_f * 8))));}
/// Solid-block fill over a dashed track. The two parts are drawn in different/// colors, so they are returned apart.pub fn gauge(value: f64, max: f64, width: usize) Gauge { const eighths = eighthsFor(value, max, width); const full = eighths / 8; const partial: u3 = @intCast(eighths % 8); const fill = full + @intFromBool(partial != 0); return .{ .full = full, .partial = partial, .track = width - fill };}
/// A center-pivot gauge: download grows leftward from the pivot and upload/// rightward, both against the same `max` so rows compare across it.pub const MirrorGauge = struct { left_track: usize, /// The left side's boundary cell, as an index into `fill_eighths`, to be /// drawn *inverted* -- foreground the surface, background the fill -- so /// that its ink reads as right-aligned. Unicode has only left-aligned /// partial blocks. Zero is no boundary cell. left_partial: u3, left_fill: usize, right_fill: usize, right_partial: u3, right_track: usize,};
/// `width` includes the pivot cell, which the caller draws between the two/// halves; each half is `(width - 1) / 2` cells.pub fn mirrorGauge(rx: f64, tx: f64, max: f64, width: usize) MirrorGauge { const side = (width - 1) / 2; const le = eighthsFor(rx, max, side); const l_full = le / 8; const l_part: u3 = @intCast(le % 8); const re = eighthsFor(tx, max, side); const r_full = re / 8; const r_part: u3 = @intCast(re % 8); return .{ .left_track = side - l_full - @intFromBool(l_part != 0), .left_partial = if (l_part != 0) @intCast(8 - @as(u4, l_part)) else 0, .left_fill = l_full, .right_fill = r_full, .right_partial = r_part, .right_track = side - r_full - @intFromBool(r_part != 0), };}
/// The number of codepoints in `s`, which for everything this program prints/// is the number of cells it takes.pub fn displayLen(s: []const u8) usize { return std.unicode.utf8CountCodepoints(s) catch s.len;}
/// The longest prefix of `s` that is at most `n` codepoints.fn prefixCodepoints(s: []const u8, n: usize) []const u8 { var it = std.unicode.Utf8View.initUnchecked(s).iterator(); var i: usize = 0; while (i < n) : (i += 1) { if (it.nextCodepointSlice() == null) break; } return s[0..it.i];}
const Align = enum { left, right };
fn writePadded(w: *Writer, s: []const u8, width: usize, alignment: Align) Writer.Error!void { const len = displayLen(s); if (len > width) { try w.writeAll(prefixCodepoints(s, width -| 1)); return w.writeAll("…"); } if (alignment == .right) try w.splatByteAll(' ', width - len); try w.writeAll(s); if (alignment == .left) try w.splatByteAll(' ', width - len);}
/// `s` in a field `width` cells wide, left-aligned, truncated with an/// ellipsis when it does not fit.pub const PadEnd = struct { s: []const u8, width: usize, pub fn format(self: PadEnd, w: *Writer) Writer.Error!void { return writePadded(w, self.s, self.width, .left); }};
/// `s` in a field `width` cells wide, right-aligned, truncated with an/// ellipsis when it does not fit.pub const PadStart = struct { s: []const u8, width: usize, pub fn format(self: PadStart, w: *Writer) Writer.Error!void { return writePadded(w, self.s, self.width, .right); }};
pub fn padEnd(s: []const u8, width: usize) PadEnd { return .{ .s = s, .width = width };}
pub fn padStart(s: []const u8, width: usize) PadStart { return .{ .s = s, .width = width };}
const testing = std.testing;
fn expectFmt(expected: []const u8, comptime fmt: []const u8, args: anytype) !void { var buffer: [128]u8 = undefined; try testing.expectEqualStrings(expected, try std.fmt.bufPrint(&buffer, fmt, args));}
test "Bytes scales units" { try expectFmt("0 B", "{f}", .{Bytes{ .n = 0 }}); try expectFmt("512 B", "{f}", .{Bytes{ .n = 512 }}); try expectFmt("1.00 KB", "{f}", .{Bytes{ .n = 1024 }}); try expectFmt("1.50 KB", "{f}", .{Bytes{ .n = 1536 }}); try expectFmt("10.0 MB", "{f}", .{Bytes{ .n = 10 * 1024 * 1024 }}); try expectFmt("5.50 GB", "{f}", .{Bytes{ .n = 5.5 * 1024 * 1024 * 1024 }});}
test "Rate appends /s" { try expectFmt("2.00 KB/s", "{f}", .{Rate{ .n = 2048 }}); const split = splitRate(2048); try expectFmt("2.00", "{f}", .{split.value}); try testing.expectEqualStrings("KB/s", split.unit);}
test "Compact units" { try expectFmt("0", "{f}", .{Compact{ .n = 0 }}); try expectFmt("999B", "{f}", .{Compact{ .n = 999 }}); try expectFmt("2.0K", "{f}", .{Compact{ .n = 2048 }}); try expectFmt("150K", "{f}", .{Compact{ .n = 150 * 1024 }}); try expectFmt("3.0M", "{f}", .{Compact{ .n = 3 * 1024 * 1024 }});}
test "gauge fill and track always sum to width" { for ([_]f64{ 0, 1, 31, 50, 99, 100, 250 }) |value| { const g = gauge(value, 100, 14); try testing.expectEqual(@as(usize, 14), g.fillLen() + g.track); }}
test "gauge extremes" { try testing.expectEqual(Gauge{ .full = 10, .partial = 0, .track = 0 }, gauge(100, 100, 10)); try testing.expectEqual(Gauge{ .full = 0, .partial = 0, .track = 10 }, gauge(0, 100, 10));}
test "gauge keeps tiny values visible" { try testing.expect(gauge(0.1, 1000, 10).fillLen() > 0);}
test "mirrorGauge fills each half exactly" { const cases = [_][2]f64{ .{ 0, 0 }, .{ 1, 0 }, .{ 0, 1 }, .{ 30, 25 }, .{ 99, 1 }, .{ 100, 100 }, .{ 200, 100 }, .{ 0.05, 0.05 } }; for (cases) |case| { const g = mirrorGauge(case[0], case[1], 100, 15); try testing.expectEqual(@as(usize, 7), g.left_track + @intFromBool(g.left_partial != 0) + g.left_fill); try testing.expectEqual(@as(usize, 7), g.right_fill + @intFromBool(g.right_partial != 0) + g.right_track); }}
test "mirrorGauge halves mirror at the half mark" { // A side of 7 cells at 50% is 28 eighths: three full cells and a half. const g = mirrorGauge(50, 50, 100, 15); try testing.expectEqual(@as(usize, 3), g.left_fill); try testing.expectEqualStrings("▌", fill_eighths[g.left_partial]); try testing.expectEqual(@as(usize, 3), g.left_track); try testing.expectEqual(@as(usize, 3), g.right_fill); try testing.expectEqualStrings("▌", fill_eighths[g.right_partial]); try testing.expectEqual(@as(usize, 3), g.right_track);}
test "mirrorGauge one-directional traffic leaves the other side as track" { const g = mirrorGauge(100, 0, 100, 15); try testing.expectEqual(@as(usize, 7), g.left_fill); try testing.expectEqual(@as(usize, 0), g.left_track); try testing.expectEqual(@as(usize, 0), g.right_fill); try testing.expectEqual(@as(usize, 7), g.right_track);}
test "mirrorGauge keeps tiny values visible on their side" { const g = mirrorGauge(0.1, 0.1, 1000, 15); try testing.expect(g.left_partial != 0 or g.left_fill > 0); try testing.expect(g.right_partial != 0 or g.right_fill > 0);}
test "mirrorGauge idle is all track" { const g = mirrorGauge(0, 0, 100, 11); try testing.expectEqual(MirrorGauge{ .left_track = 5, .left_partial = 0, .left_fill = 0, .right_fill = 0, .right_partial = 0, .right_track = 5, }, g);}
test "padEnd truncates with an ellipsis" { try expectFmt("abc…", "{f}", .{padEnd("abcdef", 4)}); try expectFmt("ab ", "{f}", .{padEnd("ab", 4)}); try expectFmt("né…", "{f}", .{padEnd("nééé", 3)});}
test "padStart pads on the left" { try expectFmt(" 42", "{f}", .{padStart("42", 5)});}