1use std::collections::BTreeMap;
6#[cfg(not(target_arch = "wasm32"))]
7use std::path::PathBuf;
8
9use crate::Settings;
10
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct TextSettings {
14 values: BTreeMap<String, String>,
15}
16
17impl TextSettings {
18 pub fn parse(text: &str) -> TextSettings {
20 let mut s = TextSettings::default();
21 for line in text.lines() {
22 if let Some((k, v)) = line.split_once('=') {
23 s.values.insert(k.trim().to_string(), v.trim().to_string());
24 }
25 }
26 s
27 }
28
29 pub fn serialize(&self) -> String {
31 self.values
32 .iter()
33 .map(|(k, v)| format!("{k}={v}\n"))
34 .collect()
35 }
36
37 pub fn get_text(&self, section: &str, name: &str) -> Option<&str> {
39 self.values
40 .get(&format!("{section}{name}"))
41 .map(String::as_str)
42 .filter(|v| !v.is_empty())
43 }
44
45 pub fn set_text(&mut self, section: &str, name: &str, value: &str) {
47 self.values
48 .insert(format!("{section}{name}"), value.to_string());
49 }
50
51 pub fn with_prefix<'a>(&'a self, prefix: &'a str) -> impl Iterator<Item = (&'a str, &'a str)> {
53 self.values
54 .range(prefix.to_string()..)
55 .take_while(move |(k, _)| k.starts_with(prefix))
56 .map(move |(k, v)| (&k[prefix.len()..], v.as_str()))
57 }
58
59 #[cfg(not(target_arch = "wasm32"))]
61 pub fn load(path: &std::path::Path) -> std::io::Result<TextSettings> {
62 match std::fs::read_to_string(path) {
63 Ok(text) => Ok(TextSettings::parse(&text)),
64 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(TextSettings::default()),
65 Err(e) => Err(e),
66 }
67 }
68
69 #[cfg(not(target_arch = "wasm32"))]
72 pub fn save(&self, path: &std::path::Path) -> std::io::Result<()> {
73 if let Some(dir) = path.parent() {
74 std::fs::create_dir_all(dir)?;
75 }
76 let temp = path.with_extension("txt.new");
77 std::fs::write(&temp, self.serialize())?;
78 std::fs::rename(&temp, path)
79 }
80}
81
82impl Settings for TextSettings {
83 fn get_bool(&self, section: &str, name: &str, value: &mut bool) {
84 if let Some(v) = self.values.get(&format!("{section}{name}")) {
85 *value = v == "true";
86 }
87 }
88
89 fn get_int(&self, section: &str, name: &str, value: &mut i32) {
90 if let Some(n) = self
91 .values
92 .get(&format!("{section}{name}"))
93 .and_then(|v| v.parse().ok())
94 {
95 *value = n;
96 }
97 }
98
99 fn set_bool(&mut self, section: &str, name: &str, value: bool) {
100 self.set_text(section, name, if value { "true" } else { "false" });
101 }
102
103 fn set_int(&mut self, section: &str, name: &str, value: i32) {
104 self.set_text(section, name, &value.to_string());
105 }
106}
107
108#[cfg(not(target_arch = "wasm32"))]
111pub fn settings_path() -> Option<PathBuf> {
112 if let Ok(p) = std::env::var("BANISHED_SETTINGS") {
113 return (!p.is_empty()).then(|| PathBuf::from(p));
114 }
115 let base = if cfg!(target_os = "windows") {
116 std::env::var_os("APPDATA").map(PathBuf::from)
117 } else if cfg!(target_os = "macos") {
118 std::env::var_os("HOME").map(|h| PathBuf::from(h).join("Library/Application Support"))
119 } else {
120 std::env::var_os("XDG_CONFIG_HOME")
121 .map(PathBuf::from)
122 .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
123 }?;
124 Some(base.join("banished-wasm").join("settings.txt"))
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn keeps_what_it_does_not_set() {
133 let mut s = TextSettings::parse("VideoVSync=true\nModEnabled_apiary=false\n");
134 s.set_bool("ModEnabled_", "apiary", true);
135 s.set_int("ModIndex_", "apiary", 2);
136 assert_eq!(
137 s.serialize(),
138 "ModEnabled_apiary=true\nModIndex_apiary=2\nVideoVSync=true\n"
139 );
140 let mut on = false;
141 s.get_bool("ModEnabled_", "apiary", &mut on);
142 assert!(on);
143 let found: Vec<_> = s.with_prefix("Mod").collect();
144 assert_eq!(found.len(), 2);
145 }
146}