domain/architecture/system/deployment/nix_darwin/setting/ops.bend
near-v-framework@0.5.0.0 · 697 lines · 0 definitions
1import Base
2import bend-net-url@0.4.0.0/url.bend as Url
3import near-v-framework@0.5.0.0/domain/architecture/system/plan/type.bend as P
4import near-v-framework@0.5.0.0/domain/architecture/system/deployment/nix_darwin/brew/type.bend as Br
5import near-v-framework@0.5.0.0/domain/architecture/system/deployment/nix_darwin/brew/ops.bend as BrO
6import near-v-framework@0.5.0.0/domain/architecture/system/deployment/nix_darwin/setting/type.bend as St
7
8# ---- mac.nix ----
9def literal(k: St.ValueKind, v: String) -> String:
10 match k:
11 case St.KStr{}:
12 "\"" ++ v ++ "\""
13 case _:
14 v
15
16def line(s: St.Setting) -> String:
17 match s:
18 case St.Setting{o, k, v}:
19 " " ++ o ++ " = " ++ literal(k, v) ++ ";\n"
20
21def lines(xs: List<&2, St.Setting>) -> String:
22 match xs:
23 case []:
24 ""
25 case s <> rest:
26 line(s) ++ lines(rest)
27
28def nix_bool(b: Bool) -> String:
29 Bool.pick(String, b, "true", "false")
30
31def firewall_nix(f: St.Firewall) -> String:
32 match f:
33 case St.FirewallOff{}:
34 " networking.applicationFirewall.enable = false;\n"
35 case St.FirewallOn{a, b, c, d}:
36 " networking.applicationFirewall.enable = true;\n" ++
37 " networking.applicationFirewall.allowSigned = " ++ nix_bool(a) ++ ";\n" ++
38 " networking.applicationFirewall.allowSignedApp = " ++ nix_bool(b) ++ ";\n" ++
39 " networking.applicationFirewall.blockAllIncoming = " ++ nix_bool(c) ++ ";\n" ++
40 " networking.applicationFirewall.enableStealthMode = " ++ nix_bool(d) ++ ";\n"
41
42# The nix-darwin module for the Mac: its settings, firewall and Homebrew. Homebrew uninstalls what is not
43# declared and never updates or upgrades on its own.
44def mac_nix(settings: List<&2, St.Setting>, fw: St.Firewall, brews: List<&2, Br.Brew>) -> String:
45 "# Generated by V from the system's nix-darwin settings. Do not edit.\n{ ... }: {\n" ++
46 lines(settings) ++
47 firewall_nix(fw) ++
48 " homebrew.enable = true;\n homebrew.onActivation.cleanup = \"uninstall\";\n homebrew.onActivation.autoUpdate = false;\n homebrew.onActivation.upgrade = false;\n" ++
49 BrO.nix_section(brews) ++ "}\n"
50
51def option_of(s: St.Setting) -> String:
52 match s:
53 case St.Setting{o, _, _}:
54 o
55
56def none_prefixed(xs: List<&2, St.Setting>, +p: String) -> Bool:
57 match xs:
58 case []:
59 True{}
60 case s <> rest:
61 Bool.not(String.starts_with(option_of(s), p)) && none_prefixed(rest, p)
62
63# The firewall is declared by its type, never as flat settings.
64def firewall_typed(settings: List<&2, St.Setting>) -> Bool:
65 none_prefixed(settings, "networking.applicationFirewall")
66
67# ---- comparing declared and observed ----
68def value_if(hit: Bool, s: St.Setting, other: String) -> String:
69 match s:
70 case St.Setting{_, _, v}:
71 Bool.pick(String, hit, v, other)
72
73# The first observed value of `option`, or "(unset)".
74def lookup(+option: String, now: List<&2, St.Setting>) -> String:
75 match now:
76 case []:
77 "(unset)"
78 case +s <> rest:
79 value_if(String.eq(option, option_of(s)), s, lookup(option, rest))
80
81def drop_zeros(cs: List<&2, Char>) -> List<&2, Char>:
82 match cs:
83 case []:
84 []
85 case +c <> +rest:
86 Bool.pick(List<&2, Char>, Char.is_eq(c, '0'), drop_zeros(rest), c <> rest)
87
88# "0.500000" -> "0.5", "0.000000" -> "0": numbers compare by value, not spelling.
89def trim_num(+s: String) -> String:
90 +t = String.reverse(String.from_list(drop_zeros(String.to_list(String.reverse(s)))))
91 Bool.pick(String, String.contains(s, "."), Bool.pick(String, String.ends_with(t, "."), String.take(t, Nat.sub(String.length(t), 1n)), t), s)
92
93def same(+now: String, +want: String) -> Bool:
94 String.eq(now, want) || String.eq(trim_num(now), trim_num(want))
95
96def change_if(+now: String, +option: String, +want: String) -> List<&2, P.Step>:
97 Bool.pick(List<&2, P.Step>, same(now, want), [], [P.Change{option, now, want}])
98
99def diff_one(s: St.Setting, +now: List<&2, St.Setting>) -> List<&2, P.Step>:
100 match s:
101 case St.Setting{+o, _, v}:
102 change_if(lookup(o, now), o, v)
103
104# Every declared setting whose observed value differs.
105def diff(want: List<&2, St.Setting>, +now: List<&2, St.Setting>) -> List<&2, P.Step>:
106 match want:
107 case []:
108 []
109 case s <> rest:
110 List.append(&2, P.Step, diff_one(s, now), diff(rest, now))
111
112def firewall_show(f: St.Firewall) -> String:
113 match f:
114 case St.FirewallOff{}:
115 "off"
116 case St.FirewallOn{a, b, c, d}:
117 "on (allowSigned " ++ nix_bool(a) ++ ", allowSignedApp " ++ nix_bool(b) ++ ", blockAllIncoming " ++ nix_bool(c) ++ ", stealth " ++ nix_bool(d) ++ ")"
118
119def firewall_change(want: St.Firewall, now: St.Firewall) -> List<&2, P.Step>:
120 +w = firewall_show(want)
121 +n = firewall_show(now)
122 Bool.pick(List<&2, P.Step>, String.eq(w, n), [], [P.Change{"networking.applicationFirewall", n, w}])
123
124# ---- words and lines ----
125def keep_if(keep: Bool, x: String, rest: List<&2, String>) -> List<&2, String>:
126 match keep:
127 case True{}:
128 x <> rest
129 case False{}:
130 rest
131
132def blank(cs: List<&2, Char>) -> List<&2, Char>:
133 match cs:
134 case []:
135 []
136 case +c <> rest:
137 Bool.pick(Char, Char.is_space(c), ' ', c) <> blank(rest)
138
139def words(s: String) -> List<&2, String>:
140 List.filter(~String, ~(w => Bool.not(String.is_empty(w))), String.split(String.from_list(blank(String.to_list(s))), ' '))
141
142def word(+s: String, n: Nat) -> String:
143 Maybe.default(&2, String, List.get(&2, String, words(s), n), "")
144
145def last_word(+s: String) -> String:
146 Maybe.default(&2, String, List.last(&2, String, words(s)), "")
147
148def filled_go(ls: List<&2, String>) -> List<&2, String>:
149 match ls:
150 case []:
151 []
152 case l <> rest:
153 +t = String.trim(l)
154 keep_if(Bool.not(String.is_empty(t)), t, filled_go(rest))
155
156# The trimmed, non-empty lines.
157def filled(s: String) -> List<&2, String>:
158 filled_go(String.lines(s))
159
160def distinct(xs: List<&2, String>) -> List<&2, String>:
161 match xs:
162 case []:
163 []
164 case +x <> +rest:
165 keep_if(Bool.not(List.contains(~String, ~String.eq, rest, x)), x, distinct(rest))
166
167def digits_of(cs: List<&2, Char>) -> String:
168 match cs:
169 case []:
170 ""
171 case +c <> rest:
172 Bool.pick(String, Char.is_digit(c), String.from_list([c]) ++ digits_of(rest), "")
173
174def digits(s: String) -> String:
175 digits_of(String.to_list(s))
176
177def nat_of(+s: String) -> Nat:
178 Maybe.default(&2, Nat, Nat.read(digits(String.trim(s))), 0n)
179
180def unquote(+s: String) -> String:
181 Bool.pick(String, String.starts_with(s, "\"") && String.ends_with(s, "\"") && Nat.is_gt(String.length(s), 1n),
182 String.take(String.drop(s, 1n), Nat.sub(String.length(s), 2n)), s)
183
184def nix_strs(xs: List<&2, String>) -> String:
185 match xs:
186 case []:
187 ""
188 case x <> rest:
189 " " ++ BrO.nix_str(x) ++ nix_strs(rest)
190
191# A list of strings as a Nix list.
192def nix_list(xs: List<&2, String>) -> String:
193 "[" ++ nix_strs(xs) ++ " ]"
194
195def nix_setting(o: String, v: String) -> St.Setting:
196 St.Setting{o, St.KNix{}, v}
197
198def bool_setting(o: String, b: Bool) -> St.Setting:
199 St.Setting{o, St.KBool{}, nix_bool(b)}
200
201def str_setting(o: String, v: String) -> St.Setting:
202 St.Setting{o, St.KStr{}, BrO.nix_esc(v)}
203
204def int_setting(o: String, v: String) -> St.Setting:
205 St.Setting{o, St.KInt{}, v}
206
207# The setting, when its observed value is not empty.
208def when_filled(+v: String, s: St.Setting) -> List<&2, St.Setting>:
209 Bool.pick(List<&2, St.Setting>, String.is_empty(String.trim(v)), [], [s])
210
211# ---- system.defaults: which options (nix/options.tsv, "option<TAB>type"), and where macOS keeps them ----
212# How an option's type reads from a plist; KNix for the types that are not one scalar (not read).
213def scalar_kind(+t: String) -> St.ValueKind:
214 Bool.pick(St.ValueKind, String.eq(t, "boolean"), St.KBool{},
215 Bool.pick(St.ValueKind, String.starts_with(t, "floating point"), St.KFloat{},
216 Bool.pick(St.ValueKind, String.starts_with(t, "signed integer") || String.starts_with(t, "(positive integer") || String.starts_with(t, "integer between") || String.starts_with(t, "one of 0") || String.starts_with(t, "one of 1") || String.starts_with(t, "value 1"), St.KInt{},
217 Bool.pick(St.ValueKind, String.eq(t, "string") || String.eq(t, "absolute path") || String.starts_with(t, "one of \"") || String.starts_with(t, "value \""), St.KStr{}, St.KNix{}))))
218
219def kind_of(+t: String) -> St.ValueKind:
220 Bool.pick(St.ValueKind, String.starts_with(t, "null or "), scalar_kind(String.drop(t, 8n)), St.KNix{})
221
222def option_line(+l: String) -> List<&2, St.Setting>:
223 +cols = String.split(l, '\t')
224 +o = Maybe.default(&2, String, List.head(&2, String, cols), "")
225 Bool.pick(List<&2, St.Setting>, String.is_empty(o), [],
226 [St.Setting{o, kind_of(Maybe.default(&2, String, List.get(&2, String, cols, 1n), "")), ""}])
227
228def option_lines(ls: List<&2, String>) -> List<&2, St.Setting>:
229 match ls:
230 case []:
231 []
232 case l <> rest:
233 List.append(&2, St.Setting, option_line(l), option_lines(rest))
234
235# nix-darwin's options with how each reads; each value is "" until read from the Mac.
236def options(tsv: String) -> List<&2, St.Setting>:
237 option_lines(String.lines(tsv))
238
239def after_defaults(+o: String) -> String:
240 Bool.pick(String, String.starts_with(o, "system.defaults."), String.drop(o, 16n), "")
241
242# `system.defaults.dock.tilesize` -> "dock"; `system.defaults.".GlobalPreferences".x` -> ".GlobalPreferences".
243def group_of(+o: String) -> String:
244 +r = after_defaults(o)
245 Bool.pick(String, String.starts_with(r, "\""),
246 Maybe.default(&2, String, List.get(&2, String, String.split(r, '"'), 1n), ""),
247 Maybe.default(&2, String, List.head(&2, String, String.split(r, '.')), ""))
248
249def key_of(+o: String) -> String:
250 +r = after_defaults(o)
251 unquote(Bool.pick(String, String.starts_with(r, "\""),
252 String.drop(String.join(List.drop(&2, String, String.split(r, '"'), 2n), "\""), 1n),
253 String.join(List.drop(&2, String, String.split(r, '.'), 1n), ".")))
254
255# The `defaults` domain of a group ("-currentHost d" for a per-host one), "" for groups not read.
256def domain(+g: String) -> String:
257 Bool.pick(String, String.eq(g, ".GlobalPreferences") || String.eq(g, "NSGlobalDomain"), "-g",
258 Bool.pick(String, String.eq(g, "LaunchServices"), "com.apple.LaunchServices",
259 Bool.pick(String, String.eq(g, "menuExtraClock"), "com.apple.menuextra.clock",
260 Bool.pick(String, String.eq(g, "dock"), "com.apple.dock",
261 Bool.pick(String, String.eq(g, "finder"), "com.apple.finder",
262 Bool.pick(String, String.eq(g, "hitoolbox"), "com.apple.HIToolbox",
263 Bool.pick(String, String.eq(g, "iCal"), "com.apple.iCal",
264 Bool.pick(String, String.eq(g, "magicmouse"), "com.apple.AppleMultitouchMouse",
265 Bool.pick(String, String.eq(g, "screencapture"), "com.apple.screencapture",
266 Bool.pick(String, String.eq(g, "screensaver"), "com.apple.screensaver",
267 Bool.pick(String, String.eq(g, "spaces"), "com.apple.spaces",
268 Bool.pick(String, String.eq(g, "trackpad"), "com.apple.AppleMultitouchTrackpad",
269 Bool.pick(String, String.eq(g, "universalaccess"), "com.apple.universalaccess",
270 Bool.pick(String, String.eq(g, "ActivityMonitor"), "com.apple.ActivityMonitor",
271 Bool.pick(String, String.eq(g, "WindowManager"), "com.apple.WindowManager",
272 Bool.pick(String, String.eq(g, "controlcenter"), "-currentHost com.apple.controlcenter",
273 Bool.pick(String, String.eq(g, "loginwindow"), "/Library/Preferences/com.apple.loginwindow",
274 Bool.pick(String, String.eq(g, "smb"), "/Library/Preferences/SystemConfiguration/com.apple.smb.server",
275 Bool.pick(String, String.eq(g, "SoftwareUpdate"), "/Library/Preferences/com.apple.SoftwareUpdate", "")))))))))))))))))))
276
277# `defaults` arguments that write a domain's plist XML to standard output.
278def export_args(+d: String) -> List<&2, String>:
279 Bool.pick(List<&2, String>, String.starts_with(d, "-currentHost "), ["-currentHost", "export", String.drop(d, 13n), "-"], ["export", d, "-"])
280
281def is_nix(k: St.ValueKind) -> Bool:
282 match k:
283 case St.KNix{}:
284 True{}
285 case _:
286 False{}
287
288# The domain an option is read from, "" when it is not read from one.
289def domain_of(s: St.Setting) -> String:
290 match s:
291 case St.Setting{+o, k, _}:
292 Bool.pick(String, is_nix(k), "", domain(group_of(o)))
293
294def domains_go(xs: List<&2, St.Setting>) -> List<&2, String>:
295 match xs:
296 case []:
297 []
298 case s <> rest:
299 +d = domain_of(s)
300 keep_if(Bool.not(String.is_empty(d)), d, domains_go(rest))
301
302# The domains the options are read from, each once.
303def domains(xs: List<&2, St.Setting>) -> List<&2, String>:
304 distinct(domains_go(xs))
305
306# ---- plist XML, as `defaults export` writes it (real characters, unlike `defaults read`) ----
307def text_lines_go(s: String, +cur: List<&2, Char>, +acc: List<&2, String>) -> List<&2, String>:
308 match s:
309 case SNil{}:
310 List.reverse(&2, String, String.from_list(List.reverse(&2, Char, cur)) <> acc)
311 case SCon{+h, t}:
312 +nl = Char.is_eq(h, '\n')
313 text_lines_go(t, Bool.pick(List<&2, Char>, nl, [], h <> cur), Bool.pick(List<&2, String>, nl, String.from_list(List.reverse(&2, Char, cur)) <> acc, acc))
314
315# String.lines in constant stack: Base's recurses as deep as the text is long, too deep for a large plist.
316def text_lines(s: String) -> List<&2, String>:
317 text_lines_go(s, [], [])
318
319def xml_entity(+seg: String) -> String:
320 Bool.pick(String, String.starts_with(seg, "amp;"), "&" ++ String.drop(seg, 4n),
321 Bool.pick(String, String.starts_with(seg, "lt;"), "<" ++ String.drop(seg, 3n),
322 Bool.pick(String, String.starts_with(seg, "gt;"), ">" ++ String.drop(seg, 3n),
323 Bool.pick(String, String.starts_with(seg, "quot;"), "\"" ++ String.drop(seg, 5n),
324 Bool.pick(String, String.starts_with(seg, "apos;"), "'" ++ String.drop(seg, 5n), "&" ++ seg)))))
325
326def xml_entities(segs: List<&2, String>) -> String:
327 match segs:
328 case []:
329 ""
330 case s <> rest:
331 xml_entity(s) ++ xml_entities(rest)
332
333def xml_unescape(+s: String) -> String:
334 +segs = String.split(s, '&')
335 Maybe.default(&2, String, List.head(&2, String, segs), "") ++ xml_entities(List.drop(&2, String, segs, 1n))
336
337def xml_escape_char(+c: Char) -> String:
338 Bool.pick(String, Char.is_eq(c, '&'), "&", Bool.pick(String, Char.is_eq(c, '<'), "<", Bool.pick(String, Char.is_eq(c, '>'), ">", String.from_list([c]))))
339
340def xml_escape(cs: List<&2, Char>) -> String:
341 match cs:
342 case []:
343 ""
344 case c <> rest:
345 xml_escape_char(c) ++ xml_escape(rest)
346
347# "<true/>" -> "1", "<false/>" -> "0", "<integer>3</integer>" -> "3", "<string>a & b</string>" -> "a & b";
348# "" when unset or not a scalar.
349def plist_scalar(+v: String) -> String:
350 +t = String.trim(v)
351 Bool.pick(String, String.starts_with(t, "<true/>"), "1",
352 Bool.pick(String, String.starts_with(t, "<false/>"), "0",
353 Bool.pick(String, String.starts_with(t, "<dict") || String.starts_with(t, "<array") || String.starts_with(t, "<data"), "",
354 xml_unescape(Maybe.default(&2, String, List.head(&2, String, String.split(Maybe.default(&2, String, List.get(&2, String, String.split(t, '>'), 1n), ""), '<')), "")))))
355
356def ends_scalar(+l: String) -> Bool:
357 String.contains(l, "</string>") || String.contains(l, "</integer>") || String.contains(l, "</real>") || String.contains(l, "/>") || String.contains(l, "<dict>") || String.contains(l, "<array>") || String.contains(l, "<data>")
358
359# The lines of a value, up to the one that ends it.
360def value_lines(ls: List<&2, String>, done: Bool) -> String:
361 match ls done:
362 case [] _:
363 ""
364 case _ True{}:
365 ""
366 case +l <> rest False{}:
367 l ++ "\n" ++ value_lines(rest, ends_scalar(l))
368
369def after_key(ls: List<&2, String>, +target: String, hit: Bool) -> String:
370 match ls hit:
371 case [] _:
372 ""
373 case xs True{}:
374 value_lines(xs, False{})
375 case +l <> rest False{}:
376 after_key(rest, target, String.eq(l, target))
377
378# The scalar of a top-level key in a plist's XML lines, "" when absent.
379def plist_value(+key: String, ls: List<&2, String>) -> String:
380 plist_scalar(after_key(ls, "\t<key>" ++ xml_escape(String.to_list(key)) ++ "</key>", False{}))
381
382# ---- values, written as nix-darwin writes them ----
383def normalize_kind(k: St.ValueKind, +v: String) -> String:
384 match k:
385 case St.KBool{}:
386 Bool.pick(String, String.eq(v, "1"), "true", Bool.pick(String, String.eq(v, "0"), "false", ""))
387 case St.KFloat{}:
388 Bool.pick(String, String.contains(v, "."), v, v ++ ".0")
389 case St.KStr{}:
390 BrO.nix_esc(v)
391 case _:
392 v
393
394def normalize(k: St.ValueKind, +v: String) -> String:
395 Bool.pick(String, String.is_empty(v), "", normalize_kind(k, v))
396
397def index_label(+raw: String, labels: List<&2, String>) -> String:
398 Maybe.default(&2, String, List.get(&2, String, labels, Maybe.default(&2, Nat, Nat.read(raw), 99n)), "")
399
400def finder_label(+c: String) -> String:
401 Bool.pick(String, String.eq(c, "PfCm"), "Computer", Bool.pick(String, String.eq(c, "PfVo"), "OS volume",
402 Bool.pick(String, String.eq(c, "PfHm"), "Home", Bool.pick(String, String.eq(c, "PfDe"), "Desktop",
403 Bool.pick(String, String.eq(c, "PfDo"), "Documents", Bool.pick(String, String.eq(c, "PfAF"), "Recents",
404 Bool.pick(String, String.eq(c, "PfID"), "iCloud Drive", Bool.pick(String, String.eq(c, "PfLo"), "Other", ""))))))))
405
406# An option whose stored value is a code for a label (nix-darwin maps label -> code with `apply`).
407def value_for(+g: String, +key: String, k: St.ValueKind, +raw: String) -> String:
408 Bool.pick(String, String.eq(g, "hitoolbox") && String.eq(key, "AppleFnUsageType"), index_label(raw, ["Do Nothing", "Change Input Source", "Show Emoji & Symbols", "Start Dictation"]),
409 Bool.pick(String, String.eq(g, "iCal") && String.eq(key, "first day of week"), index_label(raw, ["System Setting", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]),
410 Bool.pick(String, String.eq(g, "finder") && String.eq(key, "NewWindowTarget"), finder_label(raw),
411 Bool.pick(String, String.eq(g, "controlcenter"), Bool.pick(String, String.eq(raw, "18"), "true", Bool.pick(String, String.eq(raw, "24"), "false", "")),
412 normalize(k, raw)))))
413
414def read_one(s: St.Setting, +ls: List<&2, String>) -> List<&2, St.Setting>:
415 match s:
416 case St.Setting{+o, +k, _}:
417 +v = value_for(group_of(o), key_of(o), k, plist_value(key_of(o), ls))
418 Bool.pick(List<&2, St.Setting>, String.is_empty(v), [], [St.Setting{o, k, v}])
419
420# The options read from domain `d`, with their values in its exported XML lines.
421def read_domain(xs: List<&2, St.Setting>, +d: String, +ls: List<&2, String>) -> List<&2, St.Setting>:
422 match xs:
423 case []:
424 []
425 case +s <> rest:
426 List.append(&2, St.Setting, Bool.pick(List<&2, St.Setting>, String.eq(domain_of(s), d), read_one(s, ls), []), read_domain(rest, d, ls))
427
428# ---- power: `pmset -g custom` lines like " sleep 10" (0 minutes: never) ----
429def pm_hit(+ws: List<&2, String>, +key: String) -> String:
430 Bool.pick(String, Nat.is_eq(List.length(&2, String, ws), 2n) && String.eq(Maybe.default(&2, String, List.head(&2, String, ws), ""), key),
431 Maybe.default(&2, String, List.get(&2, String, ws, 1n), ""), "")
432
433def pm_get(ls: List<&2, String>, +key: String) -> String:
434 match ls:
435 case []:
436 ""
437 case l <> rest:
438 +hit = pm_hit(words(l), key)
439 Bool.pick(String, String.is_empty(hit), pm_get(rest, key), hit)
440
441def minutes(+v: String) -> String:
442 Bool.pick(String, String.eq(v, "0"), "\"never\"", v)
443
444def power_settings(+pm: String) -> List<&2, St.Setting>:
445 +ls = String.lines(pm)
446 List.concat(&2, St.Setting, [
447 when_filled(pm_get(ls, "sleep"), int_setting("power.sleep.computer", minutes(pm_get(ls, "sleep")))),
448 when_filled(pm_get(ls, "displaysleep"), int_setting("power.sleep.display", minutes(pm_get(ls, "displaysleep")))),
449 when_filled(pm_get(ls, "disksleep"), int_setting("power.sleep.harddisk", minutes(pm_get(ls, "disksleep")))),
450 when_filled(pm_get(ls, "autorestart"), bool_setting("power.restartAfterPowerFailure", String.eq(pm_get(ls, "autorestart"), "1"))),
451 when_filled(pm_get(ls, "womp"), bool_setting("networking.wakeOnLan.enable", String.eq(pm_get(ls, "womp"), "1")))])
452
453def button_line(ls: List<&2, String>) -> String:
454 match ls:
455 case []:
456 ""
457 case +l <> rest:
458 Bool.pick(String, String.contains(l, "Sleep On Power Button"), last_word(l), button_line(rest))
459
460# `pmset -g`'s "Sleep On Power Button 1".
461def button_setting(+pm: String) -> List<&2, St.Setting>:
462 +b = button_line(String.lines(pm))
463 when_filled(b, bool_setting("power.sleep.allowSleepByPowerButton", String.eq(b, "1")))
464
465# ---- the firewall, from socketfilterfw's four answers ----
466def firewall_on(on: Bool, +f: String) -> St.Firewall:
467 match on:
468 case True{}:
469 St.FirewallOn{String.contains(f, "built-in signed software ENABLED"), String.contains(f, "downloaded signed software ENABLED"),
470 String.contains(f, "block all state set to enabled"), String.contains(f, "stealth mode is on")}
471 case False{}:
472 St.FirewallOff{}
473
474def firewall_of(+f: String) -> St.Firewall:
475 firewall_on(String.contains(f, "Firewall is enabled"), f)
476
477# ---- Dock tiles: system.defaults.dock.persistent-apps / persistent-others ----
478# "file:///Applications/Google%20Chrome.app/" -> "/Applications/Google Chrome.app"; bend-net-url decodes.
479def url_path(+u: String) -> String:
480 +a = Bool.pick(String, String.starts_with(u, "file://"), String.drop(u, 7n), u)
481 +b = Bool.pick(String, String.ends_with(a, "/") && Nat.is_gt(String.length(a), 1n), String.take(a, Nat.sub(String.length(a), 1n)), a)
482 Maybe.default(&2, String, Url.pct.decode(b), b)
483
484def arrangement(+n: String) -> String:
485 Bool.pick(String, String.eq(n, "2"), "date-added", Bool.pick(String, String.eq(n, "3"), "date-modified",
486 Bool.pick(String, String.eq(n, "4"), "date-created", Bool.pick(String, String.eq(n, "5"), "kind", "name"))))
487
488def displayas(+n: String) -> String:
489 Bool.pick(String, String.eq(n, "1"), "folder", "stack")
490
491def showas(+n: String) -> String:
492 Bool.pick(String, String.eq(n, "1"), "fan", Bool.pick(String, String.eq(n, "2"), "grid", Bool.pick(String, String.eq(n, "3"), "list", "automatic")))
493
494def app_tile(+t: String, +p: String) -> String:
495 Bool.pick(String, String.eq(t, "spacer-tile"), "{ spacer = { small = false; }; }",
496 Bool.pick(String, String.eq(t, "small-spacer-tile"), "{ spacer = { small = true; }; }",
497 Bool.pick(String, String.eq(t, "directory-tile"), "{ folder = " ++ BrO.nix_str(p) ++ "; }",
498 Bool.pick(String, String.ends_with(p, ".app"), "{ app = " ++ BrO.nix_str(p) ++ "; }", "{ file = " ++ BrO.nix_str(p) ++ "; }"))))
499
500# A tile from its type, URL, arrangement, displayas and showas (plist fields; the last three for folders).
501def tile_nix(+key: String, +t: String, +url: String, +arr: String, +disp: String, +show: String) -> String:
502 Bool.pick(String, String.eq(key, "persistent-apps"), app_tile(t, url_path(url)),
503 Bool.pick(String, String.eq(t, "directory-tile"),
504 "{ folder = { path = " ++ BrO.nix_str(url_path(url)) ++ "; arrangement = \"" ++ arrangement(arr) ++ "\"; displayas = \"" ++ displayas(disp) ++ "\"; showas = \"" ++ showas(show) ++ "\"; }; }",
505 "{ file = " ++ BrO.nix_str(url_path(url)) ++ "; }"))
506
507def spaced(xs: List<&2, String>) -> String:
508 match xs:
509 case []:
510 ""
511 case x <> rest:
512 " " ++ x ++ spaced(rest)
513
514def dock_setting(+key: String, +count: String, tiles: List<&2, String>) -> List<&2, St.Setting>:
515 when_filled(count, nix_setting("system.defaults.dock." ++ key, "[" ++ spaced(tiles) ++ " ]"))
516
517# ---- network: nix-darwin gives every known service the same DNS servers and search domains; a Mac where
518# they differ cannot be declared ----
519# `networksetup -listallnetworkservices`: a header line, then the services ("*" marks a disabled one).
520def services(+out: String) -> List<&2, String>:
521 List.filter(~String, ~(s => Bool.not(String.starts_with(s, "*"))), List.drop(&2, String, filled(out), 1n))
522
523# One service's servers or domains, "" when it has none.
524def per_service(+out: String) -> String:
525 Bool.pick(String, String.contains(out, "aren't any"), "", String.join(words(out), " "))
526
527def same_for_all(+what: String, +outs: List<&2, String>) -> String:
528 Bool.pick(String, Nat.is_gt(List.length(&2, String, distinct(outs)), 1n), "throw \"V: " ++ what ++ " differ per network service\"",
529 nix_list(words(Maybe.default(&2, String, List.head(&2, String, outs), ""))))
530
531# ---- security ----
532def is_nix_link(+target: String) -> Bool:
533 String.starts_with(String.trim(target), "/etc/static/")
534
535def sudo_keep(+l: String) -> Bool:
536 Bool.not(String.is_empty(String.trim(l)) || String.starts_with(l, "#") || String.contains(l, "env_keep+=TERMINFO"))
537
538def sudo_line(l: String) -> Bool:
539 sudo_keep(l)
540
541# The sudoers lines nix-darwin writes from security.sudo.extraConfig (not its terminfo lines).
542def sudo_extra(+sudo: String) -> String:
543 String.join(List.filter(~String, ~sudo_line, String.lines(sudo)), "\n")
544
545# ---- keyboard: hidutil's UserKeyMapping ----
546# The number after `w =` for each `w` in the words.
547def after_word(ws: List<&2, String>, +w: String) -> List<&2, String>:
548 match ws:
549 case []:
550 []
551 case +x <> +rest:
552 List.append(&2, String, Bool.pick(List<&2, String>, String.eq(x, w), [digits(Maybe.default(&2, String, List.get(&2, String, rest, 1n), ""))], []), after_word(rest, w))
553
554def pairs(src: List<&2, String>, dst: List<&2, String>) -> String:
555 match src:
556 case []:
557 ""
558 case s <> srest:
559 match dst:
560 case []:
561 ""
562 case d <> drest:
563 " { HIDKeyboardModifierMappingSrc = " ++ s ++ "; HIDKeyboardModifierMappingDst = " ++ d ++ "; }" ++ pairs(srest, drest)
564
565def keyboard_settings(+km: String) -> List<&2, St.Setting>:
566 +none = String.eq(String.trim(km), "(null)") || String.eq(String.trim(km), "(\n)") || String.is_empty(String.trim(km))
567 [bool_setting("system.keyboard.enableKeyMapping", Bool.not(none)),
568 nix_setting("system.keyboard.userKeyMapping", "[" ++ pairs(after_word(words(km), "HIDKeyboardModifierMappingSrc"), after_word(words(km), "HIDKeyboardModifierMappingDst")) ++ " ]"),
569 bool_setting("system.keyboard.remapCapsLockToControl", False{}),
570 bool_setting("system.keyboard.remapCapsLockToEscape", False{}),
571 bool_setting("system.keyboard.swapCapsLockAndEscape", False{}),
572 bool_setting("system.keyboard.swapLeftCommandAndLeftAlt", False{}),
573 bool_setting("system.keyboard.swapLeftCtrlAndFn", False{}),
574 bool_setting("system.keyboard.swapRightCommandAndRightOption", False{}),
575 bool_setting("system.keyboard.nonUS.remapTilde", False{})]
576
577# ---- system ----
578def after_segment(segs: List<&2, String>, +s: String) -> String:
579 match segs:
580 case []:
581 ""
582 case +x <> +rest:
583 Bool.pick(String, String.eq(x, s), String.join(rest, "/"), after_segment(rest, s))
584
585# "/var/db/timezone/zoneinfo/America/Matamoros" -> "America/Matamoros".
586def time_zone(+link: String) -> String:
587 after_segment(String.split(String.trim(link), '/'), "zoneinfo")
588
589def extra_shell_keep(+l: String) -> Bool:
590 Bool.not(String.starts_with(l, "#") || List.contains(~String, ~String.eq, ["/bin/bash", "/bin/csh", "/bin/dash", "/bin/ksh", "/bin/sh", "/bin/tcsh", "/bin/zsh"], l))
591
592def extra_shell(l: String) -> Bool:
593 extra_shell_keep(l)
594
595# /etc/shells beyond macOS's own.
596def extra_shells(+shells: String) -> List<&2, String>:
597 List.filter(~String, ~extra_shell, filled(shells))
598
599# nvram's "StartupMute %01" (muted); nothing when it is not set.
600def chime_setting(+mute: String) -> List<&2, St.Setting>:
601 when_filled(mute, bool_setting("system.startup.chime", Bool.not(String.eq(word(mute, 1n), "%01"))))
602
603def fonts_setting(fonts: List<&2, String>) -> St.Setting:
604 nix_setting("fonts.packages", Bool.pick(String, List.is_empty(&2, String, fonts), "[ ]", "throw \"V: /Library/Fonts/Nix Fonts has fonts from no declared package\""))
605
606# ---- users and groups (id >= 500, not system accounts) ----
607# `dscl . -list /Users UniqueID` lines "name 501" -> the names.
608def account(+l: String) -> List<&2, String>:
609 +n = word(l, 0n)
610 Bool.pick(List<&2, String>, Nat.is_ge(nat_of(word(l, 1n)), 500n) && Bool.not(String.starts_with(n, "_")) && Bool.not(String.is_empty(n)), [n], [])
611
612def accounts_go(ls: List<&2, String>) -> List<&2, String>:
613 match ls:
614 case []:
615 []
616 case l <> rest:
617 List.append(&2, String, account(l), accounts_go(rest))
618
619def accounts(+out: String) -> List<&2, String>:
620 accounts_go(String.lines(out))
621
622# `dscl . -read <record> Attr` answers "Attr: value" or "Attr:\n value".
623def dscl_value(+attr: String, +out: String) -> String:
624 +t = String.trim(out)
625 String.trim(String.join(String.lines(Bool.pick(String, String.starts_with(t, attr ++ ":"), String.drop(t, Nat.add(String.length(attr), 1n)), t)), " "))
626
627def user_settings(+n: String, +uid: String, +gid: String, +dir: String, +real: String, +shell: String, +hidden: String) -> List<&2, St.Setting>:
628 +u = "users.users.\"" ++ n ++ "\"."
629 [int_setting(u ++ "uid", uid), int_setting(u ++ "gid", gid), str_setting(u ++ "home", dir), str_setting(u ++ "description", real),
630 nix_setting(u ++ "shell", shell), bool_setting(u ++ "isHidden", String.eq(hidden, "1"))]
631
632def group_settings(+n: String, +gid: String, +real: String, +members: String) -> List<&2, St.Setting>:
633 +g = "users.groups.\"" ++ n ++ "\"."
634 [int_setting(g ++ "gid", gid), str_setting(g ++ "description", real), nix_setting(g ++ "members", nix_list(words(members)))]
635
636# ---- launchd files other than hidden ones, nix-darwin's own (org.nixos.*), the system's services and Determinate
637# Nix's; each is named by its content hash, as `./launch/<hash>-<file>` ----
638def plist_of(labels: List<&2, String>, +f: String) -> Bool:
639 match labels:
640 case []:
641 False{}
642 case l <> rest:
643 String.eq(l ++ ".plist", f) || plist_of(rest, f)
644
645def launch_kept(+f: String, +labels: List<&2, String>) -> Bool:
646 Bool.not(String.starts_with(f, ".") || String.starts_with(f, "org.nixos.") || String.starts_with(f, "systems.determinate.") || plist_of(labels, f))
647
648# `shasum -a 256` output -> the setting.
649def launch_setting(+opt: String, +f: String, +sum: String) -> St.Setting:
650 nix_setting(opt ++ ".\"" ++ f ++ "\".source", "./launch/" ++ String.take(String.trim(sum), 12n) ++ "-" ++ f)
651
652# ---- a settings file in V's types, for a system to import ----
653def kind_bend(k: St.ValueKind) -> String:
654 match k:
655 case St.KBool{}:
656 "St.KBool{}"
657 case St.KInt{}:
658 "St.KInt{}"
659 case St.KFloat{}:
660 "St.KFloat{}"
661 case St.KStr{}:
662 "St.KStr{}"
663 case St.KNix{}:
664 "St.KNix{}"
665
666def setting_bend(s: St.Setting) -> String:
667 match s:
668 case St.Setting{o, k, v}:
669 " St.Setting{" ++ BrO.bend_str(o) ++ ", " ++ kind_bend(k) ++ ", " ++ BrO.bend_str(v) ++ "}"
670
671def settings_bend(xs: List<&2, St.Setting>) -> List<&2, String>:
672 match xs:
673 case []:
674 []
675 case s <> rest:
676 setting_bend(s) <> settings_bend(rest)
677
678def bool_bend(b: Bool) -> String:
679 Bool.pick(String, b, "True{}", "False{}")
680
681def firewall_bend(f: St.Firewall) -> String:
682 match f:
683 case St.FirewallOff{}:
684 "St.FirewallOff{}"
685 case St.FirewallOn{a, b, c, d}:
686 "St.FirewallOn{" ++ bool_bend(a) ++ ", " ++ bool_bend(b) ++ ", " ++ bool_bend(c) ++ ", " ++ bool_bend(d) ++ "}"
687
688# The V package whose types the file names.
689def v_package() -> String:
690 "near-v-framework@0.3.0.0/domain/architecture/system/deployment/nix-darwin"
691
692# Bend source for the Mac's settings, firewall and Homebrew entries in V's types.
693def import_text(settings: List<&2, St.Setting>, fw: St.Firewall, brews: List<&2, Br.Brew>) -> String:
694 "import Base\nimport " ++ v_package() ++ "/setting/type.bend as St\nimport " ++ v_package() ++ "/brew/type.bend as Br\n\n" ++
695 "# The Mac as V read it.\n\ndef settings() -> List<&2, St.Setting>:\n [\n" ++ String.join(settings_bend(settings), ",\n") ++ "\n ]\n\n" ++
696 "# The application firewall: its options exist only while it is on.\ndef firewall() -> St.Firewall:\n " ++ firewall_bend(fw) ++ "\n\n" ++
697 "def brews() -> List<&2, Br.Brew>:\n" ++ BrO.bend_list(brews) ++ "\n"