defaults_rs/preferences/
mod.rs1mod convert;
11pub mod types;
12
13use anyhow::{Context, Result, bail};
14use std::{
15 collections::{HashMap, HashSet},
16 fs::{self, File},
17 io::Cursor,
18 path::PathBuf,
19};
20
21use crate::{
22 Domain, FindMatch, PrefValue,
23 preferences::convert::{plist_to_prefvalue, prefvalue_to_plist},
24};
25use plist::Value;
26
27use crate::core::foundation;
29
30pub struct Preferences;
33
34impl Preferences {
35 pub fn list_domains() -> Result<HashSet<Domain>> {
37 let list = foundation::list_domains()?;
38
39 let domains: HashSet<Domain> = list.iter().map(|f| Domain::User(f.to_string())).collect();
40 Ok(domains)
41 }
42
43 pub fn find(word: &str) -> Result<HashMap<Domain, Vec<FindMatch>>> {
45 let word_lower = word.to_lowercase();
46 let mut results: std::collections::HashMap<Domain, Vec<FindMatch>> =
47 std::collections::HashMap::new();
48
49 let domains: Vec<Domain> = Self::list_domains()?
50 .into_iter()
51 .chain([Domain::Global])
52 .collect();
53
54 for domain in domains {
55 let loaded = foundation::read_pref_domain(&domain.to_string())?;
56 let mut matches = Vec::new();
57
58 Self::find_in_value(&loaded, &word_lower, String::new(), &mut matches);
59 if !matches.is_empty() {
60 results.insert(domain, matches);
61 }
62 }
63 Ok(results)
64 }
65
66 fn find_in_value(
68 val: &PrefValue,
69 word_lower: &str,
70 key_path: String,
71 matches: &mut Vec<FindMatch>,
72 ) {
73 fn contains_word(haystack: &str, needle: &str) -> bool {
74 haystack.to_lowercase().contains(needle)
75 }
76 match val {
77 PrefValue::Dictionary(dict) => {
78 for (k, v) in dict {
79 let new_key_path = if key_path.is_empty() {
80 k.clone()
81 } else {
82 format!("{key_path}.{k}")
83 };
84 if contains_word(k, word_lower) {
85 matches.push(FindMatch {
86 key: new_key_path.clone(),
87 value: v.clone(),
88 });
89 }
90 Self::find_in_value(v, word_lower, new_key_path, matches);
91 }
92 }
93 PrefValue::Array(arr) => {
94 for (i, v) in arr.iter().enumerate() {
95 let new_key_path = format!("{key_path}[{i}]");
96 Self::find_in_value(v, word_lower, new_key_path, matches);
97 }
98 }
99 _ => {
100 if contains_word(&val.to_string(), word_lower) {
101 matches.push(FindMatch {
102 key: key_path.clone(),
103 value: val.clone(),
104 });
105 }
106 }
107 }
108 }
109
110 pub fn read(domain: Domain, key: &str) -> Result<PrefValue> {
112 let cf_name = &domain.get_cf_name();
113 foundation::read_pref(cf_name, key)
114 }
115
116 pub fn read_domain(domain: Domain) -> Result<PrefValue> {
118 let cf_name = &domain.get_cf_name();
119 foundation::read_pref_domain(cf_name)
120 }
121
122 pub fn write(domain: Domain, key: &str, value: PrefValue) -> Result<()> {
127 let cf_name = &domain.get_cf_name();
128 foundation::write_pref(cf_name, key, &value)?;
129
130 Ok(())
131 }
132
133 pub fn delete(domain: Domain, key: &str) -> Result<()> {
135 let cf_name = &domain.get_cf_name();
136 foundation::delete_key(cf_name, key)
137 }
138
139 pub fn delete_domain(domain: Domain) -> Result<()> {
141 let cf_name = &domain.get_cf_name();
142 foundation::delete_domain(cf_name)
143 }
144
145 pub fn read_type(domain: Domain, key: &str) -> Result<String> {
149 let cf_name = domain.get_cf_name();
150 let loaded = foundation::read_pref(&cf_name, key)?;
151
152 Ok(loaded.get_type().to_string())
153 }
154
155 pub fn rename(domain: Domain, old_key: &str, new_key: &str) -> Result<()> {
159 let cf_name = &domain.get_cf_name();
160
161 let val = foundation::read_pref(cf_name, old_key)?;
163
164 foundation::write_pref(cf_name, new_key, &val)?;
165 foundation::delete_key(cf_name, old_key)?;
166
167 Ok(())
168 }
169
170 pub fn import(domain: Domain, import_path: &str) -> Result<()> {
174 let data = fs::read(import_path)?;
175
176 let plist_val = Value::from_reader(Cursor::new(&data))?;
177
178 let dict = match plist_val {
179 Value::Dictionary(d) => d,
180 _ => {
181 bail!("Import must be a dictionary at root.")
182 }
183 };
184
185 let cf_name = &domain.get_cf_name();
186 for (k, v) in dict {
187 let pv = plist_to_prefvalue(&v)?;
188 foundation::write_pref(cf_name, &k, &pv)?;
189 }
190 Ok(())
191 }
192
193 pub fn export(domain: Domain, export_path: &str) -> Result<()> {
195 let cf_name = &domain.get_cf_name();
196 let pref = foundation::read_pref_domain(cf_name)?;
197
198 if !matches!(pref, PrefValue::Dictionary(_)) {
199 bail!("CF export produced non-dictionary root")
200 }
201
202 let plist = prefvalue_to_plist(&pref);
203 let path = PathBuf::from(export_path);
204
205 let file = File::create(path)?;
206 plist
207 .to_writer_binary(file)
208 .context("failed to export CF domain to plist")?;
209
210 Ok(())
211 }
212}