hyper_scripter/
config.rs

1use crate::color::Color;
2use crate::error::{DisplayError, DisplayResult, Error, FormatCode, Result};
3use crate::path;
4use crate::script_type::{ScriptType, ScriptTypeConfig};
5use crate::tag::{TagGroup, TagSelector, TagSelectorGroup};
6use crate::util;
7use crate::util::{impl_de_by_from_str, impl_ser_by_to_string};
8use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet};
9use handlebars::Handlebars;
10use serde::{Deserialize, Serialize};
11use std::path::{Path, PathBuf};
12use std::str::FromStr;
13use std::time::SystemTime;
14
15const CONFIG_FILE: &str = ".config.toml";
16
17crate::local_global_state!(config_state, Config, || { Default::default() });
18crate::local_global_state!(runtime_conf_state, RuntimeConf, || { unreachable!() });
19
20struct RuntimeConf {
21    prompt_level: PromptLevel,
22    no_caution: bool,
23}
24
25fn de_nonempty_vec<'de, D, T>(deserializer: D) -> std::result::Result<Vec<T>, D::Error>
26where
27    D: serde::de::Deserializer<'de>,
28    T: Deserialize<'de>,
29{
30    let v: Vec<T> = Deserialize::deserialize(deserializer)?;
31    if v.is_empty() {
32        return Err(serde::de::Error::custom(
33            FormatCode::NonEmptyArray.to_err(String::new()),
34        ));
35    }
36    Ok(v)
37}
38
39fn config_file(home: &Path) -> PathBuf {
40    home.join(CONFIG_FILE)
41}
42
43fn is_false(b: &bool) -> bool {
44    !*b
45}
46
47#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
48pub struct NamedTagSelector {
49    pub content: TagSelector,
50    pub name: String,
51    #[serde(default, skip_serializing_if = "is_false")]
52    pub inactivated: bool,
53}
54
55#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
56pub struct Alias {
57    #[serde(deserialize_with = "de_nonempty_vec")]
58    pub after: Vec<String>,
59}
60impl From<Vec<String>> for Alias {
61    fn from(after: Vec<String>) -> Self {
62        Alias { after }
63    }
64}
65impl Alias {
66    /// ```rust
67    /// use hyper_scripter::config::Alias;
68    ///
69    /// fn get_args(alias: &Alias) -> (bool, Vec<&str>) {
70    ///     let (is_shell, args) = alias.args();
71    ///     (is_shell, args.collect())
72    /// }
73    ///
74    /// let alias = Alias::from(vec!["!".to_owned()]);
75    /// assert_eq!((false, vec!["!"]), get_args(&alias));
76    ///
77    /// let alias = Alias::from(vec!["!".to_owned(), "args".to_owned()]);
78    /// assert_eq!((false, vec!["!", "args"]), get_args(&alias));
79    ///
80    /// let alias = Alias::from(vec!["! args".to_owned()]);
81    /// assert_eq!((false, vec!["! args"]), get_args(&alias));
82    ///
83    /// let alias = Alias::from(vec!["!!".to_owned()]);
84    /// assert_eq!((true, vec!["!"]), get_args(&alias));
85    ///
86    /// let alias = Alias::from(vec!["!ls".to_owned()]);
87    /// assert_eq!((true, vec!["ls"]), get_args(&alias));
88    ///
89    /// let alias = Alias::from(vec!["!ls".to_owned(), "*".to_owned()]);
90    /// assert_eq!((true, vec!["ls", "*"]), get_args(&alias));
91    /// ```
92    pub fn args(&self) -> (bool, impl Iterator<Item = &'_ str>) {
93        let mut is_shell = false;
94        let mut iter = self.after.iter().map(String::as_str);
95        let mut first_args = iter.next().unwrap();
96        let mut chars = first_args.chars();
97        if chars.next() == Some('!') {
98            if first_args.len() > 1 {
99                if chars.next() != Some(' ') {
100                    is_shell = true;
101                    first_args = &first_args[1..];
102                }
103            }
104        }
105
106        return (is_shell, std::iter::once(first_args).chain(iter));
107    }
108}
109
110#[derive(Display, PartialEq, Eq, Debug, Clone, Copy)]
111pub enum PromptLevel {
112    #[display(fmt = "always")]
113    Always,
114    #[display(fmt = "never")]
115    Never,
116    #[display(fmt = "smart")]
117    Smart,
118    #[display(fmt = "on_multi_fuzz")]
119    OnMultiFuzz,
120}
121impl FromStr for PromptLevel {
122    type Err = DisplayError;
123    fn from_str(s: &str) -> DisplayResult<Self> {
124        let l = match s {
125            "always" => PromptLevel::Always,
126            "never" => PromptLevel::Never,
127            "smart" => PromptLevel::Smart,
128            "on-multi-fuzz" => PromptLevel::OnMultiFuzz,
129            _ => return FormatCode::PromptLevel.to_display_res(s.to_owned()),
130        };
131        Ok(l)
132    }
133}
134impl_ser_by_to_string!(PromptLevel);
135impl_de_by_from_str!(PromptLevel);
136
137#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
138pub struct Config {
139    pub recent: Option<u32>,
140    pub main_tag_selector: TagSelector,
141    #[serde(default)]
142    pub caution_tags: TagGroup,
143    prompt_level: PromptLevel,
144    #[serde(deserialize_with = "de_nonempty_vec")]
145    pub editor: Vec<String>,
146    pub tag_selectors: Vec<NamedTagSelector>,
147    pub alias: HashMap<String, Alias>,
148    pub types: HashMap<ScriptType, ScriptTypeConfig>,
149    pub env: HashMap<String, String>,
150    #[serde(skip)]
151    last_modified: Option<SystemTime>,
152}
153impl Default for Config {
154    fn default() -> Self {
155        fn gen_alias(from: &str, after: &[&str]) -> (String, Alias) {
156            (
157                from.to_owned(),
158                Alias {
159                    after: after.iter().map(|s| s.to_string()).collect(),
160                },
161            )
162        }
163        Config {
164            last_modified: None,
165            recent: Some(999999), // NOTE: 顯示兩千多年份的資料!
166            editor: vec!["vim".to_string()],
167            prompt_level: PromptLevel::Smart,
168            tag_selectors: vec![
169                NamedTagSelector {
170                    content: "+pin,util".parse().unwrap(),
171                    name: "pin".to_owned(),
172                    inactivated: false,
173                },
174                NamedTagSelector {
175                    content: "+^hide!".parse().unwrap(),
176                    name: "no-hidden".to_owned(),
177                    inactivated: false,
178                },
179                NamedTagSelector {
180                    content: "+^remove!".parse().unwrap(),
181                    name: "no-removed".to_owned(),
182                    inactivated: false,
183                },
184            ],
185            main_tag_selector: "+all".parse().unwrap(),
186            caution_tags: "caution".parse().unwrap(),
187            types: ScriptTypeConfig::default_script_types(),
188            alias: [
189                gen_alias("la", &["ls", "-a"]),
190                gen_alias("ll", &["ls", "-l"]),
191                gen_alias("l", &["ls", "--grouping", "none", "--limit", "5"]),
192                gen_alias("e", &["edit"]),
193                gen_alias("gc", &["rm", "--timeless", "--purge", "-s", "remove", "*"]),
194                gen_alias("t", &["tags"]),
195                gen_alias("p", &["run", "--previous"]),
196                gen_alias(
197                    "pc",
198                    &["=util/historian!", "--sequence", "c", "--display=all"],
199                ),
200                gen_alias(
201                    "pr",
202                    &["=util/historian!", "--sequence", "r", "--display=all"],
203                ),
204                gen_alias("h", &["=util/historian!", "--display=all"]),
205                // Showing humble events of all scripts will be a mess
206                gen_alias(
207                    "hh",
208                    &["=util/historian!", "*", "--display=all", "--no-humble"],
209                ),
210            ]
211            .into_iter()
212            .collect(),
213            env: [
214                ("NAME", "{{name}}"),
215                ("HS_HOME", "{{home}}"),
216                ("HS_CMD", "{{cmd}}"),
217                ("HS_RUN_ID", "{{run_id}}"),
218                (
219                    "HS_TAGS",
220                    "{{#each tags}}{{{this}}}{{#unless @last}} {{/unless}}{{/each}}",
221                ),
222                (
223                    "HS_ENV_DESC",
224                    "{{#each env_desc}}{{{this}}}{{#unless @last}}\n{{/unless}}{{/each}}",
225                ),
226                ("HS_EXE", "{{exe}}"),
227                ("HS_SOURCE", "{{home}}/.hs_source"),
228                ("TMP_DIR", "/tmp"),
229            ]
230            .into_iter()
231            .map(|(k, v)| (k.to_owned(), v.to_owned()))
232            .collect(),
233        }
234    }
235}
236impl Config {
237    pub fn load(p: &Path) -> Result<Self> {
238        let path = config_file(p);
239        log::info!("載入設定檔:{:?}", path);
240        match util::read_file(&path) {
241            Ok(s) => {
242                let meta = util::handle_fs_res(&[&path], std::fs::metadata(&path))?;
243                let modified = util::handle_fs_res(&[&path], meta.modified())?;
244
245                let mut conf: Config = toml::from_str(&s).map_err(|err| {
246                    FormatCode::Config.to_err(format!("{}: {}", path.to_string_lossy(), err))
247                })?;
248                conf.last_modified = Some(modified);
249                Ok(conf)
250            }
251            Err(Error::PathNotFound(_)) => {
252                log::debug!("找不到設定檔");
253                Ok(Default::default())
254            }
255            Err(e) => Err(e),
256        }
257    }
258
259    pub fn store(&self) -> Result {
260        let path = config_file(path::get_home());
261        log::info!("寫入設定檔至 {:?}…", path);
262        match util::handle_fs_res(&[&path], std::fs::metadata(&path)) {
263            Ok(meta) => {
264                let modified = util::handle_fs_res(&[&path], meta.modified())?;
265                // NOTE: 若設定檔是憑空造出來的,但要存入時卻發現已有檔案,同樣不做存入
266                if self.last_modified.map_or(true, |time| time < modified) {
267                    log::info!("設定檔已被修改,不寫入");
268                    return Ok(());
269                }
270            }
271            Err(Error::PathNotFound(_)) => {
272                log::debug!("設定檔不存在,寫就對了");
273            }
274            Err(err) => return Err(err),
275        }
276        util::write_file(&path, &toml::to_string_pretty(self)?)
277    }
278
279    pub fn is_from_dafault(&self) -> bool {
280        self.last_modified.is_none()
281    }
282
283    pub fn init() -> Result {
284        config_state::set(Config::load(path::get_home())?);
285        Ok(())
286    }
287
288    pub fn set_runtime_conf(prompt_level: Option<PromptLevel>, no_caution: bool) {
289        let c = Config::get();
290        let prompt_level = prompt_level.unwrap_or(c.prompt_level); // TODO: 測試由設定檔設定 prompt-level 的情境?
291        runtime_conf_state::set(RuntimeConf {
292            prompt_level,
293            no_caution,
294        });
295    }
296    pub fn get_prompt_level() -> PromptLevel {
297        runtime_conf_state::get().prompt_level
298    }
299    pub fn get_no_caution() -> bool {
300        runtime_conf_state::get().no_caution
301    }
302
303    pub fn get() -> &'static Config {
304        config_state::get()
305    }
306
307    // XXX: extract
308    pub fn gen_env(
309        &self,
310        info: &crate::util::TmplVal<'_>,
311        strict: bool,
312    ) -> Result<Vec<(String, String)>> {
313        let reg = Handlebars::new();
314        let mut env: Vec<(String, String)> = Vec::with_capacity(self.env.len());
315        for (name, e) in self.env.iter() {
316            match reg.render_template(e, info) {
317                Ok(res) => env.push((name.to_owned(), res)),
318                Err(err) => {
319                    if strict {
320                        return Err(err.into());
321                    }
322                }
323            }
324        }
325        Ok(env)
326    }
327    pub fn get_color(&self, ty: &ScriptType) -> Result<Color> {
328        let c = self.get_script_conf(ty)?.color.as_str();
329        Ok(Color::from(c))
330    }
331    pub fn get_script_conf(&self, ty: &ScriptType) -> Result<&ScriptTypeConfig> {
332        self.types
333            .get(ty)
334            .ok_or_else(|| Error::UnknownType(ty.to_string()))
335    }
336    pub fn get_tag_selector_group(&self, toggle: &mut HashSet<String>) -> TagSelectorGroup {
337        let mut group = TagSelectorGroup::default();
338        for f in self.tag_selectors.iter() {
339            let inactivated = f.inactivated ^ toggle.remove(&f.name);
340            if inactivated {
341                log::debug!("{:?} 未啟用", f);
342                continue;
343            }
344            group.push(f.content.clone()); // TODO: TagSelectorGroup 可以多帶點 lifetime 減少複製
345        }
346        group.push(self.main_tag_selector.clone());
347        group
348    }
349}
350
351#[cfg(test)]
352mod test {
353    use super::*;
354    use toml::{from_str, to_string_pretty};
355    #[test]
356    fn test_config_serde() {
357        let c1 = Config {
358            main_tag_selector: "a,^b,c".parse().unwrap(),
359            ..Default::default()
360        };
361        let s = to_string_pretty(&c1).unwrap();
362        let c2: Config = from_str(&s).unwrap();
363        assert_eq!(c1, c2);
364    }
365}