1 import Base 2 import ./json.bend as Json 3 import ./act.bend as Act 4 import ./cmd.bend as Cmd 5 import ./fs.bend as Fs 6 import ./setload.bend as Setload 7 import ./setwrite.bend as Setwrite 8 import ./setcmd.bend as Setcmd 9 import ./profile.bend as P 10 import ./policy/safe.bend as SF 11 import ./daemon/config.bend as Cfg 12 import ./profview.bend as PV 13 14 # `cswap profile list|show [NAME]|use NAME|set NAME KEY VALUE|remove NAME`, 15 # each with `--json`. The profiles live in settings.json (src/profile.bend); 16 # a write changes only `profiles` / `active_profile` and keeps every other key. 17 18 def ActM(-A: Type) -> Type: 19 Act.of(A) 20 21 def ActM.pure(-A: Type, x: A) -> Act.of(A): 22 Act.pure(A, x) 23 24 def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B): 25 Act.bind(A, B, m, f) 26 27 # ----------------------------------------------------------------- commands 28 29 # What a command settled on: an answer without writing, or the settings to 30 # write and the answer to give once they are. 31 type Plan2 is Data: 32 Answer{out: Cmd.Out} 33 Wr{raw: Json.Json, out: Cmd.Out} 34 35 def doc(pairs: List<(String & Json.Json)>) -> Cmd.Out: 36 Cmd.doc(Json.of_pairs(Con{("schemaVersion", Json.JNum{"1"}), pairs})) 37 38 def err(+json: Bool, +msg: String) -> Cmd.Out: 39 Cmd.failed(json, "ConfigError", msg) 40 41 def unknown_msg(+name: String) -> String: 42 "unknown profile '" ++ name ++ "'; `cswap profile list` shows the profiles" 43 44 def active_json(+m: Maybe<&2, String>) -> Maybe<&2, Json.Json>: 45 match m: 46 case Some{s}: 47 Some{Json.JStr{s}} 48 case None{}: 49 None{} 50 51 def list_doc(+raw: Json.Json) -> Cmd.Out: 52 doc([("active", PV.opt_json(active_json(P.active(raw)))), ("profiles", Json.arr(PV.prof_jsons(raw, Cfg.from_settings(raw), P.names(raw))))]) 53 54 def list_out(+json: Bool, +raw: Json.Json) -> Cmd.Out: 55 match json: 56 case True{}: 57 list_doc(raw) 58 case False{}: 59 Cmd.ok(PV.table(PV.rows_of(raw, Cfg.from_settings(raw), P.names(raw)))) 60 61 def yes_no(+b: Bool) -> String: 62 match b: 63 case True{}: 64 "yes" 65 case False{}: 66 "no" 67 68 def show_text2(+r: PV.Row) -> String: 69 match r: 70 case PV.Row{m, n, s, t, ms, c, i, src}: 71 "name: " ++ n ++ "\nstrategy: " ++ s ++ "\nthreshold: " ++ t ++ "\nmodels: " ++ ms ++ "\ncooldown_s: " ++ c ++ "\ninterval_s: " ++ i ++ "\nsource: " ++ src ++ "\nactive: " ++ yes_no(String.eq(m, "*")) 72 73 def show_text(+raw: Json.Json, +cfg: Cfg.Cfg, +p: P.Prof) -> String: 74 show_text2(PV.row_of(raw, cfg, p)) 75 76 def show_out(+json: Bool, +raw: Json.Json, +p: P.Prof) -> Cmd.Out: 77 match json: 78 case True{}: 79 doc([("profile", PV.prof_json(raw, Cfg.from_settings(raw), p))]) 80 case False{}: 81 Cmd.ok(show_text(raw, Cfg.from_settings(raw), p)) 82 83 def show_found(+json: Bool, +raw: Json.Json, +name: String, +m: Maybe<&2, P.Prof>) -> Cmd.Out: 84 match m: 85 case None{}: 86 err(json, unknown_msg(name)) 87 case Some{p}: 88 show_out(json, raw, p) 89 90 def show_name(+raw: Json.Json, +n: Maybe<&2, String>) -> String: 91 match n: 92 case Some{x}: 93 x 94 case None{}: 95 Maybe.default(&2, String, P.active(raw), "default") 96 97 def show_out2(+json: Bool, +raw: Json.Json, +name: String) -> Cmd.Out: 98 show_found(json, raw, name, P.find(raw, name)) 99 100 def run.list(+root: String, +json: Bool) -> IO(Cmd.Out): 101 IO.bind(Json.Json, Cmd.Out, Setload.load_raw(root), raw => IO.pure(Cmd.Out, list_out(json, raw))) 102 103 def show_run(+json: Bool, +n: Maybe<&2, String>, +raw: Json.Json) -> IO(Cmd.Out): 104 IO.pure(Cmd.Out, show_out2(json, raw, show_name(raw, n))) 105 106 def run.show(+root: String, +json: Bool, +n: Maybe<&2, String>) -> IO(Cmd.Out): 107 IO.bind(Json.Json, Cmd.Out, Setload.load_raw(root), raw => show_run(json, n, raw)) 108 109 # ---- use 110 111 def use_ok(+json: Bool, +raw: Json.Json, +name: String, +p: P.Prof) -> Cmd.Out: 112 match json: 113 case True{}: 114 doc([("activeProfile", Json.JStr{name}), ("profile", PV.prof_json(raw, Cfg.from_settings(raw), p))]) 115 case False{}: 116 Cmd.ok("active profile: " ++ name ++ " (" ++ SF.name(P.algo_base(p)) ++ ")") 117 118 def use_plan2(+m: Maybe<&2, P.Prof>, +json: Bool, +raw: Json.Json, +name: String) -> Plan2: 119 match m: 120 case None{}: 121 Answer{err(json, unknown_msg(name))} 122 case Some{p}: 123 Wr{P.use(raw, name), use_ok(json, P.use(raw, name), name, p)} 124 125 def use_plan(+json: Bool, +raw: Json.Json, +name: String) -> Plan2: 126 use_plan2(P.find(raw, name), json, raw, name) 127 128 # ---- set 129 130 def set_ok(+json: Bool, +raw: Json.Json, +name: String, +key: String, +value: String, +p: P.Prof) -> Cmd.Out: 131 match json: 132 case True{}: 133 doc([("profile", PV.prof_json(raw, Cfg.from_settings(raw), p))]) 134 case False{}: 135 Cmd.ok("profile " ++ name ++ ": " ++ key ++ " = " ++ value) 136 137 def set_plan3(+u: P.Upd, +json: Bool, +raw: Json.Json, +name: String, +key: String, +value: String) -> Plan2: 138 match u: 139 case P.UBad{m}: 140 Answer{err(json, m)} 141 case P.UOk{p}: 142 Wr{P.store(raw, p), set_ok(json, P.store(raw, p), name, key, value, p)} 143 144 def set_base(+m: Maybe<&2, P.Prof>, +name: String) -> P.Prof: 145 Maybe.default(&2, P.Prof, m, P.bare(name, SF.ABest{})) 146 147 def set_plan2(+valid: Bool, +json: Bool, +raw: Json.Json, +name: String, +key: String, +value: String) -> Plan2: 148 match valid: 149 case False{}: 150 Answer{err(json, "invalid profile name '" ++ name ++ "': use letters, digits, '-' and '_', starting with a letter or digit")} 151 case True{}: 152 set_plan3(P.update(set_base(P.find(raw, name), name), key, value), json, raw, name, key, value) 153 154 def set_plan1(+is_default: Bool, +json: Bool, +raw: Json.Json, +name: String, +key: String, +value: String) -> Plan2: 155 match is_default: 156 case True{}: 157 Answer{err(json, "profile 'default' follows the autoswitch settings and cannot be changed; create another profile, or use `cswap config set`")} 158 case False{}: 159 set_plan2(P.valid_name(name), json, raw, name, key, value) 160 161 def set_plan(+json: Bool, +raw: Json.Json, +name: String, +key: String, +value: String) -> Plan2: 162 set_plan1(String.eq(name, "default"), json, raw, name, key, value) 163 164 # ---- remove 165 166 def gone_note(+was_active: Bool) -> String: 167 match was_active: 168 case True{}: 169 "\nactive profile cleared (back to default)" 170 case False{}: 171 "" 172 173 def gone_ok(+json: Bool, +raw: Json.Json, +name: String, +was_active: Bool) -> Cmd.Out: 174 match json: 175 case True{}: 176 doc([("removed", Json.JStr{name}), ("activeProfile", PV.opt_json(active_json(P.active(raw))))]) 177 case False{}: 178 Cmd.ok("removed profile " ++ name ++ gone_note(was_active)) 179 180 def gone_missing(+builtin: Bool, +name: String) -> String: 181 match builtin: 182 case True{}: 183 "profile '" ++ name ++ "' is built in and has no stored override to remove" 184 case False{}: 185 unknown_msg(name) 186 187 def gone_plan3(+stored: Bool, +json: Bool, +raw: Json.Json, +name: String) -> Plan2: 188 match stored: 189 case True{}: 190 Wr{P.remove(raw, name), gone_ok(json, P.remove(raw, name), name, String.eq(Maybe.default(&2, String, P.active(raw), ""), name))} 191 case False{}: 192 Answer{err(json, gone_missing(P.is_builtin(name), name))} 193 194 def gone_plan1(+is_default: Bool, +json: Bool, +raw: Json.Json, +name: String) -> Plan2: 195 match is_default: 196 case True{}: 197 Answer{err(json, "profile 'default' is built in and cannot be removed")} 198 case False{}: 199 gone_plan3(P.has_stored(raw, name), json, raw, name) 200 201 def gone_plan(+json: Bool, +raw: Json.Json, +name: String) -> Plan2: 202 gone_plan1(String.eq(name, "default"), json, raw, name) 203 204 # ---- running a plan 205 206 def finish(+root: String, +w: Plan2) -> Act.of(Cmd.Out): 207 match w: 208 case Answer{o}: 209 Act.pure(Cmd.Out, o) 210 case Wr{raw, o}: 211 do ActM: 212 u : Unit <- Setcmd.write_settings(root, raw) 213 Act.pure(Cmd.Out, o) 214 215 def edit(+root: String, +plan: Plan2) -> IO(Cmd.Out): 216 IO.bind(Fs.Res(Cmd.Out), Cmd.Out, finish(root, plan), r => IO.pure(Cmd.Out, Setcmd.fin(r))) 217 218 def use.act(+root: String, +json: Bool, +name: String) -> Act.of(Plan2): 219 do ActM: 220 +raw : Json.Json <- Setcmd.load_raw_w(root) 221 Act.pure(Plan2, use_plan(json, raw, name)) 222 223 def set.act(+root: String, +json: Bool, +name: String, +key: String, +value: String) -> Act.of(Plan2): 224 do ActM: 225 +raw : Json.Json <- Setcmd.load_raw_w(root) 226 Act.pure(Plan2, set_plan(json, raw, name, key, value)) 227 228 def gone.act(+root: String, +json: Bool, +name: String) -> Act.of(Plan2): 229 do ActM: 230 +raw : Json.Json <- Setcmd.load_raw_w(root) 231 Act.pure(Plan2, gone_plan(json, raw, name)) 232 233 # Read settings.json strictly (a torn file is an error, never replaced), plan, write. 234 def planned(+root: String, r: Fs.Res(Plan2)) -> IO(Cmd.Out): 235 match r: 236 case Fail{e}: 237 IO.pure(Cmd.Out, Cmd.fail(Setcmd.err.msg(e))) 238 case Done{w}: 239 edit(root, w) 240 241 def run.use(+root: String, +json: Bool, +name: String) -> IO(Cmd.Out): 242 IO.bind(Fs.Res(Plan2), Cmd.Out, use.act(root, json, name), r => planned(root, r)) 243 244 def run.set(+root: String, +json: Bool, +name: String, +key: String, +value: String) -> IO(Cmd.Out): 245 IO.bind(Fs.Res(Plan2), Cmd.Out, set.act(root, json, name, key, value), r => planned(root, r)) 246 247 def run.gone(+root: String, +json: Bool, +name: String) -> IO(Cmd.Out): 248 IO.bind(Fs.Res(Plan2), Cmd.Out, gone.act(root, json, name), r => planned(root, r)) 249 250 # ----------------------------------------------------------------- argv 251 252 type Plan is Data: 253 PList{} 254 PShow{name: Maybe<&2, String>} 255 PUse{name: String} 256 PSet{name: String, key: String, value: String} 257 PRemove{name: String} 258 PBad{msg: String} 259 260 def join_sp(xs: List) -> String: 261 match xs: 262 case Nil{}: 263 "" 264 case Con{h, Nil{}}: 265 h 266 case Con{h, t}: 267 h ++ " " ++ join_sp(t) 268 269 def plan.none(rest: List, +p: Plan) -> Plan: 270 match rest: 271 case Nil{}: 272 p 273 case o: 274 PBad{"unrecognized arguments: " ++ join_sp(rest)} 275 276 def plan.show(rest: List) -> Plan: 277 match rest: 278 case Nil{}: 279 PShow{None{}} 280 case Con{n, t}: 281 plan.none(t, PShow{Some{n}}) 282 283 def plan.use(rest: List) -> Plan: 284 match rest: 285 case Nil{}: 286 PBad{"the following arguments are required: NAME"} 287 case Con{n, t}: 288 plan.none(t, PUse{n}) 289 290 def plan.gone(rest: List) -> Plan: 291 match rest: 292 case Nil{}: 293 PBad{"the following arguments are required: NAME"} 294 case Con{n, t}: 295 plan.none(t, PRemove{n}) 296 297 def plan.set(rest: List) -> Plan: 298 match rest: 299 case Nil{}: 300 PBad{"the following arguments are required: NAME, KEY, VALUE"} 301 case Con{n, Nil{}}: 302 PBad{"the following arguments are required: KEY, VALUE"} 303 case Con{n, Con{k, Nil{}}}: 304 PBad{"the following arguments are required: VALUE"} 305 case Con{n, Con{k, Con{v, t}}}: 306 plan.none(t, PSet{n, k, v}) 307 308 def plan.verb(+l: Bool, +sh: Bool, +u: Bool, +st: Bool, +dl: Bool, +a: String, rest: List) -> Plan: 309 match l sh u st dl: 310 case True{} _ _ _ _: 311 plan.none(rest, PList{}) 312 case False{} True{} _ _ _: 313 plan.show(rest) 314 case False{} False{} True{} _ _: 315 plan.use(rest) 316 case False{} False{} False{} True{} _: 317 plan.set(rest) 318 case False{} False{} False{} False{} True{}: 319 plan.gone(rest) 320 case _ _ _ _ _: 321 PBad{"argument action: invalid choice: '" ++ a ++ "' (choose from 'list', 'show', 'use', 'set', 'remove')"} 322 323 # `profile [list | show [NAME] | use NAME | set NAME KEY VALUE | remove NAME]`, flags removed. 324 def plan.of(args: List) -> Plan: 325 match args: 326 case Nil{}: 327 PList{} 328 case Con{+a, rest}: 329 plan.verb(String.eq(a, "list") || String.eq(a, "ls"), String.eq(a, "show"), String.eq(a, "use"), String.eq(a, "set"), String.eq(a, "remove"), a, rest) 330 331 def run(+root: String, +p: Plan, +json: Bool) -> IO(Cmd.Out): 332 match p: 333 case PList{}: 334 run.list(root, json) 335 case PShow{n}: 336 run.show(root, json, n) 337 case PUse{n}: 338 run.use(root, json, n) 339 case PSet{n, k, v}: 340 run.set(root, json, n, k, v) 341 case PRemove{n}: 342 run.gone(root, json, n) 343 case PBad{msg}: 344 IO.pure(Cmd.Out, Cmd.Out{2, "", "cswap profile: error: " ++ msg ++ "\n"}) 345 346 # `cswap profile ARGS...`: `args` are the words after `profile` with the 347 # `--json` / `--debug` flags already taken off, `json` the --json flag. 348 def profile(+root: String, args: List, +json: Bool) -> IO(Cmd.Out): 349 run(root, plan.of(args), json)