1 import Base 2 import bend-net-url@0.4.0.0/url.bend as Url 3 import ../../../plan/type.bend as P 4 import ../brew/type.bend as Br 5 import ../brew/ops.bend as BrO 6 import ./type.bend as St 7 8 # ---- mac.nix ---- 9 def literal(k: St.ValueKind, v: String) -> String: 10 match k: 11 case St.KStr{}: 12 "\"" ++ v ++ "\"" 13 case _: 14 v 15 16 def line(s: St.Setting) -> String: 17 match s: 18 case St.Setting{o, k, v}: 19 " " ++ o ++ " = " ++ literal(k, v) ++ ";\n" 20 21 def lines(xs: List<&2, St.Setting>) -> String: 22 match xs: 23 case []: 24 "" 25 case s <> rest: 26 line(s) ++ lines(rest) 27 28 def nix_bool(b: Bool) -> String: 29 Bool.pick(String, b, "true", "false") 30 31 def 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. 44 def 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 51 def option_of(s: St.Setting) -> String: 52 match s: 53 case St.Setting{o, _, _}: 54 o 55 56 def 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. 64 def firewall_typed(settings: List<&2, St.Setting>) -> Bool: 65 none_prefixed(settings, "networking.applicationFirewall") 66 67 # ---- comparing declared and observed ---- 68 def 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)". 74 def 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 81 def 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. 89 def 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 93 def same(+now: String, +want: String) -> Bool: 94 String.eq(now, want) || String.eq(trim_num(now), trim_num(want)) 95 96 def 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 99 def 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. 105 def 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 112 def 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 119 def 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 ---- 125 def 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 132 def 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 139 def 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 142 def word(+s: String, n: Nat) -> String: 143 Maybe.default(&2, String, List.get(&2, String, words(s), n), "") 144 145 def last_word(+s: String) -> String: 146 Maybe.default(&2, String, List.last(&2, String, words(s)), "") 147 148 def 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. 157 def filled(s: String) -> List<&2, String>: 158 filled_go(String.lines(s)) 159 160 def 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 167 def 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 174 def digits(s: String) -> String: 175 digits_of(String.to_list(s)) 176 177 def nat_of(+s: String) -> Nat: 178 Maybe.default(&2, Nat, Nat.read(digits(String.trim(s))), 0n) 179 180 def 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 184 def 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. 192 def nix_list(xs: List<&2, String>) -> String: 193 "[" ++ nix_strs(xs) ++ " ]" 194 195 def nix_setting(o: String, v: String) -> St.Setting: 196 St.Setting{o, St.KNix{}, v} 197 198 def bool_setting(o: String, b: Bool) -> St.Setting: 199 St.Setting{o, St.KBool{}, nix_bool(b)} 200 201 def str_setting(o: String, v: String) -> St.Setting: 202 St.Setting{o, St.KStr{}, BrO.nix_esc(v)} 203 204 def 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. 208 def 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, "optiontype"), 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). 213 def 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 219 def 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 222 def 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 228 def 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. 236 def options(tsv: String) -> List<&2, St.Setting>: 237 option_lines(String.lines(tsv)) 238 239 def 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". 243 def 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 249 def 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. 256 def 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. 278 def 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 281 def 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. 289 def 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 294 def 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. 303 def 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`) ---- 307 def 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. 316 def text_lines(s: String) -> List<&2, String>: 317 text_lines_go(s, [], []) 318 319 def 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 326 def 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 333 def 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 337 def 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 340 def 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 # "" -> "1", "" -> "0", "3" -> "3", "a & b" -> "a & b"; 348 # "" when unset or not a scalar. 349 def plist_scalar(+v: String) -> String: 350 +t = String.trim(v) 351 Bool.pick(String, String.starts_with(t, ""), "1", 352 Bool.pick(String, String.starts_with(t, ""), "0", 353 Bool.pick(String, String.starts_with(t, "'), 1n), ""), '<')), ""))))) 355 356 def ends_scalar(+l: String) -> Bool: 357 String.contains(l, "") || String.contains(l, "") || String.contains(l, "") || String.contains(l, "/>") || String.contains(l, "") || String.contains(l, "") || String.contains(l, "") 358 359 # The lines of a value, up to the one that ends it. 360 def 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 369 def 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. 379 def plist_value(+key: String, ls: List<&2, String>) -> String: 380 plist_scalar(after_key(ls, "\t" ++ xml_escape(String.to_list(key)) ++ "", False{})) 381 382 # ---- values, written as nix-darwin writes them ---- 383 def 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 394 def normalize(k: St.ValueKind, +v: String) -> String: 395 Bool.pick(String, String.is_empty(v), "", normalize_kind(k, v)) 396 397 def 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 400 def 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`). 407 def 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 414 def 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. 421 def 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) ---- 429 def 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 433 def 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 441 def minutes(+v: String) -> String: 442 Bool.pick(String, String.eq(v, "0"), "\"never\"", v) 443 444 def 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 453 def 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". 461 def 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 ---- 466 def 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 474 def 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. 479 def 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 484 def 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 488 def displayas(+n: String) -> String: 489 Bool.pick(String, String.eq(n, "1"), "folder", "stack") 490 491 def 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 494 def 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). 501 def 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 507 def spaced(xs: List<&2, String>) -> String: 508 match xs: 509 case []: 510 "" 511 case x <> rest: 512 " " ++ x ++ spaced(rest) 513 514 def 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). 520 def 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. 524 def per_service(+out: String) -> String: 525 Bool.pick(String, String.contains(out, "aren't any"), "", String.join(words(out), " ")) 526 527 def 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 ---- 532 def is_nix_link(+target: String) -> Bool: 533 String.starts_with(String.trim(target), "/etc/static/") 534 535 def 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 538 def 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). 542 def 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. 547 def 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 554 def 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 565 def 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 ---- 578 def 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". 586 def time_zone(+link: String) -> String: 587 after_segment(String.split(String.trim(link), '/'), "zoneinfo") 588 589 def 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 592 def extra_shell(l: String) -> Bool: 593 extra_shell_keep(l) 594 595 # /etc/shells beyond macOS's own. 596 def 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. 600 def 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 603 def 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. 608 def 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 612 def 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 619 def accounts(+out: String) -> List<&2, String>: 620 accounts_go(String.lines(out)) 621 622 # `dscl . -read Attr` answers "Attr: value" or "Attr:\n value". 623 def 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 627 def 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 632 def 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/-` ---- 638 def 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 645 def 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. 649 def 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 ---- 653 def 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 666 def 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 671 def 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 678 def bool_bend(b: Bool) -> String: 679 Bool.pick(String, b, "True{}", "False{}") 680 681 def 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. 689 def 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. 693 def 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"