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 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), 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 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 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); 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 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()); }
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}