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