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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213// SPDX-FileCopyrightText: © 2026 Francesc Muñoz// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Charts drawn in braille, which gives each terminal cell a grid of two by//! four dots. The functions here work out which dots are lit and leave the//! drawing -- the colors, and what an unlit cell looks like -- to the caller.
const std = @import("std");const Allocator = std.mem.Allocator;
const left_dots = [4]u8{ 0x01, 0x02, 0x04, 0x40 };const right_dots = [4]u8{ 0x08, 0x10, 0x20, 0x80 };
fn dotBit(dx: usize, dy: usize) u8 { return (if (dx == 0) left_dots else right_dots)[dy];}
/// The UTF-8 encoding of every braille pattern, `U+2800` plus its dot bits,/// so a cell can be handed a glyph whose lifetime is the program's.pub const glyphs: [256][3]u8 = blk: { @setEvalBranchQuota(10_000); var table: [256][3]u8 = undefined; for (&table, 0..) |*g, bits| { _ = std.unicode.utf8Encode(0x2800 + bits, g) catch unreachable; } break :blk table;};
pub fn glyph(bits: u8) []const u8 { return &glyphs[bits];}
/// The blank braille cell, which is what an empty chart is made of so that it/// keeps the chart's texture rather than turning into spaces.pub const blank = "⠀";
/// The newest `n` points of `data`.fn tail(data: []const f64, n: usize) []const f64 { return data[data.len -| n..];}
/// An area chart: `height` rows of `width` cells, written row-major into/// `out` as dot bits, with the newest data at the right edge. With `mirror`/// the area hangs from the top instead of rising from the bottom. A cell with/// no lit dots is zero, which the caller draws as track. `fixed_max` gives an/// absolute scale; without it the chart scales to the peak in the window.pub fn area( out: []u8, data: []const f64, width: usize, height: usize, mirror: bool, fixed_max: ?f64,) void { std.debug.assert(out.len >= width * height); const dot_width = width * 2; const dot_height = height * 4; const points = tail(data, dot_width);
var max: f64 = 1; if (fixed_max) |m| { max = m; } else for (points) |v| max = @max(max, v);
const offset = dot_width - points.len; const dot_height_f: f64 = @floatFromInt(dot_height);
@memset(out[0 .. width * height], 0); for (points, 0..) |raw, i| { const v = if (fixed_max) |m| @min(raw, m) else raw; if (v <= 0) continue; const h: usize = @max(1, @as(usize, @intFromFloat(@round(v / max * dot_height_f)))); const x = offset + i; const cell = x / 2; const dx = x % 2; for (0..dot_height) |dot_row| { const filled = if (mirror) dot_row < h else dot_row >= dot_height - h; if (filled) out[(dot_row / 4) * width + cell] |= dotBit(dx, dot_row % 4); } }}
/// One or more series drawn as lines a dot thick, sharing one scale, with/// consecutive samples joined by vertical runs so the line reads as/// continuous. A cell takes one color, so `owners` says which series each/// cell belongs to: whichever has the most lit dots in it, ties going to the/// later series, which is drawn on top. A cell with no dots is owned by -1.pub fn lines( gpa: Allocator, out: []u8, owners: []i8, series: []const []const f64, width: usize, height: usize, fixed_max: ?f64,) Allocator.Error!void { const dot_width = width * 2; const dot_height = height * 4;
var max: f64 = 1; if (fixed_max) |m| { max = m; } else for (series) |s| for (tail(s, dot_width)) |v| { max = @max(max, v); };
// The owner of every dot, -1 where unlit. const dots = try gpa.alloc(i8, dot_width * dot_height); defer gpa.free(dots); @memset(dots, -1);
const top: f64 = @floatFromInt(dot_height - 1); for (series, 0..) |s, si| { const points = tail(s, dot_width); const offset = dot_width - points.len; var prev: ?usize = null; for (points, 0..) |v, i| { const clamped = @min(@max(v, 0), max); const y = dot_height - 1 - @as(usize, @intFromFloat(@round(clamped / max * top))); const p = prev orelse y; for (@min(y, p)..@max(y, p) + 1) |yy| dots[yy * dot_width + offset + i] = @intCast(si); prev = y; } }
for (0..height) |row| for (0..width) |cell| { var bits: u8 = 0; var counts: [128]u8 = @splat(0); for (0..2) |dx| for (0..4) |dy| { const o = dots[(row * 4 + dy) * dot_width + cell * 2 + dx]; if (o >= 0) { bits |= dotBit(dx, dy); counts[@intCast(o)] += 1; } }; var owner: i8 = -1; var best: u8 = 0; for (counts[0..series.len], 0..) |n, s| { if (n > 0 and n >= best) { owner = @intCast(s); best = n; } } out[row * width + cell] = bits; owners[row * width + cell] = owner; };}
const testing = std.testing;
test "area dimensions and blank cells" { var out: [20]u8 = undefined; area(&out, &.{}, 10, 2, false, null); for (out) |bits| try testing.expectEqual(@as(u8, 0), bits);}
test "area max value fills the whole column" { var out: [15]u8 = undefined; area(&out, &.{100}, 5, 3, false, null); // The newest point is right-aligned, so it is the last cell of every row, // in its right-hand dot column. for (0..3) |row| try testing.expectEqualStrings("⢸", glyph(out[row * 5 + 4]));}
test "area mirror hangs from the top" { var out: [2]u8 = undefined; area(&out, &.{ 100, 100 }, 1, 2, true, null); try testing.expectEqualStrings("⣿", glyph(out[0])); try testing.expectEqualStrings("⣿", glyph(out[1])); // A small value next to the max fills only the top dots of its column, so // the bottom row has the left column and nothing of the right. area(&out, &.{ 100, 25 }, 1, 2, true, null); try testing.expectEqualStrings("⡇", glyph(out[1]));}
test "area fixed max is an absolute scale" { var out: [1]u8 = undefined; area(&out, &.{ 25, 25 }, 1, 1, false, 100); try testing.expectEqualStrings("⣀", glyph(out[0])); // Values over the scale are clamped rather than overflowing. area(&out, &.{ 500, 500 }, 1, 1, false, 100); try testing.expectEqualStrings("⣿", glyph(out[0]));}
test "area zero values are track" { var out: [2]u8 = undefined; area(&out, &.{ 0, 0, 100, 100 }, 2, 1, false, null); try testing.expectEqual(@as(u8, 0), out[0]); try testing.expect(out[1] != 0);}
test "lines give a cell to the series with more dots in it" { var out: [2]u8 = undefined; var owners: [2]i8 = undefined; // Series 0 sits at the bottom across both cells, series 1 at the top of // the second cell only. try lines(testing.allocator, &out, &owners, &.{ &.{ 0, 0, 0, 0 }, &.{ 4, 4 } }, 2, 1, 4); try testing.expectEqual(@as(i8, 0), owners[0]); try testing.expectEqualStrings("⣀", glyph(out[0])); try testing.expectEqual(@as(i8, 1), owners[1]); try testing.expectEqualStrings("⣉", glyph(out[1]));}
test "lines join consecutive samples" { var out: [1]u8 = undefined; var owners: [1]i8 = undefined; try lines(testing.allocator, &out, &owners, &.{&.{ 0, 3 }}, 1, 1, 3); // The left dot at the bottom, then the right column from the bottom all // the way up to the top. try testing.expectEqualStrings("⣸", glyph(out[0]));}