1 import Base 2 import ./fmt.bend as F 3 import ./model.bend as M 4 import ../usage/norm.bend as UNorm 5 6 # The account lines of the dashboard: the active account as a card with one 7 # bar per window, the others as one line each. Pure. 8 9 def plain(+t: String) -> F.Seg: 10 F.sg(t, F.RPlain{}) 11 12 def muted(+t: String) -> F.Seg: 13 F.sg(t, F.RMuted{}) 14 15 def cat(+a: List<&2, F.Seg>, +b: List<&2, F.Seg>) -> List<&2, F.Seg>: 16 List.append(&2, F.Seg, a, b) 17 18 def alias_of(+a: M.Acct) -> String: 19 match a: 20 case M.Acct{s, e, al, t, ac, d, w, g, n}: 21 al 22 23 def names_pick(+bare: Bool, +a: M.Acct) -> List<&2, F.Seg>: 24 match bare: 25 case True{}: 26 [plain(M.acct_email(a))] 27 case False{}: 28 [F.sg(alias_of(a), F.RAccentB{}), plain(" (" ++ M.acct_email(a) ++ ")")] 29 30 # "alias (email)" with the alias in the accent colour, or just the email. 31 def names(+a: M.Acct) -> List<&2, F.Seg>: 32 names_pick(String.is_empty(alias_of(a)), a) 33 34 def tag_of(+a: M.Acct) -> String: 35 match a: 36 case M.Acct{s, e, al, t, ac, d, w, g, n}: 37 t 38 39 def flag_if(+c: Bool, +s: F.Seg) -> List<&2, F.Seg>: 40 match c: 41 case True{}: 42 [s] 43 case False{}: 44 [] 45 46 # The measurement age note, shown once a reading is at least 3 minutes old. 47 def age_note(+g: Maybe<&2, U32>) -> List<&2, F.Seg>: 48 match g: 49 case None{}: 50 [] 51 case Some{s}: 52 flag_if(U32.is_ge(s, 180), muted(" · " ++ F.duration(s) ++ " ago")) 53 54 def is_stale(+g: Maybe<&2, U32>) -> Bool: 55 match g: 56 case None{}: 57 False{} 58 case Some{s}: 59 U32.is_gt(s, 300) 60 61 def tail_flags(+a: M.Acct) -> List<&2, F.Seg>: 62 cat(flag_if(M.acct_active(a), F.sg(" ● active", F.RAccentB{})), cat(flag_if(M.acct_disabled(a), muted(" (disabled)")), age_note(M.acct_age(a)))) 63 64 def head_line(+a: M.Acct) -> List<&2, F.Seg>: 65 cat([F.sg(F.pad_left(M.acct_slot(a), 2n) ++ " ", F.RBold{})], cat(names(a), cat([muted(" [" ++ tag_of(a) ++ "]")], tail_flags(a)))) 66 67 # ---- the bars -------------------------------------------------------------- 68 69 def label_width(+ws: List<&2, M.Win>, +acc: Nat) -> Nat: 70 match ws: 71 case Nil{}: 72 acc 73 case Con{w, t}: 74 label_width(t, Nat.max(acc, String.length(M.win_label(w)))) 75 76 def bar_width(+width: U32, +lw: Nat) -> Nat: 77 Nat.max(12n, Nat.min(30n, Nat.sub(Nat.sub(U32.to_nat(width), 42n), lw))) 78 79 # "resets 2h 13m" (or "resets now"); empty when no reset is known. 80 def reset_short(+r: Maybe<&2, U32>, +now: U32) -> String: 81 match r: 82 case None{}: 83 "" 84 case Some{t}: 85 F.sel.s(U32.is_gt(t, now), "resets " ++ F.duration((t - now : U32)), "resets now") 86 87 # The same with the local clock time of the reset: "resets 2h 13m · 20:39". 88 def reset_full(+r: Maybe<&2, U32>, +now: U32, +off: U32) -> String: 89 match r: 90 case None{}: 91 "" 92 case Some{t}: 93 F.sel.s(U32.is_gt(t, now), reset_short(r, now) ++ " · " ++ UNorm.clock.text(t, now, off), reset_short(r, now)) 94 95 def is_model(+w: M.Win) -> Bool: 96 Bool.not(String.eq(M.win_label(w), "5h")) && Bool.not(String.eq(M.win_label(w), "7d")) 97 98 def is_full(+p: Maybe<&2, U32>) -> Bool: 99 match p: 100 case None{}: 101 False{} 102 case Some{c}: 103 U32.is_ge(c, 10000) 104 105 def maxed(+w: M.Win) -> Bool: 106 is_model(w) && is_full(M.win_pct(w)) 107 108 def mark(+w: M.Win, +s: String) -> String: 109 F.sel.s(maxed(w), F.sel.s(String.is_empty(s), "(!)", s ++ " (!)"), s) 110 111 def pick_suffix(+full: String, +short: String, +room: Nat) -> String: 112 F.sel.s(Nat.is_le(String.length(full), room), full, short) 113 114 # The suffix of a row: the clock form where it fits, a (!) for a maxed model. 115 def suffix(+w: M.Win, +now: U32, +off: U32, +room: Nat) -> String: 116 mark(w, pick_suffix(reset_full(M.win_reset(w), now, off), reset_short(M.win_reset(w), now), room)) 117 118 def pct_seg(+p: Maybe<&2, U32>, +stale: Bool) -> F.Seg: 119 match p: 120 case None{}: 121 muted(" usage unknown") 122 case Some{c}: 123 F.Seg{" " ++ F.pct_text(c), F.severity(c), stale} 124 125 def pad2(+s: String) -> String: 126 F.sel.s(String.is_empty(s), "", " " ++ s) 127 128 def view_width(+v: M.View) -> U32: 129 match v: 130 case M.View{w, c}: 131 w 132 133 # Columns left for the suffix after indent, label, bar, percent and gap. 134 def room(+v: M.View, +lw: Nat, +bw: Nat) -> Nat: 135 Nat.sub(U32.to_nat(view_width(v)), Nat.add(Nat.add(4n, lw), Nat.add(Nat.add(1n, bw), 7n))) 136 137 # The indent, the label and the bar of a row. 138 def row_head(+w: M.Win, +lw: Nat, +bw: Nat, +thr: Maybe<&2, U32>, +stale: Bool) -> List<&2, F.Seg>: 139 cat([plain(" "), muted(F.pad_right(M.win_label(w), lw) ++ " ")], F.bar(M.win_pct(w), bw, thr, stale)) 140 141 # One row: indent, label, bar, percent, suffix. 142 def row(+w: M.Win, +lw: Nat, +bw: Nat, +v: M.View, +thr: Maybe<&2, U32>, +stale: Bool, +now: U32, +off: U32) -> List<&2, F.Seg>: 143 cat(row_head(w, lw, bw, thr, stale), [pct_seg(M.win_pct(w), stale), muted(pad2(suffix(w, now, off, room(v, lw, bw))))]) 144 145 def rows(+ws: List<&2, M.Win>, +lw: Nat, +bw: Nat, +v: M.View, +thr: Maybe<&2, U32>, +stale: Bool, +now: U32, +off: U32) -> List<&2, List<&2, F.Seg>>: 146 match ws: 147 case Nil{}: 148 Nil{} 149 case Con{w, t}: 150 Con{row(w, lw, bw, v, thr, stale, now, off), rows(t, lw, bw, v, thr, stale, now, off)} 151 152 def unavailable(+a: M.Acct) -> List<&2, F.Seg>: 153 [plain(" "), muted("usage unavailable" ++ F.sel.s(String.is_empty(M.acct_note(a)), "", " · " ++ M.acct_note(a)))] 154 155 def card_rows(+a: M.Acct, +ws: List<&2, M.Win>, +v: M.View, +thr: Maybe<&2, U32>, +now: U32, +off: U32) -> List<&2, List<&2, F.Seg>>: 156 match ws: 157 case Nil{}: 158 [unavailable(a)] 159 case _: 160 rows(ws, label_width(ws, 0n), bar_width(view_width(v), label_width(ws, 0n)), v, thr, is_stale(M.acct_age(a)), now, off) 161 162 # The full card of the active account. 163 def card(+a: M.Acct, +v: M.View, +thr: Maybe<&2, U32>, +now: U32, +off: U32) -> List<&2, List<&2, F.Seg>>: 164 Con{head_line(a), card_rows(a, M.acct_wins(a), v, thr, now, off)} 165 166 # ---- the one-line form ---------------------------------------------------------- 167 168 def reset_in(+p: Maybe<&2, U32>, +r: Maybe<&2, U32>, +now: U32) -> List<&2, F.Seg>: 169 flag_if(is_full(p) && Bool.not(String.is_empty(reset_short(r, now))), muted(" (" ++ reset_short(r, now) ++ ")")) 170 171 def pct_part(+w: M.Win, +c: U32, +now: U32) -> List<&2, F.Seg>: 172 cat([muted(M.win_label(w) ++ " "), F.sg(U32.show(F.round_even(c)) ++ "%", F.severity(c))], reset_in(M.win_pct(w), M.win_reset(w), now)) 173 174 def part_pick(+model: Bool, +full: Bool, +w: M.Win, +p: Maybe<&2, U32>, +now: U32) -> List<&2, F.Seg>: 175 match model p: 176 case True{} _: 177 flag_if(full, F.sg(M.win_label(w) ++ " (!)", F.RCrit{})) 178 case False{} None{}: 179 [] 180 case False{} Some{c}: 181 pct_part(w, c, now) 182 183 def mini_part(+w: M.Win, +now: U32) -> List<&2, F.Seg>: 184 part_pick(is_model(w), maxed(w), w, M.win_pct(w), now) 185 186 def push_part(+p: List<&2, F.Seg>, +rest: List<&2, List<&2, F.Seg>>) -> List<&2, List<&2, F.Seg>>: 187 match p: 188 case Nil{}: 189 rest 190 case _: 191 Con{p, rest} 192 193 def mini_parts(+ws: List<&2, M.Win>, +now: U32) -> List<&2, List<&2, F.Seg>>: 194 match ws: 195 case Nil{}: 196 Nil{} 197 case Con{w, t}: 198 push_part(mini_part(w, now), mini_parts(t, now)) 199 200 def joined_rest(+ps: List<&2, List<&2, F.Seg>>) -> List<&2, F.Seg>: 201 match ps: 202 case Nil{}: 203 [] 204 case Con{p, t}: 205 cat([F.sg(" · ", F.RTrack{})], cat(p, joined_rest(t))) 206 207 def joined(+ps: List<&2, List<&2, F.Seg>>) -> List<&2, F.Seg>: 208 match ps: 209 case Nil{}: 210 [] 211 case Con{p, t}: 212 cat(p, joined_rest(t)) 213 214 def mini_body(+a: M.Acct, +ps: List<&2, List<&2, F.Seg>>) -> List<&2, F.Seg>: 215 match ps: 216 case Nil{}: 217 [muted(F.sel.s(String.is_empty(M.acct_note(a)), "usage unknown", M.acct_note(a)))] 218 case _: 219 joined(ps) 220 221 def mini_head(+a: M.Acct) -> List<&2, F.Seg>: 222 cat([F.sg(F.pad_left(M.acct_slot(a), 2n) ++ " ", F.RBold{})], cat(names(a), cat([muted(" [" ++ tag_of(a) ++ "]")], flag_if(M.acct_disabled(a), muted(" (disabled)"))))) 223 224 # An inactive account on one line: "2 work@acme.dev [personal] 5h 92% · 7d 63%". 225 def mini(+a: M.Acct, +now: U32) -> List<&2, F.Seg>: 226 cat(mini_head(a), cat([plain(" ")], mini_body(a, mini_parts(M.acct_wins(a), now))))