import Base import bend-net-url@0.4.0.0/url.bend as Url import ../../../plan/type.bend as P import ../brew/type.bend as Br import ../brew/ops.bend as BrO import ./type.bend as St # ---- mac.nix ---- def literal(k: St.ValueKind, v: String) -> String: match k: case St.KStr{}: "\"" ++ v ++ "\"" case _: v def line(s: St.Setting) -> String: match s: case St.Setting{o, k, v}: " " ++ o ++ " = " ++ literal(k, v) ++ ";\n" def lines(xs: List<&2, St.Setting>) -> String: match xs: case []: "" case s <> rest: line(s) ++ lines(rest) def nix_bool(b: Bool) -> String: Bool.pick(String, b, "true", "false") def firewall_nix(f: St.Firewall) -> String: match f: case St.FirewallOff{}: " networking.applicationFirewall.enable = false;\n" case St.FirewallOn{a, b, c, d}: " networking.applicationFirewall.enable = true;\n" ++ " networking.applicationFirewall.allowSigned = " ++ nix_bool(a) ++ ";\n" ++ " networking.applicationFirewall.allowSignedApp = " ++ nix_bool(b) ++ ";\n" ++ " networking.applicationFirewall.blockAllIncoming = " ++ nix_bool(c) ++ ";\n" ++ " networking.applicationFirewall.enableStealthMode = " ++ nix_bool(d) ++ ";\n" # The nix-darwin module for the Mac: its settings, firewall and Homebrew. Homebrew uninstalls what is not # declared and never updates or upgrades on its own. def mac_nix(settings: List<&2, St.Setting>, fw: St.Firewall, brews: List<&2, Br.Brew>) -> String: "# Generated by V from the system's nix-darwin settings. Do not edit.\n{ ... }: {\n" ++ lines(settings) ++ firewall_nix(fw) ++ " homebrew.enable = true;\n homebrew.onActivation.cleanup = \"uninstall\";\n homebrew.onActivation.autoUpdate = false;\n homebrew.onActivation.upgrade = false;\n" ++ BrO.nix_section(brews) ++ "}\n" def option_of(s: St.Setting) -> String: match s: case St.Setting{o, _, _}: o def none_prefixed(xs: List<&2, St.Setting>, +p: String) -> Bool: match xs: case []: True{} case s <> rest: Bool.not(String.starts_with(option_of(s), p)) && none_prefixed(rest, p) # The firewall is declared by its type, never as flat settings. def firewall_typed(settings: List<&2, St.Setting>) -> Bool: none_prefixed(settings, "networking.applicationFirewall") # ---- comparing declared and observed ---- def value_if(hit: Bool, s: St.Setting, other: String) -> String: match s: case St.Setting{_, _, v}: Bool.pick(String, hit, v, other) # The first observed value of `option`, or "(unset)". def lookup(+option: String, now: List<&2, St.Setting>) -> String: match now: case []: "(unset)" case +s <> rest: value_if(String.eq(option, option_of(s)), s, lookup(option, rest)) def drop_zeros(cs: List<&2, Char>) -> List<&2, Char>: match cs: case []: [] case +c <> +rest: Bool.pick(List<&2, Char>, Char.is_eq(c, '0'), drop_zeros(rest), c <> rest) # "0.500000" -> "0.5", "0.000000" -> "0": numbers compare by value, not spelling. def trim_num(+s: String) -> String: +t = String.reverse(String.from_list(drop_zeros(String.to_list(String.reverse(s))))) Bool.pick(String, String.contains(s, "."), Bool.pick(String, String.ends_with(t, "."), String.take(t, Nat.sub(String.length(t), 1n)), t), s) def same(+now: String, +want: String) -> Bool: String.eq(now, want) || String.eq(trim_num(now), trim_num(want)) def change_if(+now: String, +option: String, +want: String) -> List<&2, P.Step>: Bool.pick(List<&2, P.Step>, same(now, want), [], [P.Change{option, now, want}]) def diff_one(s: St.Setting, +now: List<&2, St.Setting>) -> List<&2, P.Step>: match s: case St.Setting{+o, _, v}: change_if(lookup(o, now), o, v) # Every declared setting whose observed value differs. def diff(want: List<&2, St.Setting>, +now: List<&2, St.Setting>) -> List<&2, P.Step>: match want: case []: [] case s <> rest: List.append(&2, P.Step, diff_one(s, now), diff(rest, now)) def firewall_show(f: St.Firewall) -> String: match f: case St.FirewallOff{}: "off" case St.FirewallOn{a, b, c, d}: "on (allowSigned " ++ nix_bool(a) ++ ", allowSignedApp " ++ nix_bool(b) ++ ", blockAllIncoming " ++ nix_bool(c) ++ ", stealth " ++ nix_bool(d) ++ ")" def firewall_change(want: St.Firewall, now: St.Firewall) -> List<&2, P.Step>: +w = firewall_show(want) +n = firewall_show(now) Bool.pick(List<&2, P.Step>, String.eq(w, n), [], [P.Change{"networking.applicationFirewall", n, w}]) # ---- words and lines ---- def keep_if(keep: Bool, x: String, rest: List<&2, String>) -> List<&2, String>: match keep: case True{}: x <> rest case False{}: rest def blank(cs: List<&2, Char>) -> List<&2, Char>: match cs: case []: [] case +c <> rest: Bool.pick(Char, Char.is_space(c), ' ', c) <> blank(rest) def words(s: String) -> List<&2, String>: List.filter(~String, ~(w => Bool.not(String.is_empty(w))), String.split(String.from_list(blank(String.to_list(s))), ' ')) def word(+s: String, n: Nat) -> String: Maybe.default(&2, String, List.get(&2, String, words(s), n), "") def last_word(+s: String) -> String: Maybe.default(&2, String, List.last(&2, String, words(s)), "") def filled_go(ls: List<&2, String>) -> List<&2, String>: match ls: case []: [] case l <> rest: +t = String.trim(l) keep_if(Bool.not(String.is_empty(t)), t, filled_go(rest)) # The trimmed, non-empty lines. def filled(s: String) -> List<&2, String>: filled_go(String.lines(s)) def distinct(xs: List<&2, String>) -> List<&2, String>: match xs: case []: [] case +x <> +rest: keep_if(Bool.not(List.contains(~String, ~String.eq, rest, x)), x, distinct(rest)) def digits_of(cs: List<&2, Char>) -> String: match cs: case []: "" case +c <> rest: Bool.pick(String, Char.is_digit(c), String.from_list([c]) ++ digits_of(rest), "") def digits(s: String) -> String: digits_of(String.to_list(s)) def nat_of(+s: String) -> Nat: Maybe.default(&2, Nat, Nat.read(digits(String.trim(s))), 0n) def unquote(+s: String) -> String: Bool.pick(String, String.starts_with(s, "\"") && String.ends_with(s, "\"") && Nat.is_gt(String.length(s), 1n), String.take(String.drop(s, 1n), Nat.sub(String.length(s), 2n)), s) def nix_strs(xs: List<&2, String>) -> String: match xs: case []: "" case x <> rest: " " ++ BrO.nix_str(x) ++ nix_strs(rest) # A list of strings as a Nix list. def nix_list(xs: List<&2, String>) -> String: "[" ++ nix_strs(xs) ++ " ]" def nix_setting(o: String, v: String) -> St.Setting: St.Setting{o, St.KNix{}, v} def bool_setting(o: String, b: Bool) -> St.Setting: St.Setting{o, St.KBool{}, nix_bool(b)} def str_setting(o: String, v: String) -> St.Setting: St.Setting{o, St.KStr{}, BrO.nix_esc(v)} def int_setting(o: String, v: String) -> St.Setting: St.Setting{o, St.KInt{}, v} # The setting, when its observed value is not empty. def when_filled(+v: String, s: St.Setting) -> List<&2, St.Setting>: Bool.pick(List<&2, St.Setting>, String.is_empty(String.trim(v)), [], [s]) # ---- system.defaults: which options (nix/options.tsv, "optiontype"), and where macOS keeps them ---- # How an option's type reads from a plist; KNix for the types that are not one scalar (not read). def scalar_kind(+t: String) -> St.ValueKind: Bool.pick(St.ValueKind, String.eq(t, "boolean"), St.KBool{}, Bool.pick(St.ValueKind, String.starts_with(t, "floating point"), St.KFloat{}, 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{}, 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{})))) def kind_of(+t: String) -> St.ValueKind: Bool.pick(St.ValueKind, String.starts_with(t, "null or "), scalar_kind(String.drop(t, 8n)), St.KNix{}) def option_line(+l: String) -> List<&2, St.Setting>: +cols = String.split(l, '\t') +o = Maybe.default(&2, String, List.head(&2, String, cols), "") Bool.pick(List<&2, St.Setting>, String.is_empty(o), [], [St.Setting{o, kind_of(Maybe.default(&2, String, List.get(&2, String, cols, 1n), "")), ""}]) def option_lines(ls: List<&2, String>) -> List<&2, St.Setting>: match ls: case []: [] case l <> rest: List.append(&2, St.Setting, option_line(l), option_lines(rest)) # nix-darwin's options with how each reads; each value is "" until read from the Mac. def options(tsv: String) -> List<&2, St.Setting>: option_lines(String.lines(tsv)) def after_defaults(+o: String) -> String: Bool.pick(String, String.starts_with(o, "system.defaults."), String.drop(o, 16n), "") # `system.defaults.dock.tilesize` -> "dock"; `system.defaults.".GlobalPreferences".x` -> ".GlobalPreferences". def group_of(+o: String) -> String: +r = after_defaults(o) Bool.pick(String, String.starts_with(r, "\""), Maybe.default(&2, String, List.get(&2, String, String.split(r, '"'), 1n), ""), Maybe.default(&2, String, List.head(&2, String, String.split(r, '.')), "")) def key_of(+o: String) -> String: +r = after_defaults(o) unquote(Bool.pick(String, String.starts_with(r, "\""), String.drop(String.join(List.drop(&2, String, String.split(r, '"'), 2n), "\""), 1n), String.join(List.drop(&2, String, String.split(r, '.'), 1n), "."))) # The `defaults` domain of a group ("-currentHost d" for a per-host one), "" for groups not read. def domain(+g: String) -> String: Bool.pick(String, String.eq(g, ".GlobalPreferences") || String.eq(g, "NSGlobalDomain"), "-g", Bool.pick(String, String.eq(g, "LaunchServices"), "com.apple.LaunchServices", Bool.pick(String, String.eq(g, "menuExtraClock"), "com.apple.menuextra.clock", Bool.pick(String, String.eq(g, "dock"), "com.apple.dock", Bool.pick(String, String.eq(g, "finder"), "com.apple.finder", Bool.pick(String, String.eq(g, "hitoolbox"), "com.apple.HIToolbox", Bool.pick(String, String.eq(g, "iCal"), "com.apple.iCal", Bool.pick(String, String.eq(g, "magicmouse"), "com.apple.AppleMultitouchMouse", Bool.pick(String, String.eq(g, "screencapture"), "com.apple.screencapture", Bool.pick(String, String.eq(g, "screensaver"), "com.apple.screensaver", Bool.pick(String, String.eq(g, "spaces"), "com.apple.spaces", Bool.pick(String, String.eq(g, "trackpad"), "com.apple.AppleMultitouchTrackpad", Bool.pick(String, String.eq(g, "universalaccess"), "com.apple.universalaccess", Bool.pick(String, String.eq(g, "ActivityMonitor"), "com.apple.ActivityMonitor", Bool.pick(String, String.eq(g, "WindowManager"), "com.apple.WindowManager", Bool.pick(String, String.eq(g, "controlcenter"), "-currentHost com.apple.controlcenter", Bool.pick(String, String.eq(g, "loginwindow"), "/Library/Preferences/com.apple.loginwindow", Bool.pick(String, String.eq(g, "smb"), "/Library/Preferences/SystemConfiguration/com.apple.smb.server", Bool.pick(String, String.eq(g, "SoftwareUpdate"), "/Library/Preferences/com.apple.SoftwareUpdate", ""))))))))))))))))))) # `defaults` arguments that write a domain's plist XML to standard output. def export_args(+d: String) -> List<&2, String>: Bool.pick(List<&2, String>, String.starts_with(d, "-currentHost "), ["-currentHost", "export", String.drop(d, 13n), "-"], ["export", d, "-"]) def is_nix(k: St.ValueKind) -> Bool: match k: case St.KNix{}: True{} case _: False{} # The domain an option is read from, "" when it is not read from one. def domain_of(s: St.Setting) -> String: match s: case St.Setting{+o, k, _}: Bool.pick(String, is_nix(k), "", domain(group_of(o))) def domains_go(xs: List<&2, St.Setting>) -> List<&2, String>: match xs: case []: [] case s <> rest: +d = domain_of(s) keep_if(Bool.not(String.is_empty(d)), d, domains_go(rest)) # The domains the options are read from, each once. def domains(xs: List<&2, St.Setting>) -> List<&2, String>: distinct(domains_go(xs)) # ---- plist XML, as `defaults export` writes it (real characters, unlike `defaults read`) ---- def text_lines_go(s: String, +cur: List<&2, Char>, +acc: List<&2, String>) -> List<&2, String>: match s: case SNil{}: List.reverse(&2, String, String.from_list(List.reverse(&2, Char, cur)) <> acc) case SCon{+h, t}: +nl = Char.is_eq(h, '\n') 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)) # String.lines in constant stack: Base's recurses as deep as the text is long, too deep for a large plist. def text_lines(s: String) -> List<&2, String>: text_lines_go(s, [], []) def xml_entity(+seg: String) -> String: Bool.pick(String, String.starts_with(seg, "amp;"), "&" ++ String.drop(seg, 4n), Bool.pick(String, String.starts_with(seg, "lt;"), "<" ++ String.drop(seg, 3n), Bool.pick(String, String.starts_with(seg, "gt;"), ">" ++ String.drop(seg, 3n), Bool.pick(String, String.starts_with(seg, "quot;"), "\"" ++ String.drop(seg, 5n), Bool.pick(String, String.starts_with(seg, "apos;"), "'" ++ String.drop(seg, 5n), "&" ++ seg))))) def xml_entities(segs: List<&2, String>) -> String: match segs: case []: "" case s <> rest: xml_entity(s) ++ xml_entities(rest) def xml_unescape(+s: String) -> String: +segs = String.split(s, '&') Maybe.default(&2, String, List.head(&2, String, segs), "") ++ xml_entities(List.drop(&2, String, segs, 1n)) def xml_escape_char(+c: Char) -> String: 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])))) def xml_escape(cs: List<&2, Char>) -> String: match cs: case []: "" case c <> rest: xml_escape_char(c) ++ xml_escape(rest) # "" -> "1", "" -> "0", "3" -> "3", "a & b" -> "a & b"; # "" when unset or not a scalar. def plist_scalar(+v: String) -> String: +t = String.trim(v) Bool.pick(String, String.starts_with(t, ""), "1", Bool.pick(String, String.starts_with(t, ""), "0", Bool.pick(String, String.starts_with(t, "'), 1n), ""), '<')), ""))))) def ends_scalar(+l: String) -> Bool: String.contains(l, "") || String.contains(l, "") || String.contains(l, "") || String.contains(l, "/>") || String.contains(l, "") || String.contains(l, "") || String.contains(l, "") # The lines of a value, up to the one that ends it. def value_lines(ls: List<&2, String>, done: Bool) -> String: match ls done: case [] _: "" case _ True{}: "" case +l <> rest False{}: l ++ "\n" ++ value_lines(rest, ends_scalar(l)) def after_key(ls: List<&2, String>, +target: String, hit: Bool) -> String: match ls hit: case [] _: "" case xs True{}: value_lines(xs, False{}) case +l <> rest False{}: after_key(rest, target, String.eq(l, target)) # The scalar of a top-level key in a plist's XML lines, "" when absent. def plist_value(+key: String, ls: List<&2, String>) -> String: plist_scalar(after_key(ls, "\t" ++ xml_escape(String.to_list(key)) ++ "", False{})) # ---- values, written as nix-darwin writes them ---- def normalize_kind(k: St.ValueKind, +v: String) -> String: match k: case St.KBool{}: Bool.pick(String, String.eq(v, "1"), "true", Bool.pick(String, String.eq(v, "0"), "false", "")) case St.KFloat{}: Bool.pick(String, String.contains(v, "."), v, v ++ ".0") case St.KStr{}: BrO.nix_esc(v) case _: v def normalize(k: St.ValueKind, +v: String) -> String: Bool.pick(String, String.is_empty(v), "", normalize_kind(k, v)) def index_label(+raw: String, labels: List<&2, String>) -> String: Maybe.default(&2, String, List.get(&2, String, labels, Maybe.default(&2, Nat, Nat.read(raw), 99n)), "") def finder_label(+c: String) -> String: Bool.pick(String, String.eq(c, "PfCm"), "Computer", Bool.pick(String, String.eq(c, "PfVo"), "OS volume", Bool.pick(String, String.eq(c, "PfHm"), "Home", Bool.pick(String, String.eq(c, "PfDe"), "Desktop", Bool.pick(String, String.eq(c, "PfDo"), "Documents", Bool.pick(String, String.eq(c, "PfAF"), "Recents", Bool.pick(String, String.eq(c, "PfID"), "iCloud Drive", Bool.pick(String, String.eq(c, "PfLo"), "Other", "")))))))) # An option whose stored value is a code for a label (nix-darwin maps label -> code with `apply`). def value_for(+g: String, +key: String, k: St.ValueKind, +raw: String) -> String: Bool.pick(String, String.eq(g, "hitoolbox") && String.eq(key, "AppleFnUsageType"), index_label(raw, ["Do Nothing", "Change Input Source", "Show Emoji & Symbols", "Start Dictation"]), 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"]), Bool.pick(String, String.eq(g, "finder") && String.eq(key, "NewWindowTarget"), finder_label(raw), Bool.pick(String, String.eq(g, "controlcenter"), Bool.pick(String, String.eq(raw, "18"), "true", Bool.pick(String, String.eq(raw, "24"), "false", "")), normalize(k, raw))))) def read_one(s: St.Setting, +ls: List<&2, String>) -> List<&2, St.Setting>: match s: case St.Setting{+o, +k, _}: +v = value_for(group_of(o), key_of(o), k, plist_value(key_of(o), ls)) Bool.pick(List<&2, St.Setting>, String.is_empty(v), [], [St.Setting{o, k, v}]) # The options read from domain `d`, with their values in its exported XML lines. def read_domain(xs: List<&2, St.Setting>, +d: String, +ls: List<&2, String>) -> List<&2, St.Setting>: match xs: case []: [] case +s <> rest: 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)) # ---- power: `pmset -g custom` lines like " sleep 10" (0 minutes: never) ---- def pm_hit(+ws: List<&2, String>, +key: String) -> String: Bool.pick(String, Nat.is_eq(List.length(&2, String, ws), 2n) && String.eq(Maybe.default(&2, String, List.head(&2, String, ws), ""), key), Maybe.default(&2, String, List.get(&2, String, ws, 1n), ""), "") def pm_get(ls: List<&2, String>, +key: String) -> String: match ls: case []: "" case l <> rest: +hit = pm_hit(words(l), key) Bool.pick(String, String.is_empty(hit), pm_get(rest, key), hit) def minutes(+v: String) -> String: Bool.pick(String, String.eq(v, "0"), "\"never\"", v) def power_settings(+pm: String) -> List<&2, St.Setting>: +ls = String.lines(pm) List.concat(&2, St.Setting, [ when_filled(pm_get(ls, "sleep"), int_setting("power.sleep.computer", minutes(pm_get(ls, "sleep")))), when_filled(pm_get(ls, "displaysleep"), int_setting("power.sleep.display", minutes(pm_get(ls, "displaysleep")))), when_filled(pm_get(ls, "disksleep"), int_setting("power.sleep.harddisk", minutes(pm_get(ls, "disksleep")))), when_filled(pm_get(ls, "autorestart"), bool_setting("power.restartAfterPowerFailure", String.eq(pm_get(ls, "autorestart"), "1"))), when_filled(pm_get(ls, "womp"), bool_setting("networking.wakeOnLan.enable", String.eq(pm_get(ls, "womp"), "1")))]) def button_line(ls: List<&2, String>) -> String: match ls: case []: "" case +l <> rest: Bool.pick(String, String.contains(l, "Sleep On Power Button"), last_word(l), button_line(rest)) # `pmset -g`'s "Sleep On Power Button 1". def button_setting(+pm: String) -> List<&2, St.Setting>: +b = button_line(String.lines(pm)) when_filled(b, bool_setting("power.sleep.allowSleepByPowerButton", String.eq(b, "1"))) # ---- the firewall, from socketfilterfw's four answers ---- def firewall_on(on: Bool, +f: String) -> St.Firewall: match on: case True{}: St.FirewallOn{String.contains(f, "built-in signed software ENABLED"), String.contains(f, "downloaded signed software ENABLED"), String.contains(f, "block all state set to enabled"), String.contains(f, "stealth mode is on")} case False{}: St.FirewallOff{} def firewall_of(+f: String) -> St.Firewall: firewall_on(String.contains(f, "Firewall is enabled"), f) # ---- Dock tiles: system.defaults.dock.persistent-apps / persistent-others ---- # "file:///Applications/Google%20Chrome.app/" -> "/Applications/Google Chrome.app"; bend-net-url decodes. def url_path(+u: String) -> String: +a = Bool.pick(String, String.starts_with(u, "file://"), String.drop(u, 7n), u) +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) Maybe.default(&2, String, Url.pct.decode(b), b) def arrangement(+n: String) -> String: Bool.pick(String, String.eq(n, "2"), "date-added", Bool.pick(String, String.eq(n, "3"), "date-modified", Bool.pick(String, String.eq(n, "4"), "date-created", Bool.pick(String, String.eq(n, "5"), "kind", "name")))) def displayas(+n: String) -> String: Bool.pick(String, String.eq(n, "1"), "folder", "stack") def showas(+n: String) -> String: 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"))) def app_tile(+t: String, +p: String) -> String: Bool.pick(String, String.eq(t, "spacer-tile"), "{ spacer = { small = false; }; }", Bool.pick(String, String.eq(t, "small-spacer-tile"), "{ spacer = { small = true; }; }", Bool.pick(String, String.eq(t, "directory-tile"), "{ folder = " ++ BrO.nix_str(p) ++ "; }", Bool.pick(String, String.ends_with(p, ".app"), "{ app = " ++ BrO.nix_str(p) ++ "; }", "{ file = " ++ BrO.nix_str(p) ++ "; }")))) # A tile from its type, URL, arrangement, displayas and showas (plist fields; the last three for folders). def tile_nix(+key: String, +t: String, +url: String, +arr: String, +disp: String, +show: String) -> String: Bool.pick(String, String.eq(key, "persistent-apps"), app_tile(t, url_path(url)), Bool.pick(String, String.eq(t, "directory-tile"), "{ folder = { path = " ++ BrO.nix_str(url_path(url)) ++ "; arrangement = \"" ++ arrangement(arr) ++ "\"; displayas = \"" ++ displayas(disp) ++ "\"; showas = \"" ++ showas(show) ++ "\"; }; }", "{ file = " ++ BrO.nix_str(url_path(url)) ++ "; }")) def spaced(xs: List<&2, String>) -> String: match xs: case []: "" case x <> rest: " " ++ x ++ spaced(rest) def dock_setting(+key: String, +count: String, tiles: List<&2, String>) -> List<&2, St.Setting>: when_filled(count, nix_setting("system.defaults.dock." ++ key, "[" ++ spaced(tiles) ++ " ]")) # ---- network: nix-darwin gives every known service the same DNS servers and search domains; a Mac where # they differ cannot be declared ---- # `networksetup -listallnetworkservices`: a header line, then the services ("*" marks a disabled one). def services(+out: String) -> List<&2, String>: List.filter(~String, ~(s => Bool.not(String.starts_with(s, "*"))), List.drop(&2, String, filled(out), 1n)) # One service's servers or domains, "" when it has none. def per_service(+out: String) -> String: Bool.pick(String, String.contains(out, "aren't any"), "", String.join(words(out), " ")) def same_for_all(+what: String, +outs: List<&2, String>) -> String: Bool.pick(String, Nat.is_gt(List.length(&2, String, distinct(outs)), 1n), "throw \"V: " ++ what ++ " differ per network service\"", nix_list(words(Maybe.default(&2, String, List.head(&2, String, outs), "")))) # ---- security ---- def is_nix_link(+target: String) -> Bool: String.starts_with(String.trim(target), "/etc/static/") def sudo_keep(+l: String) -> Bool: Bool.not(String.is_empty(String.trim(l)) || String.starts_with(l, "#") || String.contains(l, "env_keep+=TERMINFO")) def sudo_line(l: String) -> Bool: sudo_keep(l) # The sudoers lines nix-darwin writes from security.sudo.extraConfig (not its terminfo lines). def sudo_extra(+sudo: String) -> String: String.join(List.filter(~String, ~sudo_line, String.lines(sudo)), "\n") # ---- keyboard: hidutil's UserKeyMapping ---- # The number after `w =` for each `w` in the words. def after_word(ws: List<&2, String>, +w: String) -> List<&2, String>: match ws: case []: [] case +x <> +rest: 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)) def pairs(src: List<&2, String>, dst: List<&2, String>) -> String: match src: case []: "" case s <> srest: match dst: case []: "" case d <> drest: " { HIDKeyboardModifierMappingSrc = " ++ s ++ "; HIDKeyboardModifierMappingDst = " ++ d ++ "; }" ++ pairs(srest, drest) def keyboard_settings(+km: String) -> List<&2, St.Setting>: +none = String.eq(String.trim(km), "(null)") || String.eq(String.trim(km), "(\n)") || String.is_empty(String.trim(km)) [bool_setting("system.keyboard.enableKeyMapping", Bool.not(none)), nix_setting("system.keyboard.userKeyMapping", "[" ++ pairs(after_word(words(km), "HIDKeyboardModifierMappingSrc"), after_word(words(km), "HIDKeyboardModifierMappingDst")) ++ " ]"), bool_setting("system.keyboard.remapCapsLockToControl", False{}), bool_setting("system.keyboard.remapCapsLockToEscape", False{}), bool_setting("system.keyboard.swapCapsLockAndEscape", False{}), bool_setting("system.keyboard.swapLeftCommandAndLeftAlt", False{}), bool_setting("system.keyboard.swapLeftCtrlAndFn", False{}), bool_setting("system.keyboard.swapRightCommandAndRightOption", False{}), bool_setting("system.keyboard.nonUS.remapTilde", False{})] # ---- system ---- def after_segment(segs: List<&2, String>, +s: String) -> String: match segs: case []: "" case +x <> +rest: Bool.pick(String, String.eq(x, s), String.join(rest, "/"), after_segment(rest, s)) # "/var/db/timezone/zoneinfo/America/Matamoros" -> "America/Matamoros". def time_zone(+link: String) -> String: after_segment(String.split(String.trim(link), '/'), "zoneinfo") def extra_shell_keep(+l: String) -> Bool: 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)) def extra_shell(l: String) -> Bool: extra_shell_keep(l) # /etc/shells beyond macOS's own. def extra_shells(+shells: String) -> List<&2, String>: List.filter(~String, ~extra_shell, filled(shells)) # nvram's "StartupMute %01" (muted); nothing when it is not set. def chime_setting(+mute: String) -> List<&2, St.Setting>: when_filled(mute, bool_setting("system.startup.chime", Bool.not(String.eq(word(mute, 1n), "%01")))) def fonts_setting(fonts: List<&2, String>) -> St.Setting: 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\"")) # ---- users and groups (id >= 500, not system accounts) ---- # `dscl . -list /Users UniqueID` lines "name 501" -> the names. def account(+l: String) -> List<&2, String>: +n = word(l, 0n) 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], []) def accounts_go(ls: List<&2, String>) -> List<&2, String>: match ls: case []: [] case l <> rest: List.append(&2, String, account(l), accounts_go(rest)) def accounts(+out: String) -> List<&2, String>: accounts_go(String.lines(out)) # `dscl . -read Attr` answers "Attr: value" or "Attr:\n value". def dscl_value(+attr: String, +out: String) -> String: +t = String.trim(out) String.trim(String.join(String.lines(Bool.pick(String, String.starts_with(t, attr ++ ":"), String.drop(t, Nat.add(String.length(attr), 1n)), t)), " ")) def user_settings(+n: String, +uid: String, +gid: String, +dir: String, +real: String, +shell: String, +hidden: String) -> List<&2, St.Setting>: +u = "users.users.\"" ++ n ++ "\"." [int_setting(u ++ "uid", uid), int_setting(u ++ "gid", gid), str_setting(u ++ "home", dir), str_setting(u ++ "description", real), nix_setting(u ++ "shell", shell), bool_setting(u ++ "isHidden", String.eq(hidden, "1"))] def group_settings(+n: String, +gid: String, +real: String, +members: String) -> List<&2, St.Setting>: +g = "users.groups.\"" ++ n ++ "\"." [int_setting(g ++ "gid", gid), str_setting(g ++ "description", real), nix_setting(g ++ "members", nix_list(words(members)))] # ---- launchd files other than hidden ones, nix-darwin's own (org.nixos.*), the system's services and Determinate # Nix's; each is named by its content hash, as `./launch/-` ---- def plist_of(labels: List<&2, String>, +f: String) -> Bool: match labels: case []: False{} case l <> rest: String.eq(l ++ ".plist", f) || plist_of(rest, f) def launch_kept(+f: String, +labels: List<&2, String>) -> Bool: Bool.not(String.starts_with(f, ".") || String.starts_with(f, "org.nixos.") || String.starts_with(f, "systems.determinate.") || plist_of(labels, f)) # `shasum -a 256` output -> the setting. def launch_setting(+opt: String, +f: String, +sum: String) -> St.Setting: nix_setting(opt ++ ".\"" ++ f ++ "\".source", "./launch/" ++ String.take(String.trim(sum), 12n) ++ "-" ++ f) # ---- a settings file in V's types, for a system to import ---- def kind_bend(k: St.ValueKind) -> String: match k: case St.KBool{}: "St.KBool{}" case St.KInt{}: "St.KInt{}" case St.KFloat{}: "St.KFloat{}" case St.KStr{}: "St.KStr{}" case St.KNix{}: "St.KNix{}" def setting_bend(s: St.Setting) -> String: match s: case St.Setting{o, k, v}: " St.Setting{" ++ BrO.bend_str(o) ++ ", " ++ kind_bend(k) ++ ", " ++ BrO.bend_str(v) ++ "}" def settings_bend(xs: List<&2, St.Setting>) -> List<&2, String>: match xs: case []: [] case s <> rest: setting_bend(s) <> settings_bend(rest) def bool_bend(b: Bool) -> String: Bool.pick(String, b, "True{}", "False{}") def firewall_bend(f: St.Firewall) -> String: match f: case St.FirewallOff{}: "St.FirewallOff{}" case St.FirewallOn{a, b, c, d}: "St.FirewallOn{" ++ bool_bend(a) ++ ", " ++ bool_bend(b) ++ ", " ++ bool_bend(c) ++ ", " ++ bool_bend(d) ++ "}" # The V package whose types the file names. def v_package() -> String: "near-v-framework@0.3.0.0/domain/architecture/system/deployment/nix-darwin" # Bend source for the Mac's settings, firewall and Homebrew entries in V's types. def import_text(settings: List<&2, St.Setting>, fw: St.Firewall, brews: List<&2, Br.Brew>) -> String: "import Base\nimport " ++ v_package() ++ "/setting/type.bend as St\nimport " ++ v_package() ++ "/brew/type.bend as Br\n\n" ++ "# The Mac as V read it.\n\ndef settings() -> List<&2, St.Setting>:\n [\n" ++ String.join(settings_bend(settings), ",\n") ++ "\n ]\n\n" ++ "# The application firewall: its options exist only while it is on.\ndef firewall() -> St.Firewall:\n " ++ firewall_bend(fw) ++ "\n\n" ++ "def brews() -> List<&2, Br.Brew>:\n" ++ BrO.bend_list(brews) ++ "\n"