tui/run.bend
cswap-bend-cli@0.1.0.0 · 309 lines · 54 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/fs.bend as Fs
3import cswap-bend-cli@0.1.0.0/clock.bend as Clock
4import cswap-bend-cli@0.1.0.0/tui/fmt.bend as F
5import cswap-bend-cli@0.1.0.0/tui/model.bend as M
6import cswap-bend-cli@0.1.0.0/tui/view.bend as V
7import cswap-bend-cli@0.1.0.0/tui/read.bend as R
8import cswap-bend-cli@0.1.0.0/tui/num.bend as N
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24type Opts is Data:
25 Opts{once: Bool, interval: U32, color: Bool, bad: String}
26
27def usage_text() -> String:
28 "usage: cswap tui [--once] [--interval SECONDS] [--no-color]"
29
30
31
32def set_interval(+o: Opts, +n: U32) -> Opts:
33 match o:
34 case Opts{once, i, c, b}:
35 Opts{once, n, c, b}
36
37def with_bad(+o: Opts, +msg: String) -> Opts:
38 match o:
39 case Opts{once, i, c, b}:
40 Opts{once, i, c, msg}
41
42def with_interval(+o: Opts, +v: Maybe<&2, U32>) -> Opts:
43 match v:
44 case None{}:
45 with_bad(o, "--interval needs a whole number of seconds")
46 case Some{n}:
47 set_interval(o, n)
48
49def with_once(+o: Opts) -> Opts:
50 match o:
51 case Opts{once, i, c, b}:
52 Opts{True{}, i, c, b}
53
54def with_plain(+o: Opts) -> Opts:
55 match o:
56 case Opts{once, i, c, b}:
57 Opts{once, i, False{}, b}
58
59def keep(+ok: Bool, +m: Maybe<&2, U32>) -> Maybe<&2, U32>:
60 match ok:
61 case True{}:
62 m
63 case False{}:
64 None{}
65
66def positive(+m: Maybe<&2, U32>) -> Maybe<&2, U32>:
67 match m:
68 case Some{n}:
69 keep(U32.is_gt(n, 0), m)
70 case None{}:
71 None{}
72
73def default_opts() -> Opts:
74 Opts{False{}, 2, True{}, ""}
75
76
77
78def env_pick(r: Result<&1, &1, U32 & String, String>, +d: String) -> String:
79 match r:
80 case Fail{e}:
81 d
82 case Done{s}:
83 s
84
85def env_or(+name: String, +d: String) -> IO(String):
86 IO.bind(Result<&1, &1, U32 & String, String>, String, IO.get_env(name), r => IO.pure(String, env_pick(r, d)))
87
88def text_pick(r: Fs.Res(String)) -> String:
89 match r:
90 case Fail{e}:
91 ""
92 case Done{t}:
93 t
94
95def read_text(+path: String) -> IO(String):
96 IO.bind(Fs.Res(String), String, Fs.read_file(path), r => IO.pure(String, text_pick(r)))
97
98def out_of(v: U32 & (String & String)) -> String:
99 (st, oe) = v
100 (o, e) = oe
101 o
102
103def out_pick(r: Result<&1, &1, U32 & String, U32 & (String & String)>) -> String:
104 match r:
105 case Fail{e}:
106 ""
107 case Done{v}:
108 out_of(v)
109
110def code_of(v: U32 & (String & String)) -> U32:
111 (st, oe) = v
112 st
113
114def code_pick(r: Result<&1, &1, U32 & String, U32 & (String & String)>) -> U32:
115 match r:
116 case Fail{e}:
117 1
118 case Done{v}:
119 code_of(v)
120
121
122def tail_text(+path: String, +n: String) -> IO(String):
123 IO.bind(Result<&1, &1, U32 & String, U32 & (String & String)>, String, Process.run("tail", ["-n", n, path], "", 262144, 2000), r => IO.pure(String, out_pick(r)))
124
125def local_off() -> IO(U32):
126 IO.bind(Result<&1, &1, U32 & String, U32 & (String & String)>, U32, Process.run("date", ["+%z"], "", 64, 2000),
127 r => IO.pure(U32, N.read_or(Clock.tz(String.trim(out_pick(r))), 86400)))
128
129
130def alive(+pid: Maybe<&2, U32>) -> IO(Bool):
131 match pid:
132 case None{}:
133 IO.pure(Bool, False{})
134 case Some{p}:
135 IO.bind(Result<&1, &1, U32 & String, U32 & (String & String)>, Bool, Process.run("kill", ["-0", U32.show(p)], "", 64, 2000), r => IO.pure(Bool, U32.is_eq(code_pick(r), 0)))
136
137def now_pick(+pinned: String, +real: U32) -> U32:
138 N.read_or(U32.read(pinned), real)
139
140def clock_now() -> IO(U32):
141 IO.bind(String, U32, env_or("CSWAP_TUI_NOW", ""), p => IO.bind(U32, U32, Clock.now_s(), t => IO.pure(U32, now_pick(p, t))))
142
143
144
145def frame_of(+v: M.View, +version: String, +now: U32, +off: U32, +live: Bool, +seq_t: String, +cache_t: String, +status_t: String, +settings_t: String, +log_t: String) -> String:
146 V.frame(R.snap(version, live, now, off, seq_t, cache_t, status_t, settings_t, log_t), v)
147
148def pid_of(+status_t: String) -> Maybe<&2, U32>:
149 N.uint_at(R.f_pid(), N.parse_text(status_t))
150
151def frame_io(+v: M.View, +version: String, +root: String, +state: String) -> IO(String):
152 do IO<String>:
153 now : U32 <- clock_now()
154 off : U32 <- local_off()
155 seq_t : String <- read_text(root ++ "/sequence.json")
156 cache_t : String <- read_text(root ++ "/cache/usage.json")
157 settings_t : String <- read_text(root ++ "/settings.json")
158 +status_t : String <- read_text(state ++ "/daemon-status.json")
159 log_t : String <- tail_text(state ++ "/daemon.log", "8")
160 live : Bool <- alive(pid_of(status_t))
161 IO.pure(String, frame_of(v, version, now, off, live, seq_t, cache_t, status_t, settings_t, log_t))
162
163
164
165def clear() -> String:
166 "\u{1B}[H\u{1B}[2J"
167
168@unsafe def forever(+v: M.View, +version: String, +root: String, +state: String, +secs: U32) -> IO(Unit):
169 do IO<Unit>:
170 f : String <- frame_io(v, version, root, state)
171 IO.write(clear() ++ f ++ "\n")
172 IO.sleep((secs * 1000 : U32))
173 forever(v, version, root, state, secs)
174
175def width_of(+cols: String) -> U32:
176 N.read_or(positive(U32.read(cols)), 100)
177
178def color_of(+want: Bool, +no_color: String) -> Bool:
179 want && String.is_empty(no_color)
180
181def opts_color(+o: Opts) -> Bool:
182 match o:
183 case Opts{once, i, c, b}:
184 c
185
186def opts_once(+o: Opts) -> Bool:
187 match o:
188 case Opts{once, i, c, b}:
189 once
190
191def opts_secs(+o: Opts) -> U32:
192 match o:
193 case Opts{once, i, c, b}:
194 i
195
196def once_frame(+v: M.View, +version: String, +root: String, +state: String) -> IO(Unit):
197 IO.bind(String, Unit, frame_io(v, version, root, state), f => IO.print(f))
198
199def pick_mode(+once: Bool, +v: M.View, +version: String, +root: String, +state: String, +secs: U32) -> IO(Unit):
200 match once:
201 case True{}:
202 once_frame(v, version, root, state)
203 case False{}:
204 forever(v, version, root, state, secs)
205
206def run_with(+o: Opts, +version: String, +v: M.View, +root: String, +state: String) -> IO(Unit):
207 pick_mode(opts_once(o), v, version, root, state, opts_secs(o))
208
209def run(+o: Opts, +version: String, +cols: String, +no_color: String, +home: String, +state: String) -> IO(Unit):
210 run_with(o, version, M.View{width_of(cols), color_of(opts_color(o), no_color)}, home ++ "/.claude-swap-backup", state)
211
212def opts_bad(+o: Opts) -> String:
213 match o:
214 case Opts{once, i, c, b}:
215 b
216
217def start2(+o: Opts, +version: String, +cols: String, +no_color: String, +home: String, +st: String) -> IO(Unit):
218 run(o, version, cols, no_color, home, st)
219
220def start1(+o: Opts, +version: String, +cols: String, +no_color: String, +home: String) -> IO(Unit):
221 IO.bind(String, Unit, env_or("CSWAP_STATE_DIR", home ++ "/.claude-swap-backup"), st => start2(o, version, cols, no_color, home, st))
222
223def start(+o: Opts, +version: String) -> IO(Unit):
224 do IO<Unit>:
225 home : String <- IO.try(String, IO.get_env("HOME"))
226 cols : String <- env_or("COLUMNS", "")
227 no_color : String <- env_or("NO_COLOR", "")
228 start1(o, version, cols, no_color, home)
229
230def reject(+msg: String) -> IO(Unit):
231 IO.bind(Unit, Unit, IO.print_err("cswap tui: error: " ++ msg ++ "\n" ++ usage_text()), u => IO.exit(2))
232
233def read_opt(+v: Maybe<&2, String>) -> Maybe<&2, U32>:
234 match v:
235 case None{}:
236 None{}
237 case Some{s}:
238 U32.read(s)
239
240def step_pick(+once: Bool, +plain: Bool, +iv: Bool, +o: Opts, +a: String, +v: Maybe<&2, String>) -> Opts:
241 match once plain iv:
242 case True{} _ _:
243 with_once(o)
244 case False{} True{} _:
245 with_plain(o)
246 case False{} False{} True{}:
247 with_interval(o, positive(read_opt(v)))
248 case False{} False{} False{}:
249 with_bad(o, "unknown argument " ++ a)
250
251
252def step(+o: Opts, +a: String, +v: Maybe<&2, String>) -> Opts:
253 step_pick(String.eq(a, "--once"), String.eq(a, "--no-color"), String.eq(a, "--interval"), o, a, v)
254
255def head_opt(+l: List<&2, String>) -> Maybe<&2, String>:
256 match l:
257 case Nil{}:
258 None{}
259 case Con{h, t}:
260 Some{h}
261
262def tail_of(+l: List<&2, String>) -> List<&2, String>:
263 match l:
264 case Nil{}:
265 Nil{}
266 case Con{h, t}:
267 t
268
269def rest_after2(+iv: Bool, +t: List<&2, String>) -> List<&2, String>:
270 match iv:
271 case True{}:
272 tail_of(t)
273 case False{}:
274 t
275
276
277def parse(+fuel: Nat, +args: List<&2, String>, +o: Opts) -> Opts:
278 match fuel args:
279 case _ Nil{}:
280 o
281 case 0n _:
282 o
283 case 1n+p Con{a, t}:
284 parse(p, rest_after2(String.eq(a, "--interval"), t), step(o, a, head_opt(t)))
285
286def dispatch_pick(+ok: Bool, +o: Opts, +version: String) -> IO(Unit):
287 match ok:
288 case True{}:
289 start(o, version)
290 case False{}:
291 reject(opts_bad(o))
292
293def dispatch(+o: Opts, +version: String) -> IO(Unit):
294 dispatch_pick(String.is_empty(opts_bad(o)), o, version)
295
296
297def share(args: List<String>) -> List<&2, String>:
298 match args:
299 case Nil{}:
300 Nil{}
301 case Con{h, t}:
302 Con{h, share(t)}
303
304def main2(+args: List<&2, String>, +version: String) -> IO(Unit):
305 dispatch(parse(List.length(&2, String, args), args, default_opts()), version)
306
307
308def main(args: List<String>, version: String) -> IO(Unit):
309 main2(share(args), version)