1use alloc::string::String;
42use alloc::vec::Vec;
43use core::marker::PhantomData;
44
45use crate::ini::Ini;
46use crate::num::{f64_format, f64_parse, parse_i64};
47
48pub const KF_HIDDEN: u32 = 1;
50pub const KF_RESTART: u32 = 2;
52
53#[derive(Clone, Copy, Debug)]
55#[non_exhaustive]
56pub enum Kind {
57 Bool(bool),
58 Int { def: i64, min: i64, max: i64, step: i64 },
59 Num { def: f64, min: f64, max: f64, dec: u32 },
60 Text { def: &'static str, max: usize },
61 Choice { def: usize, items: &'static [(&'static str, &'static str)] },
62 Color(u32),
63}
64
65#[derive(Clone, Copy, Debug)]
67pub struct Desc {
68 pub name: &'static str,
69 pub label: &'static str,
70 pub help: &'static str,
71 pub flags: u32,
72 pub kind: Kind,
73}
74
75impl Desc {
76 pub fn split(&self) -> (&'static str, &'static str) {
78 split(self.name)
79 }
80
81 pub fn default_val(&self) -> Val {
83 match self.kind {
84 Kind::Bool(d) => Val::Bool(d),
85 Kind::Int { def, .. } => Val::Int(def),
86 Kind::Num { def, .. } => Val::Num(def),
87 Kind::Text { def, .. } => Val::Text(String::from(def)),
88 Kind::Choice { def, .. } => Val::Choice(def),
89 Kind::Color(d) => Val::Color(d & 0xFF_FFFF),
90 }
91 }
92
93 pub fn fit(&self, v: Val) -> Val {
95 match (self.kind, v) {
96 (Kind::Bool(_), Val::Bool(b)) => Val::Bool(b),
97 (Kind::Int { min, max, .. }, Val::Int(x)) => Val::Int(x.clamp(min, max.max(min))),
98 (Kind::Num { min, max, .. }, Val::Num(x)) if !x.is_nan() && !min.is_nan() && !max.is_nan() => Val::Num(x.clamp(min, max.max(min))),
99 (Kind::Text { max, .. }, Val::Text(mut s)) => {
100 cut(&mut s, max);
101 Val::Text(s)
102 }
103 (Kind::Choice { items, .. }, Val::Choice(i)) if i < items.len() => Val::Choice(i),
104 (Kind::Color(_), Val::Color(c)) => Val::Color(c & 0xFF_FFFF),
105 _ => self.default_val(),
106 }
107 }
108
109 pub fn format(&self, v: &Val) -> String {
111 match v {
112 Val::Bool(b) => String::from(if *b { "true" } else { "false" }),
113 Val::Int(x) => alloc::format!("{}", x),
114 Val::Num(x) => f64_format(*x),
115 Val::Text(s) => s.clone(),
116 Val::Choice(i) => match self.kind {
117 Kind::Choice { items, .. } => String::from(items.get(*i).map_or("", |it| it.0)),
118 _ => String::new(),
119 },
120 Val::Color(c) => alloc::format!("#{:06X}", c & 0xFF_FFFF),
121 }
122 }
123
124 pub fn parse(&self, s: &str) -> Option<Val> {
126 let t = s.trim();
127 let v = match self.kind {
128 Kind::Bool(_) => Val::Bool(parse_i64(t.as_bytes())? != 0),
129 Kind::Int { .. } => Val::Int(parse_i64(t.as_bytes())?),
130 Kind::Num { .. } => {
131 let (x, used) = f64_parse(t.as_bytes());
132 if used == 0 || used != t.len() || x.is_nan() {
133 return None;
134 }
135 Val::Num(x)
136 }
137 Kind::Text { .. } => Val::Text(String::from(s)),
138 Kind::Choice { items, .. } => Val::Choice(items.iter().position(|it| it.0.eq_ignore_ascii_case(t))?),
139 Kind::Color(_) => {
140 let h = t.strip_prefix('#').unwrap_or(t);
141 if h.len() != 6 || !h.bytes().all(|b| b.is_ascii_hexdigit()) {
142 return None;
143 }
144 Val::Color(u32::from_str_radix(h, 16).ok()?)
145 }
146 };
147 Some(self.fit(v))
148 }
149}
150
151#[derive(Clone, Debug, PartialEq)]
153#[non_exhaustive]
154pub enum Val {
155 Bool(bool),
156 Int(i64),
157 Num(f64),
158 Text(String),
159 Choice(usize),
160 Color(u32),
161}
162
163pub trait Value: Sized {
165 #[doc(hidden)]
166 fn from_val(v: &Val) -> Option<Self>;
167 #[doc(hidden)]
168 fn into_val(self) -> Val;
169}
170
171impl Value for bool {
172 fn from_val(v: &Val) -> Option<bool> {
173 if let Val::Bool(b) = v { Some(*b) } else { None }
174 }
175 fn into_val(self) -> Val {
176 Val::Bool(self)
177 }
178}
179
180impl Value for i64 {
181 fn from_val(v: &Val) -> Option<i64> {
182 if let Val::Int(x) = v { Some(*x) } else { None }
183 }
184 fn into_val(self) -> Val {
185 Val::Int(self)
186 }
187}
188
189impl Value for f64 {
190 fn from_val(v: &Val) -> Option<f64> {
191 if let Val::Num(x) = v { Some(*x) } else { None }
192 }
193 fn into_val(self) -> Val {
194 Val::Num(self)
195 }
196}
197
198impl Value for String {
199 fn from_val(v: &Val) -> Option<String> {
200 if let Val::Text(s) = v { Some(s.clone()) } else { None }
201 }
202 fn into_val(self) -> Val {
203 Val::Text(self)
204 }
205}
206
207impl Value for usize {
208 fn from_val(v: &Val) -> Option<usize> {
209 if let Val::Choice(i) = v { Some(*i) } else { None }
210 }
211 fn into_val(self) -> Val {
212 Val::Choice(self)
213 }
214}
215
216impl Value for u32 {
217 fn from_val(v: &Val) -> Option<u32> {
218 if let Val::Color(c) = v { Some(*c) } else { None }
219 }
220 fn into_val(self) -> Val {
221 Val::Color(self)
222 }
223}
224
225pub struct Key<T> {
227 pub d: Desc,
228 t: PhantomData<fn() -> T>,
229}
230
231impl<T> Clone for Key<T> {
232 fn clone(&self) -> Key<T> {
233 *self
234 }
235}
236impl<T> Copy for Key<T> {}
237
238const fn key<T>(name: &'static str, kind: Kind) -> Key<T> {
239 Key { d: Desc { name, label: name, help: "", flags: 0, kind }, t: PhantomData }
240}
241
242impl Key<bool> {
243 pub const fn bool(name: &'static str, def: bool) -> Key<bool> {
244 key(name, Kind::Bool(def))
245 }
246}
247
248impl Key<i64> {
249 pub const fn int(name: &'static str, def: i64, min: i64, max: i64) -> Key<i64> {
251 key(name, Kind::Int { def, min, max, step: 1 })
252 }
253
254 pub const fn step(self, step: i64) -> Key<i64> {
256 let mut k = self;
257 if let Kind::Int { def, min, max, .. } = k.d.kind {
258 k.d.kind = Kind::Int { def, min, max, step };
259 }
260 k
261 }
262}
263
264impl Key<f64> {
265 pub const fn num(name: &'static str, def: f64, min: f64, max: f64, dec: u32) -> Key<f64> {
267 key(name, Kind::Num { def, min, max, dec })
268 }
269}
270
271impl Key<String> {
272 pub const fn text(name: &'static str, def: &'static str, max: usize) -> Key<String> {
274 key(name, Kind::Text { def, max })
275 }
276}
277
278impl Key<usize> {
279 pub const fn choice(name: &'static str, def: usize, items: &'static [(&'static str, &'static str)]) -> Key<usize> {
281 key(name, Kind::Choice { def, items })
282 }
283}
284
285impl Key<u32> {
286 pub const fn color(name: &'static str, def: u32) -> Key<u32> {
288 key(name, Kind::Color(def))
289 }
290}
291
292impl<T> Key<T> {
293 pub const fn label(self, label: &'static str) -> Key<T> {
295 let mut k = self;
296 k.d.label = label;
297 k
298 }
299
300 pub const fn help(self, help: &'static str) -> Key<T> {
302 let mut k = self;
303 k.d.help = help;
304 k
305 }
306
307 pub const fn hidden(self) -> Key<T> {
309 let mut k = self;
310 k.d.flags |= KF_HIDDEN;
311 k
312 }
313
314 pub const fn restart(self) -> Key<T> {
316 let mut k = self;
317 k.d.flags |= KF_RESTART;
318 k
319 }
320}
321
322pub struct Schema {
325 pub version: u32,
326 pub keys: &'static [Desc],
327 pub sections: &'static [(&'static str, &'static str)],
330 pub migrate: Option<fn(u32, &mut Ini)>,
332}
333
334impl Schema {
335 pub const fn new(version: u32, keys: &'static [Desc]) -> Schema {
336 Schema { version, keys, sections: &[], migrate: None }
337 }
338
339 pub const fn sections(self, sections: &'static [(&'static str, &'static str)]) -> Schema {
340 Schema { sections, ..self }
341 }
342
343 pub const fn migrate(self, f: fn(u32, &mut Ini)) -> Schema {
344 Schema { migrate: Some(f), ..self }
345 }
346
347 pub fn index(&self, name: &str) -> Option<usize> {
349 self.keys.iter().position(|d| d.name == name)
350 }
351}
352
353pub fn split(name: &str) -> (&str, &str) {
355 name.split_once('.').unwrap_or(("", name))
356}
357
358pub fn rename(ini: &mut Ini, old: &str, new: &str) -> bool {
360 let (os, ok) = split(old);
361 let Some(v) = ini.get(os, ok).map(String::from) else { return false };
362 let (ns, nk) = split(new);
363 if !ini.set(ns, nk, &v) {
364 return false;
365 }
366 if (os, ok) != (ns, nk) {
367 ini.remove(os, ok);
368 }
369 true
370}
371
372fn cut(s: &mut String, max: usize) {
374 if s.len() > max {
375 let mut n = max;
376 while !s.is_char_boundary(n) {
377 n -= 1;
378 }
379 s.truncate(n);
380 }
381}
382
383pub struct Settings {
385 schema: &'static Schema,
386 path: String,
387 ini: Ini,
388 vals: Vec<Val>,
389 dirty: bool,
390 rev: u32,
391 file_version: u32,
392 unreadable: bool, }
394
395impl Settings {
396 pub fn open(path: &str, schema: &'static Schema) -> Settings {
398 let exists = plat::file_exists(path) == 1;
399 let loaded = Ini::load_checked(path);
400 let unreadable = loaded.is_none();
401 let mut s = Settings::from_ini_ex(loaded.unwrap_or_else(Ini::new), schema, exists && !unreadable);
402 s.path = String::from(path);
403 s.unreadable = unreadable;
404 if unreadable {
405 plat::log_warn("settings", &alloc::format!("could not read {}, it is not overwritten", path));
406 }
407 s
408 }
409
410 pub fn new(schema: &'static Schema) -> Settings {
412 Settings::from_ini_ex(Ini::new(), schema, false)
413 }
414
415 pub fn from_ini(ini: Ini, schema: &'static Schema) -> Settings {
417 Settings::from_ini_ex(ini, schema, true)
418 }
419
420 fn from_ini_ex(mut ini: Ini, schema: &'static Schema, existed: bool) -> Settings {
421 let mut dirty = false;
422 let file_version = ini.get_int("", "version", 0).clamp(0, u32::MAX as i64) as u32;
423 if existed && file_version < schema.version {
424 if let Some(m) = schema.migrate {
425 m(file_version, &mut ini);
426 }
427 ini.set_int("", "version", schema.version as i64);
428 dirty = true;
429 plat::log_info("settings", &alloc::format!("migrated from version {} to {}", file_version, schema.version));
430 } else if file_version > schema.version {
431 plat::log_warn("settings", &alloc::format!("the file has version {}, the program knows {}", file_version, schema.version));
432 }
433 let vals = schema.keys.iter().map(|d| read(&ini, d)).collect();
434 Settings { schema, path: String::new(), ini, vals, dirty, rev: 0, file_version, unreadable: false }
435 }
436
437 pub fn schema(&self) -> &'static Schema {
438 self.schema
439 }
440
441 pub fn path(&self) -> &str {
443 &self.path
444 }
445
446 pub fn file_version(&self) -> u32 {
448 self.file_version
449 }
450
451 pub fn get<T: Value>(&self, k: &Key<T>) -> T {
453 let v = match self.schema.index(k.d.name) {
454 Some(i) => T::from_val(&self.vals[i]),
455 None => T::from_val(&read(&self.ini, &k.d)),
456 };
457 v.or_else(|| T::from_val(&k.d.default_val())).unwrap_or_else(|| {
458 plat::log_warn("settings", &alloc::format!("{}: the key has another type than the schema's", k.d.name));
459 T::from_val(&Val::Bool(false)).or_else(|| T::from_val(&Val::Int(0))).or_else(|| T::from_val(&Val::Num(0.0))).or_else(|| T::from_val(&Val::Text(String::new()))).or_else(|| T::from_val(&Val::Choice(0))).or_else(|| T::from_val(&Val::Color(0))).expect("settings: the type of a key")
460 })
461 }
462
463 pub fn text(&self, k: &Key<String>) -> &str {
465 match self.schema.index(k.d.name).map(|i| &self.vals[i]) {
466 Some(Val::Text(s)) => s,
467 _ => {
468 let (s, n) = k.d.split();
469 match self.ini.get(s, n) {
470 Some(t) => t,
471 None => match k.d.kind {
472 Kind::Text { def, .. } => def,
473 _ => "",
474 },
475 }
476 }
477 }
478 }
479
480 pub fn set<T: Value>(&mut self, k: &Key<T>, v: T) -> bool {
482 match self.schema.index(k.d.name) {
483 Some(i) => self.set_at(i, v.into_val()),
484 None => {
485 let v = k.d.fit(v.into_val());
486 if read(&self.ini, &k.d) == v {
487 return false;
488 }
489 write(&mut self.ini, &k.d, &v);
490 self.changed();
491 true
492 }
493 }
494 }
495
496 pub fn reset<T>(&mut self, k: &Key<T>) -> bool {
498 match self.schema.index(k.d.name) {
499 Some(i) => self.set_at(i, k.d.default_val()),
500 None => {
501 let (s, n) = k.d.split();
502 let gone = self.ini.remove(s, n);
503 if gone {
504 self.changed();
505 }
506 gone
507 }
508 }
509 }
510
511 pub fn reset_all(&mut self) -> bool {
513 let mut any = false;
514 for i in 0..self.schema.keys.len() {
515 any |= self.set_at(i, self.schema.keys[i].default_val());
516 }
517 any
518 }
519
520 pub fn is_default<T>(&self, k: &Key<T>) -> bool {
522 match self.schema.index(k.d.name) {
523 Some(i) => self.vals[i] == self.default_at(i),
524 None => read(&self.ini, &k.d) == k.d.default_val(),
525 }
526 }
527
528 pub fn at(&self, i: usize) -> &Val {
530 &self.vals[i]
531 }
532
533 pub fn default_at(&self, i: usize) -> Val {
535 self.schema.keys[i].default_val()
536 }
537
538 pub fn set_at(&mut self, i: usize, v: Val) -> bool {
540 let d = &self.schema.keys[i];
541 let v = d.fit(v);
542 if self.vals[i] == v {
543 return false;
544 }
545 write(&mut self.ini, d, &v);
546 self.vals[i] = v;
547 self.changed();
548 true
549 }
550
551 fn changed(&mut self) {
552 self.dirty = true;
553 self.rev = self.rev.wrapping_add(1);
554 }
555
556 pub fn dirty(&self) -> bool {
558 self.dirty
559 }
560
561 pub fn revision(&self) -> u32 {
563 self.rev
564 }
565
566 pub fn ini(&self) -> &Ini {
568 &self.ini
569 }
570
571 pub fn ini_mut(&mut self) -> &mut Ini {
573 self.changed();
574 &mut self.ini
575 }
576
577 pub fn save(&mut self) -> bool {
579 if !self.dirty {
580 return true;
581 }
582 if self.path.is_empty() {
583 self.dirty = false;
584 return true;
585 }
586 if self.unreadable {
587 return false;
588 }
589 if self.ini.get("", "version").is_none() || self.file_version < self.schema.version {
590 self.ini.set_int("", "version", self.schema.version.max(self.file_version) as i64);
591 }
592 let ok = self.ini.save(&self.path);
593 if ok {
594 self.dirty = false;
595 } else {
596 plat::log_warn("settings", &alloc::format!("could not write {}", self.path));
597 }
598 ok
599 }
600}
601
602fn read(ini: &Ini, d: &Desc) -> Val {
604 let (s, n) = d.split();
605 match ini.get(s, n) {
606 None => d.default_val(),
607 Some(t) => match d.parse(t) {
608 Some(v) => v,
609 None => {
610 plat::log_warn("settings", &alloc::format!("{}: \"{}\" is not a valid value, the default is used", d.name, t));
611 d.default_val()
612 }
613 },
614 }
615}
616
617fn write(ini: &mut Ini, d: &Desc, v: &Val) {
619 let (s, n) = d.split();
620 if *v == d.default_val() {
621 ini.remove(s, n);
622 } else {
623 ini.set(s, n, &d.format(v));
624 }
625}