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.1 kB 192 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 + empty blocks. width is total cells. 79pub fn meter(percent: Float, width: Int) -> String { 80 let p = float.clamp(percent, 0.0, 100.0) 81 let filled = float.round(p /. 100.0 *. int.to_float(width)) 82 let filled = int.clamp(filled, 0, width) 83 string.repeat("", filled) <> string.repeat("", width - filled) 84} 85 86/// Gradient meter characters for finer resolution (btop-ish). 87pub fn meter_fine(percent: Float, width: Int) -> String { 88 let p = float.clamp(percent, 0.0, 100.0) 89 let cells = p /. 100.0 *. int.to_float(width) 90 let full = float.truncate(cells) 91 let frac = cells -. int.to_float(full) 92 // Left-block partials: ▉ ▊ ▋ ▌ ▍ ▎ ▏ 93 let partial = case frac { 94 f if f >=. 0.875 -> "\u{2589}" 95 f if f >=. 0.75 -> "\u{258A}" 96 f if f >=. 0.625 -> "\u{258B}" 97 f if f >=. 0.5 -> "\u{258C}" 98 f if f >=. 0.375 -> "\u{258D}" 99 f if f >=. 0.25 -> "\u{258E}" 100 f if f >=. 0.125 -> "\u{258F}" 101 f if f >. 0.0 -> "\u{258F}" 102 _ -> "" 103 } 104 let full = int.clamp(full, 0, width) 105 let body = string.repeat("", full) <> partial 106 let len = string.length(body) 107 case len >= width { 108 True -> string.slice(body, 0, width) 109 False -> body <> string.repeat(" ", width - len) 110 } 111} 112 113/// Sparkline from 0–100 history values. 114pub fn sparkline(history: List(Float), width: Int) -> String { 115 let chars = [" ", "", "", "", "", "", "", "", ""] 116 let n = list.length(chars) 117 let hist = case list.length(history) > width { 118 True -> list.drop(history, list.length(history) - width) 119 False -> history 120 } 121 let padded = case list.length(hist) < width { 122 True -> list.append(list.repeat(0.0, width - list.length(hist)), hist) 123 False -> hist 124 } 125 padded 126 |> list.map(fn(v) { 127 let v = float.clamp(v, 0.0, 100.0) 128 let idx = 129 float.round(v /. 100.0 *. int.to_float(n - 1)) 130 |> int.clamp(0, n - 1) 131 case list.drop(chars, idx) { 132 [c, ..] -> c 133 [] -> " " 134 } 135 }) 136 |> string.concat 137} 138 139pub fn format_uptime(secs: Float) -> String { 140 let total = float.truncate(secs) 141 let days = total / 86_400 142 let hours = int.remainder(total / 3600, 24) |> result_or_0 143 let mins = int.remainder(total / 60, 60) |> result_or_0 144 case days > 0 { 145 True -> 146 int.to_string(days) 147 <> "d " 148 <> int.to_string(hours) 149 <> "h " 150 <> int.to_string(mins) 151 <> "m" 152 False -> 153 case hours > 0 { 154 True -> int.to_string(hours) <> "h " <> int.to_string(mins) <> "m" 155 False -> int.to_string(mins) <> "m" 156 } 157 } 158} 159 160fn result_or_0(r: Result(Int, Nil)) -> Int { 161 case r { 162 Ok(n) -> n 163 Error(_) -> 0 164 } 165} 166 167pub fn format_load(a: Float, b: Float, c: Float) -> String { 168 one_decimal(a) <> " " <> one_decimal(b) <> " " <> one_decimal(c) 169} 170 171/// Color band for usage: green / yellow / red thresholds. 172pub type Band { 173 Low 174 Mid 175 High 176 Critical 177} 178 179pub fn band(percent: Float) -> Band { 180 case percent >=. 90.0 { 181 True -> Critical 182 False -> 183 case percent >=. 70.0 { 184 True -> High 185 False -> 186 case percent >=. 40.0 { 187 True -> Mid 188 False -> Low 189 } 190 } 191 } 192}