1use std::{
2 collections::BTreeMap,
3 ffi::OsStr,
4 fs::{create_dir_all, File},
5 io::{BufRead, BufReader, BufWriter, LineWriter, Write},
6 path::{Path, PathBuf},
7 sync::{
8 atomic::{AtomicBool, Ordering},
9 Arc,
10 },
11 thread::{sleep, spawn},
12 time::Duration,
13};
14
15pub use const_str::replace;
16use parking_lot::Mutex;
17use rmw_str::Str;
18
19type Map = Arc<Mutex<BTreeMap<Box<[u8]>, Box<[u8]>>>>;
20
21#[derive(Debug)]
22pub struct Config {
23 map: Map,
24 run: Arc<AtomicBool>,
25 env: PathBuf,
26 prefix: Box<str>,
27}
28
29const ENV: &str = "env";
30
31fn line_key(line: &str) -> Option<(&str, usize)> {
32 let t = line.trim_start();
33 if !t.starts_with('#') {
34 if let Some(pos) = t.find('=') {
35 return Some((t[..pos].trim_end(), pos + 1));
36 }
37 }
38 None
39}
40
41fn save(path: impl AsRef<Path>, map: &mut BTreeMap<Box<[u8]>, Box<[u8]>>) {
42 if map.is_empty() {
43 return;
44 }
45
46 let mut li = vec![];
47
48 macro_rules! push {
49 ($k:expr,$v:expr) => {
50 li.push([&$k, &b" = "[..], &$v, &[b'\n'][..]].concat());
51 };
52 }
53
54 if let Ok(env) = err::ok!(File::open(&path)) {
55 for line in BufReader::new(env).lines().flatten() {
56 if let Some((k, _)) = line_key(&line) {
57 let k = k.as_bytes();
58 if let Some(v) = map.get(k) {
59 push!(k, v);
60 map.remove(k);
61 continue;
62 }
63 }
64 let mut line = line.as_bytes().to_vec();
65 line.push(b'\n');
66 li.push(line);
67 }
68 for (k, v) in map.iter() {
69 push!(k, v);
70 }
71 }
72
73 *map = BTreeMap::new();
74
75 if let Ok(file) = err::ok!(File::create(path)) {
76 let mut w = LineWriter::new(BufWriter::new(file));
77 for i in li {
78 err::log!(w.write_all(&i));
79 }
80 }
81}
82
83impl Config {
84 pub fn new(prefix: Box<str>) -> Self {
85 let root = &env_dir::home(&prefix);
86 let env = root.join(ENV);
87 if err::ok!(create_dir_all(root)).is_ok() {
88 if env.exists() {
89 if let Ok(env) = err::ok!(File::open(&env)) {
90 for line in BufReader::new(env).lines().flatten() {
91 if let Some((key, pos)) = line_key(&line) {
92 let key = format!("{}_{}", prefix, key);
93 std::env::set_var(key, line[pos..].trim());
94 }
95 }
96 }
97 } else {
98 err::log!(File::create(&env));
99 }
100 }
101
102 Self {
103 run: Arc::new(AtomicBool::new(false)),
104 map: Map::default(),
105 env,
106 prefix,
107 }
108 }
109
110 pub fn get<T: Str>(&self, key: impl AsRef<str>, init: impl Fn() -> T) -> T {
111 let key_ref = key.as_ref();
112 let key = format!("{}_{}", self.prefix, key_ref);
113
114 self._get(&key, || {
115 let r = init();
116 let mut map = self.map.lock();
117
118 let val = r.encode();
119 std::env::set_var(&key, unsafe { &std::str::from_utf8_unchecked(&val) });
120 map.insert(Box::from(key_ref.as_bytes()), val);
121
122 if !self.run.fetch_or(true, Ordering::SeqCst) {
123 let map = self.map.clone();
124 let env = self.env.clone();
125 let run = self.run.clone();
126 spawn(move || {
127 sleep(Duration::from_secs(1));
128 save(env, &mut map.lock());
129 run.store(false, Ordering::Relaxed);
130 });
131 }
132
133 r
134 })
135 }
136
137 fn _get<T: Str>(&self, key: impl AsRef<OsStr>, init: impl Fn() -> T) -> T {
138 if let Ok(bin) = std::env::var(&key) {
139 if let Ok(r) = err::ok!(T::decode(bin.as_bytes())) {
140 return r;
141 }
142 }
143
144 init()
145 }
146}
147
148#[macro_export]
149macro_rules! config {
150 ($prefix:expr) => {
151 let config = $crate::Config::new(stringify!(prefix).into());
152 $crate::macro_def!(config, get);
153 };
154}
155
156#[macro_export]
157macro_rules! macro_def {
158 ( $config:expr, $action:ident) => {
159 macro_rules! $action {
160 ($key:expr, $default:expr) => {
161 $config.$action(
162 $crate::replace!($crate::replace!(stringify!($key), " ", ""), "/", "_"),
163 || $default,
164 )
165 };
166 }
167 };
168}