Opinionated Android 15+ Linux Terminal Setup
android
linux
command-line-tools
1use owo_colors::OwoColorize;
2use std::{collections::HashMap, fmt};
3
4use crate::config::{Configuration, OhMyPosh, SshConfig};
5
6#[derive(Debug)]
7pub enum Diff {
8 Added(String, String, String), // Parent, child, value
9 Removed(String, String, String), // Parent, child, value
10 Changed(String, String, String, String), // Parent, child, old value, new value
11 Nested(String, Vec<Diff>), // Parent field, nested differences
12}
13
14impl fmt::Display for Diff {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 match self {
17 Diff::Added(parent, child, value) => {
18 if child.is_empty() {
19 write!(f, "+ {}: {}", parent.green(), value.green())
20 } else {
21 write!(
22 f,
23 "+ {}:\n + {}: {}",
24 parent.green(),
25 child.green(),
26 value.green()
27 )
28 }
29 }
30 Diff::Removed(parent, child, value) => {
31 if child.is_empty() {
32 write!(f, "- {}: {}", parent.magenta(), value.magenta())
33 } else {
34 write!(
35 f,
36 "- {}:\n - {}: {}",
37 parent.magenta(),
38 child.magenta(),
39 value.magenta()
40 )
41 }
42 }
43 Diff::Changed(parent, child, old_value, new_value) => {
44 if child.is_empty() {
45 write!(
46 f,
47 "- {}: {}\n+ {}: {}",
48 parent.magenta(),
49 old_value.magenta(),
50 parent.green(),
51 new_value.green()
52 )
53 } else {
54 write!(
55 f,
56 "- {}:\n - {}: {}\n+ {}:\n + {}: {}",
57 parent.magenta(),
58 child.magenta(),
59 old_value.magenta(),
60 parent.green(),
61 child.green(),
62 new_value.green()
63 )
64 }
65 }
66 Diff::Nested(parent, diffs) => {
67 write!(f, "{}:", parent)?;
68 for diff in diffs {
69 write!(f, "\n {}", diff)?;
70 }
71 Ok(())
72 }
73 }
74 }
75}
76
77fn compare_hashmap(
78 parent: &str,
79 old: &Option<HashMap<String, String>>,
80 new: &Option<HashMap<String, String>>,
81) -> Vec<Diff> {
82 let mut diffs = Vec::new();
83 match (old, new) {
84 (None, Some(new_map)) => {
85 for (key, value) in new_map {
86 diffs.push(Diff::Added(parent.to_string(), key.clone(), value.clone()));
87 }
88 }
89 (Some(old_map), None) => {
90 for (key, value) in old_map {
91 diffs.push(Diff::Removed(
92 parent.to_string(),
93 key.clone(),
94 value.clone(),
95 ));
96 }
97 }
98 (Some(old_map), Some(new_map)) => {
99 for (key, old_value) in old_map {
100 match new_map.get(key) {
101 None => diffs.push(Diff::Removed(
102 parent.to_string(),
103 key.clone(),
104 old_value.clone(),
105 )),
106 Some(new_value) if new_value != old_value => {
107 diffs.push(Diff::Changed(
108 parent.to_string(),
109 key.clone(),
110 old_value.clone(),
111 new_value.clone(),
112 ));
113 }
114 _ => {}
115 }
116 }
117 for (key, new_value) in new_map {
118 if !old_map.contains_key(key) {
119 diffs.push(Diff::Added(
120 parent.to_string(),
121 key.clone(),
122 new_value.clone(),
123 ));
124 }
125 }
126 }
127 (None, None) => {}
128 }
129 diffs
130}
131
132fn compare_vec(parent: &str, old: &Option<Vec<String>>, new: &Option<Vec<String>>) -> Vec<Diff> {
133 let mut diffs = Vec::new();
134 match (old, new) {
135 (None, Some(new_vec)) => {
136 for item in new_vec {
137 diffs.push(Diff::Added(
138 parent.to_string(),
139 "".to_string(),
140 item.clone(),
141 ));
142 }
143 }
144 (Some(old_vec), None) => {
145 for item in old_vec {
146 diffs.push(Diff::Removed(
147 parent.to_string(),
148 "".to_string(),
149 item.clone(),
150 ));
151 }
152 }
153 (Some(old_vec), Some(new_vec)) => {
154 let old_set: std::collections::HashSet<_> = old_vec.iter().collect();
155 let new_set: std::collections::HashSet<_> = new_vec.iter().collect();
156 for item in new_set.difference(&old_set) {
157 diffs.push(Diff::Added(
158 parent.to_string(),
159 "".to_string(),
160 item.to_string(),
161 ));
162 }
163 for item in old_set.difference(&new_set) {
164 diffs.push(Diff::Removed(
165 parent.to_string(),
166 "".to_string(),
167 item.to_string(),
168 ));
169 }
170 }
171 (None, None) => {}
172 }
173 diffs
174}
175
176fn compare_bool(parent: &str, old: &Option<bool>, new: &Option<bool>) -> Vec<Diff> {
177 match (old, new) {
178 (None, Some(new_val)) => vec![Diff::Added(
179 parent.to_string(),
180 "".to_string(),
181 new_val.to_string(),
182 )],
183 (Some(old_val), None) => vec![Diff::Removed(
184 parent.to_string(),
185 "".to_string(),
186 old_val.to_string(),
187 )],
188 (Some(old_val), Some(new_val)) if old_val != new_val => {
189 vec![Diff::Changed(
190 parent.to_string(),
191 "".to_string(),
192 old_val.to_string(),
193 new_val.to_string(),
194 )]
195 }
196 _ => vec![],
197 }
198}
199
200fn compare_oh_my_posh(old: &Option<OhMyPosh>, new: &Option<OhMyPosh>) -> Vec<Diff> {
201 let mut diffs = Vec::new();
202 match (old, new) {
203 (None, Some(new_omp)) => {
204 if let Some(theme) = &new_omp.theme {
205 diffs.push(Diff::Added(
206 "oh_my_posh".to_string(),
207 "theme".to_string(),
208 theme.clone(),
209 ));
210 }
211 }
212 (Some(old_omp), None) => {
213 if let Some(theme) = &old_omp.theme {
214 diffs.push(Diff::Removed(
215 "oh_my_posh".to_string(),
216 "theme".to_string(),
217 theme.clone(),
218 ));
219 }
220 }
221 (Some(old_omp), Some(new_omp)) => match (&old_omp.theme, &new_omp.theme) {
222 (None, Some(new_theme)) => {
223 diffs.push(Diff::Added(
224 "oh_my_posh".to_string(),
225 "theme".to_string(),
226 new_theme.clone(),
227 ));
228 }
229 (Some(old_theme), None) => {
230 diffs.push(Diff::Removed(
231 "oh_my_posh".to_string(),
232 "theme".to_string(),
233 old_theme.clone(),
234 ));
235 }
236 (Some(old_theme), Some(new_theme)) if old_theme != new_theme => {
237 diffs.push(Diff::Changed(
238 "oh_my_posh".to_string(),
239 "theme".to_string(),
240 old_theme.clone(),
241 new_theme.clone(),
242 ));
243 }
244 _ => {}
245 },
246 (None, None) => {}
247 }
248 if !diffs.is_empty() {
249 vec![Diff::Nested("oh_my_posh".to_string(), diffs)]
250 } else {
251 vec![]
252 }
253}
254
255fn compare_ssh_config(old: &Option<SshConfig>, new: &Option<SshConfig>) -> Vec<Diff> {
256 let mut diffs = Vec::new();
257 match (old, new) {
258 (None, Some(new_ssh)) => {
259 if let Some(port) = new_ssh.port {
260 diffs.push(Diff::Added(
261 "ssh".to_string(),
262 "port".to_string(),
263 port.to_string(),
264 ));
265 }
266 if let Some(keys) = &new_ssh.authorized_keys {
267 for key in keys {
268 diffs.push(Diff::Added(
269 "ssh".to_string(),
270 "authorized_keys".to_string(),
271 key.clone(),
272 ));
273 }
274 }
275 }
276 (Some(old_ssh), None) => {
277 if let Some(port) = old_ssh.port {
278 diffs.push(Diff::Removed(
279 "ssh".to_string(),
280 "port".to_string(),
281 port.to_string(),
282 ));
283 }
284 if let Some(keys) = &old_ssh.authorized_keys {
285 for key in keys {
286 diffs.push(Diff::Removed(
287 "ssh".to_string(),
288 "authorized_keys".to_string(),
289 key.clone(),
290 ));
291 }
292 }
293 }
294 (Some(old_ssh), Some(new_ssh)) => {
295 match (old_ssh.port, new_ssh.port) {
296 (None, Some(new_port)) => {
297 diffs.push(Diff::Added(
298 "ssh".to_string(),
299 "port".to_string(),
300 new_port.to_string(),
301 ));
302 }
303 (Some(old_port), None) => {
304 diffs.push(Diff::Removed(
305 "ssh".to_string(),
306 "port".to_string(),
307 old_port.to_string(),
308 ));
309 }
310 (Some(old_port), Some(new_port)) if old_port != new_port => {
311 diffs.push(Diff::Changed(
312 "ssh".to_string(),
313 "port".to_string(),
314 old_port.to_string(),
315 new_port.to_string(),
316 ));
317 }
318 _ => {}
319 }
320 let key_diffs = compare_vec("ssh", &old_ssh.authorized_keys, &new_ssh.authorized_keys);
321 diffs.extend(key_diffs.into_iter().map(|diff| match diff {
322 Diff::Added(_, _, value) => {
323 Diff::Added("ssh".to_string(), "authorized_keys".to_string(), value)
324 }
325 Diff::Removed(_, _, value) => {
326 Diff::Removed("ssh".to_string(), "authorized_keys".to_string(), value)
327 }
328 Diff::Changed(_, _, old_value, new_value) => Diff::Changed(
329 "ssh".to_string(),
330 "authorized_keys".to_string(),
331 old_value,
332 new_value,
333 ),
334 Diff::Nested(_, _) => diff, // Unreachable
335 }));
336 }
337 (None, None) => {}
338 }
339 if !diffs.is_empty() {
340 vec![Diff::Nested("ssh".to_string(), diffs)]
341 } else {
342 vec![]
343 }
344}
345
346pub fn compare_configurations(old: &Configuration, new: &Configuration) -> Vec<Diff> {
347 let mut diffs = Vec::new();
348
349 diffs.extend(compare_hashmap("stow", &old.stow, &new.stow));
350 diffs.extend(compare_hashmap("mise", &old.mise, &new.mise));
351 diffs.extend(compare_hashmap("nix", &old.nix, &new.nix));
352 diffs.extend(compare_hashmap("pkgx", &old.pkgx, &new.pkgx));
353 diffs.extend(compare_hashmap("curl", &old.curl, &new.curl));
354 diffs.extend(compare_hashmap("alias", &old.alias, &new.alias));
355 diffs.extend(compare_hashmap("npm", &old.npm, &new.npm));
356
357 diffs.extend(compare_vec("apt-get", &old.apt_get, &new.apt_get));
358
359 diffs.extend(compare_bool("blesh", &old.blesh, &new.blesh));
360 diffs.extend(compare_bool("zoxide", &old.zoxide, &new.zoxide));
361 diffs.extend(compare_bool("tailscale", &old.tailscale, &new.tailscale));
362 diffs.extend(compare_bool("neofetch", &old.neofetch, &new.neofetch));
363 diffs.extend(compare_bool("doppler", &old.doppler, &new.doppler));
364
365 diffs.extend(compare_oh_my_posh(&old.oh_my_posh, &new.oh_my_posh));
366 diffs.extend(compare_ssh_config(&old.ssh, &new.ssh));
367
368 diffs
369}