Terminal system monitor in Gleam — htop × btop
1

Configure Feed

Select the types of activity you want to include in your feed.

monitor / src / format.gleam
5.2 kB 193 lines
1//// Human-readable sizes, percentages, meter bars, and sparklines. 2//// Visual style inspired by btop; table density inspired by htop. 3 4import gleam/float 5import gleam/int 6import gleam/list 7import gleam/string 8 9/// Format kibibytes as human size (K/M/G/T). 10pub fn human_kb(kb: Int) -> String { 11 let b = int.to_float(kb) 12 case kb < 1024 { 13 True -> int.to_string(kb) <> "K" 14 False -> 15 case kb < 1024 * 1024 { 16 True -> one_decimal(b /. 1024.0) <> "M" 17 False -> 18 case kb < 1024 * 1024 * 1024 { 19 True -> one_decimal(b /. 1_048_576.0) <> "G" 20 False -> one_decimal(b /. 1_073_741_824.0) <> "T" 21 } 22 } 23 } 24} 25 26pub fn one_decimal(x: Float) -> String { 27 let x = float.to_precision(x, 1) 28 float.to_string(x) 29} 30 31pub fn percent(p: Float) -> String { 32 let p = float.clamp(p, 0.0, 999.9) 33 case p <. 10.0 { 34 True -> pad_start(one_decimal(p), 4) <> "%" 35 False -> 36 case p <. 100.0 { 37 True -> pad_start(one_decimal(p), 4) <> "%" 38 False -> pad_start(int.to_string(float.round(p)), 4) <> "%" 39 } 40 } 41} 42 43pub fn percent_short(p: Float) -> String { 44 one_decimal(float.clamp(p, 0.0, 100.0)) <> "%" 45} 46 47fn pad_start(s: String, width: Int) -> String { 48 string.pad_start(s, to: width, with: " ") 49} 50 51pub fn pad_end(s: String, width: Int) -> String { 52 string.pad_end(s, to: width, with: " ") 53} 54 55pub fn pad_left(s: String, width: Int) -> String { 56 string.pad_start(s, to: width, with: " ") 57} 58 59pub fn truncate(s: String, width: Int) -> String { 60 case string.length(s) <= width { 61 True -> s 62 False -> 63 case width <= 1 { 64 True -> string.slice(s, 0, width) 65 False -> string.slice(s, 0, width - 1) <> "" 66 } 67 } 68} 69 70pub fn fit(s: String, width: Int) -> String { 71 truncate(s, width) |> pad_end(width) 72} 73 74pub fn fit_right(s: String, width: Int) -> String { 75 truncate(s, width) |> pad_left(width) 76} 77 78/// Solid meter: filled blocks + light track. width is total cells. 79/// Prefer `ui.colored_meter` for the live TUI (truecolor gradient). 80pub fn meter(percent: Float, width: Int) -> String { 81 let p = float.clamp(percent, 0.0, 100.0) 82 let filled = float.round(p /. 100.0 *. int.to_float(width)) 83 let filled = int.clamp(filled, 0, width) 84 string.repeat("", filled) <> string.repeat("", width - filled) 85} 86 87/// Fine meter with partial left-blocks and a light track for empty cells. 88pub fn meter_fine(percent: Float, width: Int) -> String { 89 let p = float.clamp(percent, 0.0, 100.0) 90 let cells = p /. 100.0 *. int.to_float(width) 91 let full = float.truncate(cells) 92 let frac = cells -. int.to_float(full) 93 // Left-block partials: ▉ ▊ ▋ ▌ ▍ ▎ ▏ 94 let partial = case frac { 95 f if f >=. 0.875 -> "\u{2589}" 96 f if f >=. 0.75 -> "\u{258A}" 97 f if f >=. 0.625 -> "\u{258B}" 98 f if f >=. 0.5 -> "\u{258C}" 99 f if f >=. 0.375 -> "\u{258D}" 100 f if f >=. 0.25 -> "\u{258E}" 101 f if f >=. 0.125 -> "\u{258F}" 102 f if f >. 0.0 -> "\u{258F}" 103 _ -> "" 104 } 105 let full = int.clamp(full, 0, width) 106 let body = string.repeat("", full) <> partial 107 let len = string.length(body) 108 case len >= width { 109 True -> string.slice(body, 0, width) 110 False -> body <> string.repeat("", width - len) 111 } 112} 113 114/// Sparkline from 0–100 history values. 115pub fn sparkline(history: List(Float), width: Int) -> String { 116 let chars = [" ", "", "", "", "", "", "", "", ""] 117 let n = list.length(chars) 118 let hist = case list.length(history) > width { 119 True -> list.drop(history, list.length(history) - width) 120 False -> history 121 } 122 let padded = case list.length(hist) < width { 123 True -> list.append(list.repeat(0.0, width - list.length(hist)), hist) 124 False -> hist 125 } 126 padded 127 |> list.map(fn(v) { 128 let v = float.clamp(v, 0.0, 100.0) 129 let idx = 130 float.round(v /. 100.0 *. int.to_float(n - 1)) 131 |> int.clamp(0, n - 1) 132 case list.drop(chars, idx) { 133 [c, ..] -> c 134 [] -> " " 135 } 136 }) 137 |> string.concat 138} 139 140pub fn format_uptime(secs: Float) -> String { 141 let total = float.truncate(secs) 142 let days = total / 86_400 143 let hours = int.remainder(total / 3600, 24) |> result_or_0 144 let mins = int.remainder(total / 60, 60) |> result_or_0 145 case days > 0 { 146 True -> 147 int.to_string(days) 148 <> "d " 149 <> int.to_string(hours) 150 <> "h " 151 <> int.to_string(mins) 152 <> "m" 153 False -> 154 case hours > 0 { 155 True -> int.to_string(hours) <> "h " <> int.to_string(mins) <> "m" 156 False -> int.to_string(mins) <> "m" 157 } 158 } 159} 160 161fn result_or_0(r: Result(Int, Nil)) -> Int { 162 case r { 163 Ok(n) -> n 164 Error(_) -> 0 165 } 166} 167 168pub fn format_load(a: Float, b: Float, c: Float) -> String { 169 one_decimal(a) <> " " <> one_decimal(b) <> " " <> one_decimal(c) 170} 171 172/// Color band for usage: green / yellow / red thresholds. 173pub type Band { 174 Low 175 Mid 176 High 177 Critical 178} 179 180pub fn band(percent: Float) -> Band { 181 case percent >=. 90.0 { 182 True -> Critical 183 False -> 184 case percent >=. 70.0 { 185 True -> High 186 False -> 187 case percent >=. 40.0 { 188 True -> Mid 189 False -> Low 190 } 191 } 192 } 193}