hyper_scripter/
script_type.rs

1use crate::error::{DisplayError, DisplayResult, Error, FormatCode::ScriptType as TypeCode};
2use crate::util::illegal_name;
3use crate::util::impl_ser_by_to_string;
4use fxhash::FxHashMap as HashMap;
5use handlebars::Handlebars;
6use serde::{Deserialize, Serialize};
7use std::fmt::{Display, Formatter, Result as FmtResult};
8use std::str::FromStr;
9
10const DEFAULT_WELCOME_MSG: &str = "{{#each content}}{{{this}}}
11{{/each}}";
12
13const SHELL_WELCOME_MSG: &str = "set -eux
14
15# [HS_HELP]: Help message goes here...
16# [HS_ENV]: VAR -> Description for env var `VAR` goes here
17# [HS_ENV_HELP]: VAR2 -> Description for `VAR2` goes here, BUT won't be recorded
18{{#if birthplace_in_home}}
19cd ~/{{birthplace_rel}}
20{{else}}
21cd {{birthplace}}
22{{/if}}
23{{#each content}}{{{this}}}
24{{/each}}";
25
26const JS_WELCOME_MSG: &str = "// [HS_HELP]: Help message goes here...
27// [HS_ENV]: VAR -> Description for env var `VAR` goes here
28// [HS_ENV_HELP]: VAR2 -> Description for `VAR2` goes here, BUT won't be recorded
29
30process.chdir(require('os').homedir());
31{{#if birthplace_in_home}}
32process.chdir(process.env.HOME);
33process.chdir('{{birthplace_rel}}');
34{{else}}
35process.chdir('{{birthplace}}');
36{{/if}}
37let spawn = require('child_process').spawnSync;
38spawn('test', [], { stdio: 'inherit' });
39
40let writeFile = require('fs').writeFileSync;
41writeFile('/dev/null', 'some content');
42
43{{#each content}}{{{this}}}
44{{/each}}";
45
46const TMUX_WELCOME_MSG: &str = "# [HS_HELP]: Help message goes here...
47# [HS_ENV]: VAR -> Description for env var `VAR` goes here
48# [HS_ENV_HELP]: VAR2 -> Description for `VAR2` goes here, BUT won't be recorded
49
50NAME=${NAME/./_}
51tmux has-session -t=$NAME
52if [ $? = 0 ]; then
53    echo attach to existing session
54    tmux -2 attach-session -t $NAME
55    exit
56fi
57
58set -eux
59{{#if birthplace_in_home}}
60cd ~/{{birthplace_rel}}
61{{else}}
62cd {{birthplace}}
63{{/if}}
64tmux new-session -s $NAME -d \"{{{content.0}}}; $SHELL\" || exit 1
65tmux split-window -h \"{{{content.1}}}; $SHELL\"
66{{#if content.2}}tmux split-window -v \"{{{content.2}}}; $SHELL\"
67{{/if}}
68tmux -2 attach-session -d";
69
70const RB_WELCOME_MSG: &str = "# [HS_HELP]: Help message goes here...
71# [HS_ENV]: VAR -> Description for env var `VAR` goes here
72# [HS_ENV_HELP]: VAR2 -> Description for `VAR2` goes here, BUT won't be recorded
73{{#if birthplace_in_home}}
74Dir.chdir(\"#{ENV['HOME']}/{{birthplace_rel}}\")
75{{else}}
76Dir.chdir(\"{{birthplace}}\")
77{{/if}}
78{{#each content}}{{{this}}}
79{{/each}}";
80
81const RB_CD_WELCOME_MSG: &str =
82"{{#if birthplace_in_home}}BASE = \"#{ENV['HOME']}/{{birthplace_rel}}\"
83{{else}}BASE = '{{birthplace}}'
84{{/if}}
85require File.realpath(\"#{ENV['HS_HOME']}/util/common.rb\")
86require 'set'
87
88def cd(dir)
89  File.open(HS_ENV.env_var(:source), 'w') do |file|
90    file.write(\"cd #{BASE}/#{dir}\")
91  end
92  exit
93end
94
95cd(ARGV[0]) if ARGV != []
96
97Dir.chdir(BASE)
98dirs_set = Dir.entries('.').select do |c|
99  !c.start_with?('.') && File.directory?(c)
100end.to_set
101dirs_set.add('.')
102
103history_arr = HS_ENV.do_hs(\"history show =#{HS_ENV.env_var(:name)}!\", false).lines.map(&:strip).reject(&:empty?)
104
105history_arr = history_arr.select do |d|
106  if dirs_set.include?(d)
107    dirs_set.delete(d)
108    true
109  else
110    false
111  end
112end
113
114require_relative \"#{ENV['HS_HOME']}/util/selector.rb\"
115selector = Selector.new
116selector.load(history_arr + dirs_set.to_a)
117is_dot = false
118selector.register_keys('.', lambda { |_, _|
119  is_dot = true
120}, msg: 'go to \".\"')
121
122dir = begin
123  content = selector.run.options[0]
124  if is_dot
125    '.'
126  else
127    content
128  end
129rescue Selector::Empty
130  warn 'empty'
131  exit 1
132rescue Selector::Quit
133  warn 'quit'
134  exit
135end
136
137HS_ENV.do_hs(\"run --dummy =#{HS_ENV.env_var(:name)}! #{dir}\", false)
138cd(dir)";
139
140const RB_TRAVERSE_WELCOME_MSG: &str = "# [HS_HELP]: Help message goes here...
141# [HS_ENV]: VAR -> Description for env var `VAR` goes here
142# [HS_ENV_HELP]: VAR2 -> Description for `VAR2` goes here, BUT won't be recorded
143
144def directory_tree(path)
145  files = []
146  Dir.foreach(path) do |entry|
147    next if ['..', '.'].include?(entry)
148
149    full_path = File.join(path, entry)
150    if File.directory?(full_path)
151      directory_tree(full_path).each do |f|
152        files.push(f)
153      end
154    else
155      files.push(full_path)
156    end
157  end
158  files
159end
160{{#if birthplace_in_home}}
161Dir.chdir(\"#{ENV['HOME']}/{{birthplace_rel}}\")
162{{else}}
163Dir.chdir(\"{{birthplace}}\")
164{{/if}}
165directory_tree('.').each do |full_path|
166  {{#each content}}{{{this}}}
167  {{else}} # TODO{{/each}}
168end";
169
170#[derive(Clone, Display, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
171#[serde(transparent)]
172pub struct ScriptType(String);
173impl ScriptType {
174    pub fn new_unchecked(s: String) -> Self {
175        ScriptType(s)
176    }
177}
178impl AsRef<str> for ScriptType {
179    fn as_ref(&self) -> &str {
180        &self.0
181    }
182}
183impl FromStr for ScriptType {
184    type Err = DisplayError;
185    fn from_str(s: &str) -> DisplayResult<Self> {
186        if illegal_name(s) {
187            log::error!("類型格式不符:{}", s);
188            return TypeCode.to_display_res(s.to_owned());
189        }
190        Ok(ScriptType(s.to_string()))
191    }
192}
193impl Default for ScriptType {
194    fn default() -> Self {
195        ScriptType("sh".to_string())
196    }
197}
198
199#[derive(Clone, Debug, Eq, PartialEq)]
200pub struct ScriptFullType {
201    pub ty: ScriptType,
202    pub sub: Option<ScriptType>,
203}
204impl FromStr for ScriptFullType {
205    type Err = DisplayError;
206    fn from_str(s: &str) -> DisplayResult<Self> {
207        if let Some((first, second)) = s.split_once("/") {
208            Ok(ScriptFullType {
209                ty: first.parse()?,
210                sub: Some(second.parse()?),
211            })
212        } else {
213            Ok(ScriptFullType {
214                ty: s.parse()?,
215                sub: None,
216            })
217        }
218    }
219}
220impl Default for ScriptFullType {
221    fn default() -> Self {
222        Self {
223            ty: ScriptType::default(),
224            sub: None,
225        }
226    }
227}
228impl_ser_by_to_string!(ScriptFullType);
229
230impl Display for ScriptFullType {
231    fn fmt(&self, w: &mut Formatter<'_>) -> FmtResult {
232        if let Some(sub) = &self.sub {
233            write!(w, "{}/{}", self.ty, sub)
234        } else {
235            write!(w, "{}", self.ty)
236        }
237    }
238}
239
240#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
241pub struct ScriptTypeConfig {
242    pub ext: Option<String>,
243    pub color: String,
244    pub cmd: Option<String>,
245    args: Vec<String>,
246    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
247    env: HashMap<String, String>,
248}
249
250impl ScriptTypeConfig {
251    // XXX: extract
252    pub fn args(&self, info: &crate::util::TmplVal<'_>) -> Result<Vec<String>, Error> {
253        let reg = Handlebars::new();
254        let mut args: Vec<String> = Vec::with_capacity(self.args.len());
255        for c in self.args.iter() {
256            let res = reg.render_template(c, info)?;
257            args.push(res);
258        }
259        Ok(args)
260    }
261    // XXX: extract
262    pub fn gen_env(&self, info: &crate::util::TmplVal<'_>) -> Result<Vec<(String, String)>, Error> {
263        let reg = Handlebars::new();
264        let mut env: Vec<(String, String)> = Vec::with_capacity(self.env.len());
265        for (name, e) in self.env.iter() {
266            let res = reg.render_template(e, info)?;
267            env.push((name.to_owned(), res));
268        }
269        Ok(env)
270    }
271    pub fn default_script_types() -> HashMap<ScriptType, ScriptTypeConfig> {
272        let mut ret = HashMap::default();
273        for (ty, conf) in iter_default_configs() {
274            ret.insert(ty, conf);
275        }
276        ret
277    }
278}
279
280macro_rules! create_default_types {
281    ($(( $name:literal, $tmpl:ident, $conf:expr, [ $($sub:literal: $sub_tmpl:ident),* ] )),*) => {
282        pub fn get_default_template(ty: &ScriptFullType) -> &'static str {
283            match (ty.ty.as_ref(), ty.sub.as_ref().map(|s| s.as_ref())) {
284                $(
285                    $(
286                        ($name, Some($sub)) => $sub_tmpl,
287                    )*
288                    ($name, _) => $tmpl,
289                )*
290                _ => DEFAULT_WELCOME_MSG
291            }
292        }
293        pub fn iter_default_templates() -> impl ExactSizeIterator<Item = (ScriptFullType, &'static str)> {
294            let arr = [$(
295                (ScriptFullType{ ty: ScriptType($name.to_owned()), sub: None }, $tmpl),
296                $(
297                    (ScriptFullType{ ty: ScriptType($name.to_owned()), sub: Some(ScriptType($sub.to_owned())) }, $sub_tmpl),
298                )*
299            )*];
300            arr.into_iter()
301        }
302        fn iter_default_configs() -> impl ExactSizeIterator<Item = (ScriptType, ScriptTypeConfig)> {
303            let arr = [$( (ScriptType($name.to_owned()), $conf), )*];
304            arr.into_iter()
305        }
306    };
307}
308
309fn gen_map(arr: &[(&str, &str)]) -> HashMap<String, String> {
310    arr.iter()
311        .map(|(k, v)| (k.to_string(), v.to_string()))
312        .collect()
313}
314
315create_default_types! {
316    ("sh", SHELL_WELCOME_MSG, ScriptTypeConfig {
317        ext: Some("sh".to_owned()),
318        color: "bright magenta".to_owned(),
319        cmd: Some("bash".to_owned()),
320        args: vec!["{{path}}".to_owned()],
321        env: Default::default()
322    }, []),
323    ("tmux", TMUX_WELCOME_MSG, ScriptTypeConfig {
324        ext: Some("sh".to_owned()),
325        color: "white".to_owned(),
326        cmd: Some("bash".to_owned()),
327        args: vec!["{{path}}".to_owned()],
328        env: Default::default(),
329    }, []),
330    ("js", JS_WELCOME_MSG, ScriptTypeConfig {
331        ext: Some("js".to_owned()),
332        color: "bright cyan".to_owned(),
333        cmd: Some("node".to_owned()),
334        args: vec!["{{path}}".to_owned()],
335        env: gen_map(&[(
336            "NODE_PATH",
337            "{{{home}}}/node_modules",
338        )]),
339    }, []),
340    ("js-i", JS_WELCOME_MSG, ScriptTypeConfig {
341        ext: Some("js".to_owned()),
342        color: "bright cyan".to_owned(),
343        cmd: Some("node".to_owned()),
344        args: vec!["-i".to_owned(), "-e".to_owned(), "{{{content}}}".to_owned()],
345        env: gen_map(&[(
346            "NODE_PATH",
347            "{{{home}}}/node_modules",
348        )]),
349    }, []),
350    ("rb", RB_WELCOME_MSG, ScriptTypeConfig {
351        ext: Some("rb".to_owned()),
352        color: "bright red".to_owned(),
353        cmd: Some("ruby".to_owned()),
354        args: vec!["{{path}}".to_owned()],
355        env: Default::default(),
356    }, ["traverse": RB_TRAVERSE_WELCOME_MSG, "cd": RB_CD_WELCOME_MSG]),
357    ("txt", DEFAULT_WELCOME_MSG, ScriptTypeConfig {
358        ext: None,
359        color: "bright black".to_owned(),
360        cmd: Some("cat".to_owned()),
361        args: vec!["{{path}}".to_owned()],
362        env: Default::default(),
363    }, [])
364}