cli/vimport.bend

near-v-framework@0.1.0.0 · 321 lines · 0 definitions

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