render.bend
cswap-bend-cli@0.1.0.0 · 244 lines · 45 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/clock.bend as Clock
4import cswap-bend-cli@0.1.0.0/poll.bend as Poll
5import cswap-bend-cli@0.1.0.0/view.bend as View
6
7
8
9
10def sel_n(c: Bool, +x: Nat, +y: Nat) -> Nat:
11 match c:
12 case True{}:
13 x
14 case False{}:
15 y
16
17def sel_l(c: Bool, x: List<&2, String>, y: List<&2, String>) -> List<&2, String>:
18 match c:
19 case True{}:
20 x
21 case False{}:
22 y
23
24def sp(+n: Nat) -> String:
25 String.repeat(" ", n)
26
27def padl(+s: String, +w: Nat) -> String:
28 sp(Nat.sub(w, String.length(s))) ++ s
29
30def padr(+s: String, +w: Nat) -> String:
31 s ++ sp(Nat.sub(w, String.length(s)))
32
33
34def whole.pick(+w0: U32, +r: U32) -> U32:
35 Clock.sel(U32.is_gt(r, 5), (w0 + 1 : U32), Clock.sel(U32.is_eq(r, 5), (w0 + (w0 % 2) : U32), w0))
36
37def whole(+t: U32) -> U32:
38 whole.pick((t / 10 : U32), (t % 10 : U32))
39
40def pct_text(+t: U32) -> String:
41 padl(U32.show(whole(t)) ++ "%", 4n)
42
43def money.put(comma: Bool, +c: Char, acc: String) -> String:
44 match comma:
45 case True{}:
46 SCon{c, SCon{',', acc}}
47 case False{}:
48 SCon{c, acc}
49
50def money.group(r: String, +n: Nat, acc: String) -> String:
51 match r:
52 case SNil{}:
53 acc
54 case SCon{+c, +t}:
55 money.group(t, 1n+n, money.put(Nat.is_gt(n, 0n) && Nat.is_eq(Nat.mod(n, 3n), 0n), c, acc))
56
57def money.commas(+d: String) -> String:
58 money.group(String.reverse(d), 0n, "")
59
60def money.fmt(+c: U32) -> String:
61 money.commas(U32.show((c / 100 : U32))) ++ "." ++ Clock.pad2((c % 100 : U32))
62
63def money.cents(+i: String, +f: String) -> String:
64 money.fmt(Maybe.default(&2, U32, Maybe.bind(&2, U32, U32, U32.read(i), x => Maybe.bind(&2, U32, U32, Clock.num(f ++ "000", 0n, 2n), y => Maybe.bind(&2, U32, U32, Clock.num(f ++ "000", 2n, 1n), z => Some{(x * 100 + y + Clock.sel(U32.is_ge(z, 5), 1, 0) : U32)}))), 0))
65
66def money.parts(ps: List<&2, String>) -> String:
67 match ps:
68 case Con{i, Con{f, Nil{}}}:
69 money.cents(i, f)
70 case Con{i, Nil{}}:
71 money.cents(i, "")
72 case _:
73 "0.00"
74
75
76def money(j: Json.Json) -> String:
77 match j:
78 case Json.JNum{t}:
79 money.parts(String.split(t, '.'))
80 case _:
81 "0.00"
82
83type Line is Data:
84 Line{label: String, body: String}
85
86def tenths_of(+w: Json.Json) -> U32:
87 Maybe.default(&2, U32, Poll.tenths.json(View.jv("pct", w)), 0)
88
89def marker.ahead(m: Maybe<&2, View.Pace>) -> String:
90 match m:
91 case Some{View.Pace{e, a, eta, last}}:
92 Clock.sel.s(a, " (ahead of pace)", "")
93 case None{}:
94 ""
95
96def body.cell(+pct: U32, c: Maybe<&2, View.Cell>, +marker: String) -> String:
97 match c:
98 case Some{View.Cell{cd, ck}}:
99 pct_text(pct) ++ " resets " ++ padr(ck, 12n) ++ " in " ++ cd ++ marker
100 case None{}:
101 pct_text(pct) ++ marker
102
103def window_line(+label: String, +w: Json.Json, +marker: String, +now: U32, +off: U32) -> Line:
104 Line{label, body.cell(tenths_of(w), View.fresh(w, now, off), marker)}
105
106def spend.body(c: Maybe<&2, View.Cell>, +pct: U32, +used: String, +limit: String) -> String:
107 match c:
108 case Some{View.Cell{cd, ck}}:
109 pct_text(pct) ++ " resets " ++ padr(ck, 12n) ++ " $" ++ used ++ " / $" ++ limit
110 case None{}:
111 pct_text(pct) ++ " $" ++ used ++ " / $" ++ limit
112
113def spend_line(+s: Json.Json, +now: U32, +off: U32) -> Line:
114 Line{"$$", spend.body(View.fresh(s, now, off), tenths_of(s), money(View.jv("used", s)), money(View.jv("limit", s)))}
115
116def cons_m(m: Maybe<&2, Line>, rest: List<&2, Line>) -> List<&2, Line>:
117 match m:
118 case Some{l}:
119 l <> rest
120 case None{}:
121 rest
122
123def win_m(+label: String, m: Maybe<&2, Json.Json>, weekly: Bool, +fetched: Maybe<&2, U32>, +now: U32, +off: U32) -> Maybe<&2, Line>:
124 match m:
125 case None{}:
126 None{}
127 case Some{+w}:
128 Some{window_line(label, w, Clock.sel.s(weekly, marker.ahead(View.pace_of(w, fetched)), ""), now, off)}
129
130def spend_m(m: Maybe<&2, Json.Json>, +now: U32, +off: U32) -> Maybe<&2, Line>:
131 match m:
132 case Some{s}:
133 Some{spend_line(s, now, off)}
134 case None{}:
135 None{}
136
137def scoped_marker(+w: Json.Json, +fetched: Maybe<&2, U32>) -> String:
138 Clock.sel.s(U32.is_ge(tenths_of(w), 1000), " (!)", marker.ahead(View.pace_of(w, fetched)))
139
140def scoped_lines(ws: List<&2, Json.Json>, +fetched: Maybe<&2, U32>, +now: U32, +off: U32) -> List<&2, Line>:
141 match ws:
142 case Nil{}:
143 []
144 case Con{+w, t}:
145 window_line(Maybe.default(&2, String, Json.as_string(View.jv("name", w)), "?"), w, scoped_marker(w, fetched), now, off) <> scoped_lines(t, fetched, now, off)
146
147def usage_rows(+u: Json.Json, +fetched: Maybe<&2, U32>, +now: U32, +off: U32) -> List<&2, Line>:
148 cons_m(spend_m(Json.get("spend", u), now, off),
149 cons_m(win_m("5h", Json.get("five_hour", u), False{}, fetched, now, off),
150 cons_m(win_m("7d", Json.get("seven_day", u), True{}, fetched, now, off),
151 scoped_lines(Json.to_list(View.jv("scoped", u)), fetched, now, off))))
152
153def width(ls: List<&2, Line>, +w: Nat) -> Nat:
154 match ls:
155 case Nil{}:
156 1n+w
157 case Con{Line{+l, b}, t}:
158 width(t, sel_n(Nat.is_gt(String.length(l), w), String.length(l), w))
159
160def fmt_lines(ls: List<&2, Line>, +w: Nat) -> List<&2, String>:
161 match ls:
162 case Nil{}:
163 []
164 case Con{Line{l, b}, t}:
165 (padr(l ++ ":", w) ++ " " ++ b) <> fmt_lines(t, w)
166
167def age_text(+secs: U32) -> String:
168 Clock.sel.s(U32.is_lt(secs, 60), "just now", Clock.sel.s(U32.is_lt(secs, 3600), U32.show((secs / 60 : U32)) ++ "m ago",
169 Clock.sel.s(U32.is_lt(secs, 86400), U32.show((secs / 3600 : U32)) ++ "h ago", U32.show((secs / 86400 : U32)) ++ "d ago")))
170
171def tree(ls: List<&2, String>, +last: Nat, +i: Nat) -> List<&2, String>:
172 match ls:
173 case Nil{}:
174 []
175 case Con{h, t}:
176 (Clock.sel.s(Nat.is_eq(i, last), "\u{2514} ", "\u{251C} ") ++ h) <> tree(t, last, 1n+i)
177
178def last_idx(ls: List<&2, String>) -> Nat:
179 Nat.sub(List.length(&2, String, ls), 1n)
180
181def append_last(ls: List<&2, String>, +note: String) -> List<&2, String>:
182 match ls:
183 case Nil{}:
184 []
185 case Con{h, Nil{}}:
186 [h ++ note]
187 case Con{h, Con{h2, t2}}:
188 h <> append_last(h2 <> t2, note)
189
190def with_age(ls: List<&2, String>, +note: String) -> List<&2, String>:
191 append_last(ls, note)
192
193def note.pick(old: Bool, +a: U32, ls: List<&2, String>) -> List<&2, String>:
194 match old:
195 case True{}:
196 with_age(ls, " \u{B7} " ++ age_text(a))
197 case False{}:
198 ls
199
200def age_note(+age: Maybe<&2, U32>, ls: List<&2, String>) -> List<&2, String>:
201 match age:
202 case Some{+a}:
203 note.pick(U32.is_gt(a, 180), a, ls)
204 case None{}:
205 ls
206
207def measured2(+ls: List<&2, String>, +age: Maybe<&2, U32>) -> List<&2, String>:
208 tree(age_note(age, ls), last_idx(ls), 0n)
209
210def measured(+u: Json.Json, +fetched: Maybe<&2, U32>, +age: Maybe<&2, U32>, +now: U32, +off: U32) -> List<&2, String>:
211 measured2(fmt_lines(usage_rows(u, fetched, now, off), width(usage_rows(u, fetched, now, off), 0n)), age)
212
213def error_note(+k: String) -> String:
214 Clock.sel.s(String.eq(k, "consume-busy"), "another cswap surface holds the slot - retries next pass",
215 Clock.sel.s(String.eq(k, "invalid_client"), "cswap's OAuth client was rejected - systemic, not this account",
216 Clock.sel.s(String.eq(k, "store-unmirrored"), "CLAUDE_SECURESTORAGE_CONFIG_DIR set - unset it or run from a normal shell", k)))
217
218def unavailable(le: Maybe<&2, String>) -> List<&2, String>:
219 match le:
220 case Some{k}:
221 ["usage unavailable (" ++ error_note(k) ++ ")"]
222 case None{}:
223 ["usage unavailable"]
224
225def sentinel_note(+s: String) -> String:
226 Clock.sel.s(String.eq(s, "token_expired"), "token expired - refresh deferred this pass; retries automatically",
227 Clock.sel.s(String.eq(s, "foreign_credential"), "live credential belongs to another account - a switch repairs it",
228 Clock.sel.s(String.eq(s, "api_key"), "API key (no quota)",
229 Clock.sel.s(String.eq(s, "keychain_unavailable"), "keychain unavailable - locked or in use; try again",
230 Clock.sel.s(String.eq(s, "relogin_required"), "re-login needed - refresh token dead; log in with Claude Code, then run: cswap add",
231 Clock.sel.s(String.eq(s, "no_credentials"), "no credentials", s))))))
232
233def last_seen2(b: Maybe<&2, U32>, +age: Maybe<&2, U32>) -> List<&2, String>:
234 match b age:
235 case Some{p} Some{a}:
236 ["\u{2514} last seen " ++ U32.show(whole(p)) ++ "% used \u{B7} " ++ age_text(a)]
237 case _ _:
238 []
239
240def last_seen(+lg: Json.Json, +fetched: Maybe<&2, U32>, +age: Maybe<&2, U32>, +off: U32, models: List<&2, String>) -> List<&2, String>:
241 last_seen2(Poll.binding(lg, []), age)
242
243def sentinel_lines(+s: String, +lg: Json.Json, +fetched: Maybe<&2, U32>, +age: Maybe<&2, U32>, +off: U32) -> List<&2, String>:
244 sentinel_note(s) <> sel_l(String.eq(s, "api_key"), [], last_seen(lg, fetched, age, off, []))