tui/card.bend

cswap-bend-cli@0.1.0.0 · 226 lines · 43 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/tui/fmt.bend as F
3import cswap-bend-cli@0.1.0.0/tui/model.bend as M
4import cswap-bend-cli@0.1.0.0/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
9def plain(+t: String) -> F.Seg:
10  F.sg(t, F.RPlain{})
11
12def muted(+t: String) -> F.Seg:
13  F.sg(t, F.RMuted{})
14
15def cat(+a: List<&2, F.Seg>, +b: List<&2, F.Seg>) -> List<&2, F.Seg>:
16  List.append(&2, F.Seg, a, b)
17
18def 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
23def 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.
31def names(+a: M.Acct) -> List<&2, F.Seg>:
32  names_pick(String.is_empty(alias_of(a)), a)
33
34def 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
39def 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.
47def 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
54def 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
61def 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
64def 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
69def 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
76def 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.
80def 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".
88def 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
95def 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
98def 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
105def maxed(+w: M.Win) -> Bool:
106  is_model(w) && is_full(M.win_pct(w))
107
108def mark(+w: M.Win, +s: String) -> String:
109  F.sel.s(maxed(w), F.sel.s(String.is_empty(s), "(!)", s ++ "  (!)"), s)
110
111def 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.
115def 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
118def 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
125def pad2(+s: String) -> String:
126  F.sel.s(String.is_empty(s), "", "  " ++ s)
127
128def 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.
134def 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.
138def 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.
142def 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
145def 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
152def 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
155def 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.
163def 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
168def 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
171def 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
174def 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
183def 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
186def 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
193def 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
200def 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
207def 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
214def 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
221def 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%".
225def 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))))