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