//// Human-readable sizes, percentages, meter bars, and sparklines. //// Visual style inspired by btop; table density inspired by htop. import gleam/float import gleam/int import gleam/list import gleam/string /// Format kibibytes as human size (K/M/G/T). pub fn human_kb(kb: Int) -> String { let b = int.to_float(kb) case kb < 1024 { True -> int.to_string(kb) <> "K" False -> case kb < 1024 * 1024 { True -> one_decimal(b /. 1024.0) <> "M" False -> case kb < 1024 * 1024 * 1024 { True -> one_decimal(b /. 1_048_576.0) <> "G" False -> one_decimal(b /. 1_073_741_824.0) <> "T" } } } } pub fn one_decimal(x: Float) -> String { let x = float.to_precision(x, 1) float.to_string(x) } pub fn percent(p: Float) -> String { let p = float.clamp(p, 0.0, 999.9) case p <. 10.0 { True -> pad_start(one_decimal(p), 4) <> "%" False -> case p <. 100.0 { True -> pad_start(one_decimal(p), 4) <> "%" False -> pad_start(int.to_string(float.round(p)), 4) <> "%" } } } pub fn percent_short(p: Float) -> String { one_decimal(float.clamp(p, 0.0, 100.0)) <> "%" } fn pad_start(s: String, width: Int) -> String { string.pad_start(s, to: width, with: " ") } pub fn pad_end(s: String, width: Int) -> String { string.pad_end(s, to: width, with: " ") } pub fn pad_left(s: String, width: Int) -> String { string.pad_start(s, to: width, with: " ") } pub fn truncate(s: String, width: Int) -> String { case string.length(s) <= width { True -> s False -> case width <= 1 { True -> string.slice(s, 0, width) False -> string.slice(s, 0, width - 1) <> "…" } } } pub fn fit(s: String, width: Int) -> String { truncate(s, width) |> pad_end(width) } pub fn fit_right(s: String, width: Int) -> String { truncate(s, width) |> pad_left(width) } /// Solid meter: filled blocks + light track. width is total cells. /// Prefer `ui.colored_meter` for the live TUI (truecolor gradient). pub fn meter(percent: Float, width: Int) -> String { let p = float.clamp(percent, 0.0, 100.0) let filled = float.round(p /. 100.0 *. int.to_float(width)) let filled = int.clamp(filled, 0, width) string.repeat("█", filled) <> string.repeat("░", width - filled) } /// Fine meter with partial left-blocks and a light track for empty cells. pub fn meter_fine(percent: Float, width: Int) -> String { let p = float.clamp(percent, 0.0, 100.0) let cells = p /. 100.0 *. int.to_float(width) let full = float.truncate(cells) let frac = cells -. int.to_float(full) // Left-block partials: ▉ ▊ ▋ ▌ ▍ ▎ ▏ let partial = case frac { f if f >=. 0.875 -> "\u{2589}" f if f >=. 0.75 -> "\u{258A}" f if f >=. 0.625 -> "\u{258B}" f if f >=. 0.5 -> "\u{258C}" f if f >=. 0.375 -> "\u{258D}" f if f >=. 0.25 -> "\u{258E}" f if f >=. 0.125 -> "\u{258F}" f if f >. 0.0 -> "\u{258F}" _ -> "" } let full = int.clamp(full, 0, width) let body = string.repeat("█", full) <> partial let len = string.length(body) case len >= width { True -> string.slice(body, 0, width) False -> body <> string.repeat("░", width - len) } } /// Sparkline from 0–100 history values. pub fn sparkline(history: List(Float), width: Int) -> String { let chars = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] let n = list.length(chars) let hist = case list.length(history) > width { True -> list.drop(history, list.length(history) - width) False -> history } let padded = case list.length(hist) < width { True -> list.append(list.repeat(0.0, width - list.length(hist)), hist) False -> hist } padded |> list.map(fn(v) { let v = float.clamp(v, 0.0, 100.0) let idx = float.round(v /. 100.0 *. int.to_float(n - 1)) |> int.clamp(0, n - 1) case list.drop(chars, idx) { [c, ..] -> c [] -> " " } }) |> string.concat } pub fn format_uptime(secs: Float) -> String { let total = float.truncate(secs) let days = total / 86_400 let hours = int.remainder(total / 3600, 24) |> result_or_0 let mins = int.remainder(total / 60, 60) |> result_or_0 case days > 0 { True -> int.to_string(days) <> "d " <> int.to_string(hours) <> "h " <> int.to_string(mins) <> "m" False -> case hours > 0 { True -> int.to_string(hours) <> "h " <> int.to_string(mins) <> "m" False -> int.to_string(mins) <> "m" } } } fn result_or_0(r: Result(Int, Nil)) -> Int { case r { Ok(n) -> n Error(_) -> 0 } } pub fn format_load(a: Float, b: Float, c: Float) -> String { one_decimal(a) <> " " <> one_decimal(b) <> " " <> one_decimal(c) } /// Color band for usage: green / yellow / red thresholds. pub type Band { Low Mid High Critical } pub fn band(percent: Float) -> Band { case percent >=. 90.0 { True -> Critical False -> case percent >=. 70.0 { True -> High False -> case percent >=. 40.0 { True -> Mid False -> Low } } } }