cli/vimport.bend
near-v-framework@0.1.0.0 · 321 lines · 0 definitions
1import Base
2import near-v-framework@0.1.0.0/lib/effs/io.bend as X
3import near-v-framework@0.1.0.0/lib/vstr.bend as S
4import near-v-framework@0.1.0.0/lib/mac_types.bend as T
5import near-v-framework@0.1.0.0/cli/project.bend as P
6import near-v-framework@0.1.0.0/cli/vmac.bend as V
7
8# `v system import`: read the Mac into mac.now.bend (and, the first time, mac.bend) in the project.
9
10
11# ---- escaping (in vstr.bend) ----
12def esc(s: String) -> String:
13 S.esc(s)
14
15def drop_if(yes: Bool, s: String) -> String:
16 match yes:
17 case True{}:
18 String.drop(s, 1n)
19 case False{}:
20 s
21
22def unquote_end(+s: String) -> String:
23 String.reverse(drop_if(String.starts_with(String.reverse(s), "\""), String.reverse(s)))
24
25def unquote(+s: String) -> String:
26 unquote_end(drop_if(String.starts_with(s, "\""), s))
27
28
29def pick(c: Bool, a: String, b: String) -> String:
30 S.pick(c, a, b)
31
32# ---- which options, and how to read them ----
33def kind_of2(+t: String) -> String:
34 pick(String.eq(t, "boolean"), "KBool",
35 pick(String.starts_with(t, "floating point"), "KFloat",
36 pick(String.starts_with(t, "signed integer") || String.starts_with(t, "(positive integer") || String.starts_with(t, "one of 0") || String.starts_with(t, "one of 1") || String.starts_with(t, "value 1"), "KInt",
37 pick(String.eq(t, "string") || String.eq(t, "absolute path") || String.starts_with(t, "one of \"") || String.starts_with(t, "value \""), "KStr", ""))))
38
39def kind_of(+t: String) -> String:
40 pick(String.starts_with(t, "null or "), kind_of2(String.drop(t, 8n)), "")
41
42def rest_of(name: String) -> String:
43 String.drop(name, 16n)
44
45def group_of(+name: String) -> String:
46 pick(String.starts_with(rest_of(name), "\""), S.nth(String.split(rest_of(name), '"'), 1n), S.nth(String.split(rest_of(name), '.'), 0n))
47
48def key_of(+name: String) -> String:
49 unquote(pick(String.starts_with(rest_of(name), "\""),
50 String.drop(String.join(List.drop(&2, String, String.split(rest_of(name), '"'), 2n), "\""), 1n),
51 String.join(List.drop(&2, String, String.split(rest_of(name), '.'), 1n), ".")))
52
53# `defaults` arguments that read a group's domain; "" for groups not imported.
54def domain(+g: String) -> String:
55 pick(String.eq(g, ".GlobalPreferences") || String.eq(g, "NSGlobalDomain"), "read -g",
56 pick(String.eq(g, "LaunchServices"), "read com.apple.LaunchServices",
57 pick(String.eq(g, "menuExtraClock"), "read com.apple.menuextra.clock",
58 pick(String.eq(g, "dock"), "read com.apple.dock",
59 pick(String.eq(g, "finder"), "read com.apple.finder",
60 pick(String.eq(g, "hitoolbox"), "read com.apple.HIToolbox",
61 pick(String.eq(g, "iCal"), "read com.apple.iCal",
62 pick(String.eq(g, "magicmouse"), "read com.apple.AppleMultitouchMouse",
63 pick(String.eq(g, "screencapture"), "read com.apple.screencapture",
64 pick(String.eq(g, "screensaver"), "read com.apple.screensaver",
65 pick(String.eq(g, "spaces"), "read com.apple.spaces",
66 pick(String.eq(g, "trackpad"), "read com.apple.AppleMultitouchTrackpad",
67 pick(String.eq(g, "universalaccess"), "read com.apple.universalaccess",
68 pick(String.eq(g, "ActivityMonitor"), "read com.apple.ActivityMonitor",
69 pick(String.eq(g, "WindowManager"), "read com.apple.WindowManager",
70 pick(String.eq(g, "controlcenter"), "-currentHost read com.apple.controlcenter",
71 pick(String.eq(g, "loginwindow"), "read /Library/Preferences/com.apple.loginwindow",
72 pick(String.eq(g, "smb"), "read /Library/Preferences/SystemConfiguration/com.apple.smb.server",
73 pick(String.eq(g, "SoftwareUpdate"), "read /Library/Preferences/com.apple.SoftwareUpdate", "")))))))))))))))))))
74
75# "read X" -> "export X", "-currentHost read X" -> "-currentHost export X".
76def export_of(+d: String) -> String:
77 pick(String.starts_with(d, "read "), "export " ++ String.drop(d, 5n), "-currentHost export " ++ String.drop(d, 18n))
78
79# ---- values ----
80def nix_esc_chars(cs: List<&2, Char>) -> String:
81 S.nix_esc_chars(cs)
82
83def normalize1(+k: String, +v: String) -> String:
84 pick(String.eq(k, "KBool"), pick(String.eq(v, "1"), "true", pick(String.eq(v, "0"), "false", "")),
85 pick(String.eq(k, "KFloat"), pick(String.contains(v, "."), v, v ++ ".0"),
86 pick(String.eq(k, "KStr"), nix_esc_chars(String.to_list(v)), v)))
87
88def normalize(+k: String, +v: String) -> String:
89 pick(String.is_empty(v), "", normalize1(k, v))
90
91def one_if(b: Bool) -> Nat:
92 match b:
93 case True{}:
94 1n
95 case False{}:
96 0n
97
98
99def nat_more(+raw: String) -> Nat:
100 Nat.add(one_if(String.eq(raw, "1")), Nat.add(Nat.mul(2n, one_if(String.eq(raw, "2"))), Nat.add(Nat.mul(3n, one_if(String.eq(raw, "3"))), Nat.add(Nat.mul(4n, one_if(String.eq(raw, "4"))), Nat.add(Nat.mul(5n, one_if(String.eq(raw, "5"))), Nat.add(Nat.mul(6n, one_if(String.eq(raw, "6"))), Nat.add(Nat.mul(7n, one_if(String.eq(raw, "7"))), Nat.mul(99n, one_if(String.is_empty(raw))))))))))
101
102
103def index_label(+raw: String, labels: List<&2, String>) -> String:
104 S.nth(labels, nat_more(raw))
105
106
107def finder_label(+c: String) -> String:
108 pick(String.eq(c, "PfCm"), "Computer", pick(String.eq(c, "PfVo"), "OS volume", pick(String.eq(c, "PfHm"), "Home", pick(String.eq(c, "PfDe"), "Desktop",
109 pick(String.eq(c, "PfDo"), "Documents", pick(String.eq(c, "PfAF"), "Recents", pick(String.eq(c, "PfID"), "iCloud Drive", pick(String.eq(c, "PfLo"), "Other", ""))))))))
110
111# Options whose stored value is a code for a label (nix-darwin maps label -> code with `apply`).
112def value_for(+g: String, +key: String, +k: String, +raw: String) -> String:
113 pick(String.eq(g, "hitoolbox") && String.eq(key, "AppleFnUsageType"), index_label(raw, ["Do Nothing", "Change Input Source", "Show Emoji & Symbols", "Start Dictation"]),
114 pick(String.eq(g, "iCal") && String.eq(key, "first day of week"), index_label(raw, ["System Setting", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]),
115 pick(String.eq(g, "finder") && String.eq(key, "NewWindowTarget"), finder_label(raw),
116 pick(String.eq(g, "controlcenter"), pick(String.eq(raw, "18"), "true", pick(String.eq(raw, "24"), "false", "")),
117 normalize(k, raw)))))
118
119def setting_line(name: String, k: String, v: String) -> String:
120 S.setting_line(name, k, v)
121
122def line_if(keep: Bool, l: String) -> List<&2, String>:
123 S.line_if(keep, l)
124
125# ---- reading the Mac ----
126def read_value(r: Result<&1, &1, U32 & String, String>) -> String:
127 match r:
128 case Done{v}:
129 String.trim(v)
130 case Fail{_}:
131 ""
132
133# One option line "name<TAB>type" -> its setting line, if the Mac has a value for it.
134def option_read(+name: String, +k: String, +g: String) -> IO(List<&2, String>):
135 do IO<List<&2, String>>:
136 out : String <- X.sh(S.export_key(export_of(domain(g)), key_of(name)))
137 +v : String = value_for(g, key_of(name), k, S.plist_scalar(out))
138 return line_if(Bool.not(String.is_empty(v)), setting_line(name, k, v))
139
140def option_skip(skip: Bool, +name: String, +k: String, +g: String) -> IO(List<&2, String>):
141 match skip:
142 case True{}:
143 IO.pure(List<&2, String>, [])
144 case False{}:
145 option_read(name, k, g)
146
147def option_setting(+name: String, +k: String, +g: String) -> IO(List<&2, String>):
148 option_skip(String.is_empty(k) || String.is_empty(domain(g)), name, k, g)
149
150def option_line(+l: String) -> IO(List<&2, String>):
151 option_setting(S.nth(String.split(l, '\t'), 0n), kind_of(S.nth(String.split(l, '\t'), 1n)), group_of(S.nth(String.split(l, '\t'), 0n)))
152
153def option_lines(ls: List<&2, String>) -> IO(List<&2, String>):
154 match ls:
155 case []:
156 IO.pure(List<&2, String>, [])
157 case l <> rest:
158 do IO<List<&2, String>>:
159 a : List<&2, String> <- option_line(l)
160 b : List<&2, String> <- option_lines(rest)
161 return S.append(a, b)
162
163# ---- power, names, firewall ----
164def pm_hit(+w: List<&2, String>, +key: String) -> String:
165 pick(Nat.is_eq(S.count(w), 2n) && String.eq(S.nth(w, 0n), key), S.nth(w, 1n), "")
166
167def first_of(+a: String, b: String) -> String:
168 pick(String.is_empty(a), b, a)
169
170
171def pm_get(ls: List<&2, String>, +key: String) -> String:
172 match ls:
173 case []:
174 ""
175 case l <> rest:
176 first_of(pm_hit(S.words(l), key), pm_get(rest, key))
177
178def minutes(+v: String) -> String:
179 pick(String.eq(v, "0"), "\"never\"", v)
180
181def bool_of(+v: String) -> String:
182 pick(String.eq(v, "1"), "true", "false")
183
184def power_lines(+pm: String) -> List<&2, String>:
185 S.append(line_if(Bool.not(String.is_empty(pm_get(String.lines(pm), "sleep"))), setting_line("power.sleep.computer", "KInt", minutes(pm_get(String.lines(pm), "sleep")))),
186 S.append(line_if(Bool.not(String.is_empty(pm_get(String.lines(pm), "displaysleep"))), setting_line("power.sleep.display", "KInt", minutes(pm_get(String.lines(pm), "displaysleep")))),
187 S.append(line_if(Bool.not(String.is_empty(pm_get(String.lines(pm), "disksleep"))), setting_line("power.sleep.harddisk", "KInt", minutes(pm_get(String.lines(pm), "disksleep")))),
188 S.append(line_if(Bool.not(String.is_empty(pm_get(String.lines(pm), "autorestart"))), setting_line("power.restartAfterPowerFailure", "KBool", bool_of(pm_get(String.lines(pm), "autorestart")))),
189 line_if(Bool.not(String.is_empty(pm_get(String.lines(pm), "womp"))), setting_line("networking.wakeOnLan.enable", "KBool", bool_of(pm_get(String.lines(pm), "womp"))))))))
190
191def name_line(opt: String, r: Result<&1, &1, U32 & String, String>) -> List<&2, String>:
192 match r:
193 case Done{v}:
194 [setting_line(opt, "KStr", nix_esc_chars(String.to_list(String.trim(v))))]
195 case Fail{_}:
196 []
197
198def names() -> IO(List<&2, String>):
199 do IO<List<&2, String>>:
200 a : Result<&1, &1, U32 & String, String> <- X.Proc.run("scutil --get ComputerName 2>/dev/null")
201 b : Result<&1, &1, U32 & String, String> <- X.Proc.run("scutil --get LocalHostName 2>/dev/null")
202 c : Result<&1, &1, U32 & String, String> <- X.Proc.run("scutil --get HostName 2>/dev/null")
203 return S.append(name_line("networking.computerName", a), S.append(name_line("networking.localHostName", b), name_line("networking.hostName", c)))
204
205# ---- Homebrew ----
206def count_prefix(ls: List<&2, String>, +p: String) -> Nat:
207 match ls:
208 case []:
209 0n
210 case l <> rest:
211 Nat.add(one_if(String.starts_with(l, p)), count_prefix(rest, p))
212
213# `mas "Numbers", id: 409203825` -> `"Numbers" = 409203825`; a name the Brewfile repeats gets its id added.
214def mas_attr(+n: String, +id: String, +all: List<&2, String>) -> String:
215 S.nix_str(pick(Nat.is_gt(count_prefix(all, "mas \"" ++ n ++ "\","), 1n), n ++ " (" ++ id ++ ")", n)) ++ " = " ++ id
216
217def brew_pick(+k: String, +n: String, +l: String, +all: List<&2, String>) -> List<&2, String>:
218 line_if(Bool.not(String.is_empty(k)),
219 pick(String.eq(k, "tap") || String.eq(k, "brew") || String.eq(k, "cask"),
220 " T.Brew{\"" ++ k ++ "\", \"" ++ esc(n) ++ "\"}",
221 pick(String.eq(k, "mas"),
222 " T.Brew{\"mas\", \"" ++ esc(mas_attr(n, S.last(S.words(l)), all)) ++ "\"}",
223 " T.Brew{\"other\", \"" ++ esc(l) ++ "\"}")))
224
225def brew_entry(+l: String, +all: List<&2, String>) -> List<&2, String>:
226 brew_pick(pick(String.starts_with(String.trim(l), "#"), "", S.nth(S.words(l), 0n)), S.nth(String.split(l, '"'), 1n), l, all)
227
228def brew_entries(ls: List<&2, String>, +all: List<&2, String>) -> List<&2, String>:
229 match ls:
230 case []:
231 []
232 case l <> rest:
233 S.append(brew_entry(l, all), brew_entries(rest, all))
234
235# ---- the files ----
236def mac_text(+vlib: String, title: String, settings: List<&2, String>, fw: String, brew: List<&2, String>) -> String:
237 "import Base\nimport " ++ vlib ++ "/mac_types.bend as T\n\n# " ++ title ++ "\n\ndef settings() -> List<&2, T.Setting>:\n [\n" ++
238 String.join(settings, ",\n") ++ "\n ]\n\n# The application firewall (T.Firewall: its options exist only while it is on).\ndef firewall() -> T.Firewall:\n " ++ fw ++
239 "\n\ndef brew() -> List<&2, T.Brew>:\n [\n" ++ String.join(brew, ",\n") ++ "\n ]\n"
240
241# ---- keeping mac.bend: declared settings stay; options seen for the first time are added as observed ----
242def before_go(cs: List<&2, Char>, +pat: String) -> String:
243 match cs:
244 case []:
245 ""
246 case +c <> +rest:
247 pick(String.starts_with(String.from_list(c <> rest), pat), "", String.from_list([c]) ++ before_go(rest, pat))
248
249# A setting line's option part, which names it.
250def head_of(l: String) -> String:
251 before_go(String.to_list(l), "\", T.K")
252
253def heads(ls: List<&2, String>) -> List<&2, String>:
254 match ls:
255 case []:
256 []
257 case l <> rest:
258 head_of(l) <> heads(rest)
259
260def no_comma(+l: String) -> String:
261 pick(String.ends_with(l, ","), String.take(l, Nat.sub(String.length(l), 1n)), l)
262
263def entries(ls: List<&2, String>, +prefix: String) -> List<&2, String>:
264 match ls:
265 case []:
266 []
267 case +l <> rest:
268 S.append(line_if(String.starts_with(l, prefix), no_comma(l)), entries(rest, prefix))
269
270def unseen(obs: List<&2, String>, +known: List<&2, String>) -> List<&2, String>:
271 match obs:
272 case []:
273 []
274 case +l <> rest:
275 S.append(line_if(Bool.not(T.has(head_of(l), known)), l), unseen(rest, known))
276
277# The declared firewall: the line after its def ("" if mac.bend has none yet).
278def after_line(ls: List<&2, String>, +marker: String, prev: Bool) -> String:
279 match ls:
280 case []:
281 ""
282 case +l <> rest:
283 pick(prev, String.trim(l), after_line(rest, marker, String.eq(l, marker)))
284
285def declared_title() -> String:
286 "The Mac as it should be. Edit this; `v system dryrun` shows what would change."
287
288def write_declared(missing: Bool, +vlib: String, +settings: List<&2, String>, +fw: String, +brew: List<&2, String>) -> IO(Unit):
289 match missing:
290 case True{}:
291 do IO<Unit>:
292 X.write_file("mac.bend", mac_text(vlib, declared_title(), settings, fw, brew))
293 IO.print("wrote mac.bend (declared) from the Mac")
294 case False{}:
295 do IO<Unit>:
296 text : String <- X.sh("cat mac.bend")
297 +ls : List<&2, String> = String.lines(text)
298 +declared : List<&2, String> = entries(ls, " T.Setting{")
299 +added : List<&2, String> = unseen(settings, heads(declared))
300 +dfw : String = after_line(ls, "def firewall() -> T.Firewall:", False{})
301 X.write_file("mac.bend", mac_text(vlib, declared_title(), S.append(declared, added), pick(String.is_empty(dfw), fw, dfw), entries(ls, " T.Brew{")))
302 IO.print("mac.bend: " ++ Nat.show(S.count(added)) ++ " options declared for the first time (as observed)")
303
304def import_mac() -> IO(Unit):
305 do IO<Unit>:
306 +f : P.Facts <- P.read_facts()
307 P.when(Bool.not(P.has_target(f, "nix-darwin")), IO.die(Unit, 1, "import reads a Mac for a nix-darwin target; this project declares none"))
308 opts : String <- X.sh("cat nix/options.tsv")
309 prefs : List<&2, String> <- option_lines(String.lines(opts))
310 pm : String <- X.sh("pmset -g custom")
311 nm : List<&2, String> <- names()
312 +fw : String <- V.firewall()
313 rest : List<&2, String> <- V.read_all(P.labels_of(f))
314 bf : String <- X.sh("brew bundle dump --force --file=/tmp/v-Brewfile >/dev/null 2>&1; cat /tmp/v-Brewfile")
315 +settings : List<&2, String> = S.append(prefs, S.append(power_lines(pm), S.append(nm, rest)))
316 +bls : List<&2, String> = String.lines(bf)
317 +brew : List<&2, String> = brew_entries(bls, bls)
318 X.write_file("mac.now.bend", mac_text(P.vlib_of(f), "The Mac as it is, read by `v system import`. Generated; do not edit.", settings, fw, brew))
319 exists : Bool <- X.ok("test -f mac.bend")
320 write_declared(Bool.not(exists), P.vlib_of(f), settings, fw, brew)
321 IO.print("import: " ++ Nat.show(S.count(settings)) ++ " settings, " ++ Nat.show(S.count(brew)) ++ " Homebrew entries")