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 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 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}