tui/view.bend
cswap-bend-cli@0.1.0.0 · 300 lines · 51 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/tui/card.bend as C
5
6
7
8
9
10
11
12
13
14type Best is Data:
15 Best{slot: String, email: String, free: U32}
16
17def frac2(+r: U32) -> String:
18 F.sel.s(U32.is_eq((r % 10 : U32), 0), U32.show((r / 10 : U32)), F.sel.s(U32.is_lt(r, 10), "0" ++ U32.show(r), U32.show(r)))
19
20
21
22
23def pct_label(+c: U32) -> String:
24 F.sel.s(U32.is_eq((c % 100 : U32), 0), U32.show((c / 100 : U32)), U32.show((c / 100 : U32)) ++ "." ++ frac2((c % 100 : U32)))
25
26def version_of(+h: M.Hdr) -> String:
27 match h:
28 case M.Hdr{v, l, p, t, i, n}:
29 v
30
31def title(+h: M.Hdr) -> List<&2, F.Seg>:
32 [F.sg("cswap tui", F.RBold{}), C.muted(" " ++ version_of(h))]
33
34def live_of(+h: M.Hdr) -> Bool:
35 match h:
36 case M.Hdr{v, l, p, t, i, n}:
37 l
38
39def state_seg(+live: Bool) -> F.Seg:
40 match live:
41 case True{}:
42 F.sg("● auto-switch LIVE", F.ROk{})
43 case False{}:
44 F.sg("○ auto-switch STOPPED", F.RWarn{})
45
46def opt_text(+m: Maybe<&2, String>) -> List<&2, F.Seg>:
47 match m:
48 case None{}:
49 []
50 case Some{s}:
51 [C.muted(" · " ++ s)]
52
53def thr_some(+m: Maybe<&2, U32>) -> Maybe<&2, String>:
54 match m:
55 case None{}:
56 None{}
57 case Some{c}:
58 Some{"threshold " ++ pct_label(c) ++ "%"}
59
60def thr_text(+h: M.Hdr) -> Maybe<&2, String>:
61 match h:
62 case M.Hdr{v, l, p, t, i, n}:
63 thr_some(t)
64
65def every_some(+m: Maybe<&2, U32>) -> Maybe<&2, String>:
66 match m:
67 case None{}:
68 None{}
69 case Some{c}:
70 Some{"every " ++ U32.show(c) ++ "s"}
71
72def every_text(+h: M.Hdr) -> Maybe<&2, String>:
73 match h:
74 case M.Hdr{v, l, p, t, i, n}:
75 every_some(i)
76
77def live_pid(+live: Bool, +p: Maybe<&2, U32>) -> Maybe<&2, String>:
78 match live p:
79 case True{} Some{n}:
80 Some{"pid " ++ U32.show(n)}
81 case _ _:
82 None{}
83
84def pid_text(+h: M.Hdr) -> Maybe<&2, String>:
85 match h:
86 case M.Hdr{v, l, p, t, i, n}:
87 live_pid(l, p)
88
89def next_poll(+live: Bool, +n: Maybe<&2, U32>, +now: U32) -> Maybe<&2, String>:
90 match live n:
91 case True{} Some{t}:
92 Some{F.sel.s(U32.is_gt(t, now), "next poll in " ++ F.duration((t - now : U32)), "polling now")}
93 case _ _:
94 None{}
95
96def poll_text(+h: M.Hdr, +now: U32) -> Maybe<&2, String>:
97 match h:
98 case M.Hdr{v, l, p, t, i, n}:
99 next_poll(l, n, now)
100
101def state_line(+h: M.Hdr, +now: U32) -> List<&2, F.Seg>:
102 List.append(&2, F.Seg, [state_seg(live_of(h))], List.append(&2, F.Seg, opt_text(thr_text(h)), List.append(&2, F.Seg, opt_text(every_text(h)), List.append(&2, F.Seg, opt_text(poll_text(h, now)), opt_text(pid_text(h))))))
103
104
105
106def threshold_of(+h: M.Hdr) -> Maybe<&2, U32>:
107 match h:
108 case M.Hdr{v, l, p, t, i, n}:
109 t
110
111def is_multi(+b: List<&2, List<&2, F.Seg>>) -> Bool:
112 Nat.is_gt(List.length(&2, List<&2, F.Seg>, b), 1n)
113
114def lines_cat(+a: List<&2, List<&2, F.Seg>>, +b: List<&2, List<&2, F.Seg>>) -> List<&2, List<&2, F.Seg>>:
115 List.append(&2, List<&2, F.Seg>, a, b)
116
117def gap_if(+b: Bool) -> List<&2, List<&2, F.Seg>>:
118 match b:
119 case True{}:
120 [[]]
121 case False{}:
122 []
123
124def empty_or(+as: List<&2, M.Acct>, +bs: List<&2, List<&2, F.Seg>>) -> List<&2, List<&2, F.Seg>>:
125 match as:
126 case Nil{}:
127 [[C.muted("No managed accounts yet. Add one with: cswap add")]]
128 case _:
129 bs
130
131
132
133def block_pick(+active: Bool, +a: M.Acct, +v: M.View, +h: M.Hdr, +now: U32, +off: U32) -> List<&2, List<&2, F.Seg>>:
134 match active:
135 case True{}:
136 C.card(a, v, threshold_of(h), now, off)
137 case False{}:
138 [C.mini(a, now)]
139
140def block(+a: M.Acct, +v: M.View, +h: M.Hdr, +now: U32, +off: U32) -> List<&2, List<&2, F.Seg>>:
141 block_pick(M.acct_active(a), a, v, h, now, off)
142
143def block_join(+b: List<&2, List<&2, F.Seg>>, +rest: List<&2, List<&2, F.Seg>>, +prev: Bool, +first: Bool) -> List<&2, List<&2, F.Seg>>:
144 lines_cat(gap_if(Bool.not(first) && (prev || is_multi(b))), lines_cat(b, rest))
145
146
147
148def blocks(+as: List<&2, M.Acct>, +v: M.View, +h: M.Hdr, +now: U32, +off: U32, +prev: Bool, +first: Bool) -> List<&2, List<&2, F.Seg>>:
149 match as:
150 case Nil{}:
151 Nil{}
152 case Con{a, t}:
153 block_join(block(a, v, h, now, off), blocks(t, v, h, now, off, is_multi(block(a, v, h, now, off)), False{}), prev, first)
154
155def accounts_lines(+s: M.Snap, +v: M.View) -> List<&2, List<&2, F.Seg>>:
156 match s:
157 case M.Snap{now, off, h, as, lg}:
158 empty_or(as, blocks(as, v, h, now, off, False{}, True{}))
159
160def max_opt(+a: Maybe<&2, U32>, +b: Maybe<&2, U32>) -> Maybe<&2, U32>:
161 match a b:
162 case None{} _:
163 b
164 case Some{x} None{}:
165 a
166 case Some{x} Some{y}:
167 Some{U32.max(x, y)}
168
169
170def busiest(+ws: List<&2, M.Win>, +acc: Maybe<&2, U32>) -> Maybe<&2, U32>:
171 match ws:
172 case Nil{}:
173 acc
174 case Con{w, t}:
175 busiest(t, max_opt(acc, M.win_pct(w)))
176
177def free_some(+m: Maybe<&2, U32>) -> Maybe<&2, U32>:
178 match m:
179 case None{}:
180 None{}
181 case Some{c}:
182 Some{(10000 - U32.min(c, 10000) : U32)}
183
184def free_of(+a: M.Acct) -> Maybe<&2, U32>:
185 free_some(busiest(M.acct_wins(a), None{}))
186
187def eligible(+a: M.Acct) -> Bool:
188 Bool.not(M.acct_active(a)) && Bool.not(M.acct_disabled(a))
189
190def best_free(+b: Best) -> U32:
191 match b:
192 case Best{s, e, f}:
193 f
194
195def better(+cur: Maybe<&2, Best>, +f: U32) -> Bool:
196 match cur:
197 case None{}:
198 True{}
199 case Some{b}:
200 U32.is_gt(f, best_free(b))
201
202def take_if(+yes: Bool, +a: M.Acct, +x: U32, +cur: Maybe<&2, Best>) -> Maybe<&2, Best>:
203 match yes:
204 case True{}:
205 Some{Best{M.acct_slot(a), M.acct_email(a), x}}
206 case False{}:
207 cur
208
209def take(+a: M.Acct, +f: Maybe<&2, U32>, +cur: Maybe<&2, Best>) -> Maybe<&2, Best>:
210 match f:
211 case None{}:
212 cur
213 case Some{x}:
214 take_if(better(cur, x), a, x, cur)
215
216def next_pick(+ok: Bool, +a: M.Acct, +cur: Maybe<&2, Best>) -> Maybe<&2, Best>:
217 match ok:
218 case True{}:
219 take(a, free_of(a), cur)
220 case False{}:
221 cur
222
223def next_best(+as: List<&2, M.Acct>, +cur: Maybe<&2, Best>) -> Maybe<&2, Best>:
224 match as:
225 case Nil{}:
226 cur
227 case Con{a, t}:
228 next_best(t, next_pick(eligible(a), a, cur))
229
230def best_name(+b: Best) -> String:
231 match b:
232 case Best{s, e, f}:
233 s ++ " " ++ e
234
235
236
237def best_line(+m: Maybe<&2, Best>) -> List<&2, F.Seg>:
238 match m:
239 case None{}:
240 [C.muted("next best none (no other account has a reading)")]
241 case Some{b}:
242 [C.muted("next best "), F.sg(best_name(b), F.RAccent{}), C.muted(" · " ++ pct_label(best_free(b)) ++ "% free")]
243
244def log_each(+ls: List<&2, String>) -> List<&2, List<&2, F.Seg>>:
245 match ls:
246 case Nil{}:
247 Nil{}
248 case Con{l, t}:
249 Con{[C.muted(" " ++ l)], log_each(t)}
250
251def log_lines(+ls: List<&2, String>) -> List<&2, List<&2, F.Seg>>:
252 match ls:
253 case Nil{}:
254 [[C.muted(" no daemon log yet")]]
255 case _:
256 log_each(ls)
257
258def log_of(+s: M.Snap) -> List<&2, String>:
259 match s:
260 case M.Snap{now, off, h, as, lg}:
261 lg
262
263def hdr_of(+s: M.Snap) -> M.Hdr:
264 match s:
265 case M.Snap{now, off, h, as, lg}:
266 h
267
268def now_of(+s: M.Snap) -> U32:
269 match s:
270 case M.Snap{now, off, h, as, lg}:
271 now
272
273def accts_of(+s: M.Snap) -> List<&2, M.Acct>:
274 match s:
275 case M.Snap{now, off, h, as, lg}:
276 as
277
278
279
280def head_lines(+s: M.Snap) -> List<&2, List<&2, F.Seg>>:
281 [title(hdr_of(s)), state_line(hdr_of(s), now_of(s)), []]
282
283def foot_lines(+s: M.Snap) -> List<&2, List<&2, F.Seg>>:
284 lines_cat([[], best_line(next_best(accts_of(s), None{})), [], [F.sg("daemon log", F.RBold{})]], log_lines(log_of(s)))
285
286def fit_all(+ls: List<&2, List<&2, F.Seg>>, +w: Nat) -> List<&2, List<&2, F.Seg>>:
287 match ls:
288 case Nil{}:
289 Nil{}
290 case Con{l, t}:
291 Con{F.fit(l, w), fit_all(t, w)}
292
293def viewcolor(+v: M.View) -> Bool:
294 match v:
295 case M.View{w, c}:
296 c
297
298
299def frame(+s: M.Snap, +v: M.View) -> String:
300 F.render(fit_all(lines_cat(head_lines(s), lines_cat(accounts_lines(s, v), foot_lines(s))), U32.to_nat(C.view_width(v))), viewcolor(v))