// SPDX-FileCopyrightText: © 2026 Francesc Muñoz // SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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)}); }