1use prettytable::{cell, row, Table};
2use snafu::{ensure, OptionExt, ResultExt};
3use std::fs;
4use std::{path::PathBuf, process::ExitStatus};
5pub mod cli;
6mod config;
7pub mod error;
8use config::Config;
9mod defaults;
10mod macros;
11use defaults::*;
12pub mod script;
13use error::*;
14use scrawl;
15use script::Script;
16
17pub type PierResult<T, E = PierError> = ::std::result::Result<T, E>;
19
20#[derive(Debug, Default)]
22pub struct Pier {
23 config: Config,
24 path: PathBuf,
25 verbose: bool,
26}
27
28#[macro_use]
29extern crate lazy_static;
30
31use prettytable::format::LineSeparator;
32use prettytable::format::LinePosition;
33use prettytable::format::FormatBuilder;
34use prettytable::format::TableFormat;
35
36lazy_static! {
37 static ref COOL_SEP: LineSeparator = LineSeparator::new('\u{2256}', '\u{2256}', '\u{2256}', '\u{2256}');
38
39 pub static ref COOL_FORMAT: TableFormat = FormatBuilder::new()
40 .column_separator('\u{22EE}')
41 .borders('\u{22EE}')
42 .separator(LinePosition::Title, *COOL_SEP)
43 .separator(LinePosition::Bottom, *COOL_SEP)
44 .separator(LinePosition::Top, *COOL_SEP)
45 .padding(1, 1)
46 .build();
47}
48
49impl Pier {
50 pub fn write(&self) -> PierResult<()> {
52 self.config.write(&self.path)?;
53
54 Ok(())
55 }
56
57 pub fn config_init(&mut self, new_path: Option<PathBuf>) -> PierResult<()> {
58 self.path = new_path
59 .unwrap_or(fallback_path().unwrap_or(xdg_config_home!("pier/config.toml").unwrap()));
60
61 ensure!(!self.path.exists(), ConfigInitFileAlreadyExists {
62 path: &self.path.as_path()
63 });
64
65 if let Some(parent_dir) = &self.path.parent() {
66 if !parent_dir.exists() {
67 fs::create_dir(parent_dir).context(CreateDirectory)?;
68 }
69 };
70
71 &self.add_script(Script {
72 alias: String::from("hello-pier"),
73 command: String::from("echo Hello, Pier!"),
74 description: Some(String::from("This is an example command.")),
75 reference: None,
76 tags: None,
77 }, false);
78
79 self.write()?;
80
81 Ok(())
82 }
83
84 pub fn new() -> Self {
85 Pier::default()
86 }
87
88 pub fn from_file(path: PathBuf, verbose: bool) -> PierResult<Self> {
90 let pier = Self {
91 config: Config::from(&path)?,
92 verbose,
93 path,
94 };
95 Ok(pier)
96 }
97 pub fn from(input_path: Option<PathBuf>, verbose: bool) -> PierResult<Self> {
99 let path = match input_path {
100 Some(path) => path,
101 None => fallback_path()?,
102 };
103
104 let pier = Pier::from_file(path, verbose)?;
105
106 Ok(pier)
107 }
108
109 pub fn fetch_script(&self, alias: &str) -> PierResult<&Script> {
111 ensure!(!self.config.scripts.is_empty(), NoScriptsExists);
112
113 let script = self
114 .config
115 .scripts
116 .get(alias)
117 .context(AliasNotFound {
118 alias: &alias.to_string(),
119 })?;
120
121 Ok(script)
122 }
123
124 pub fn edit_script(&mut self, alias: &str) -> PierResult<&Script> {
126 ensure!(!self.config.scripts.is_empty(), NoScriptsExists);
127
128 let mut script =
129 self.config
130 .scripts
131 .get_mut(alias)
132 .context(AliasNotFound {
133 alias: &alias.to_string(),
134 })?;
135
136 script.command = open_editor(Some(&script.command))?;
137
138 println!("Edited {}", &alias);
139
140 Ok(script)
141 }
142
143 pub fn remove_script(&mut self, alias: &str) -> PierResult<()> {
145 ensure!(!self.config.scripts.is_empty(), NoScriptsExists);
146
147 self.config
148 .scripts
149 .remove(alias)
150 .context(AliasNotFound {
151 alias: &alias.to_string(),
152 })?;
153
154 println!("Removed {}", &alias);
155
156 Ok(())
157 }
158
159 pub fn add_script(&mut self, script: Script, force: bool) -> PierResult<()> {
161 if !force {
162 ensure!(
163 !&self.config.scripts.contains_key(&script.alias),
164 AliasAlreadyExists {
165 alias: script.alias
166 }
167 );
168 }
169
170 println!("Added {}", &script.alias);
171
172 self.config.scripts.insert(script.alias.to_string(), script);
173
174 Ok(())
175 }
176
177 pub fn list_aliases(&self, tags: Option<Vec<String>>) -> PierResult<()> {
179 ensure!(!self.config.scripts.is_empty(), NoScriptsExists);
180
181 for (alias, script) in self.config.scripts.iter() {
182 match (&tags, &script.tags) {
183 (Some(list_tags), Some(script_tags)) => {
184 for tag in list_tags {
185 if script_tags.contains(tag) {
186 println!("{}", alias);
187
188 continue;
189 }
190 }
191 }
192 (None, _) => {
193 println!("{}", alias);
194
195 continue;
196 }
197 _ => (),
198 };
199 }
200
201 Ok(())
202 }
203
204 pub fn copy_script(&mut self, from_alias: &str, new_alias: &str) -> PierResult<()> {
206 ensure!(
207 !&self.config.scripts.contains_key(new_alias),
208 AliasAlreadyExists { alias: new_alias }
209 );
210
211 let script = self
213 .config
214 .scripts
215 .get(from_alias)
216 .context(AliasNotFound {
217 alias: &from_alias.to_string(),
218 })?
219 .clone();
220
221 println!(
222 "Copy from alias {} to new alias {}",
223 &from_alias.to_string(),
224 &new_alias.to_string()
225 );
226
227 self.config.scripts.insert(new_alias.to_string(), script);
228
229 Ok(())
230 }
231
232 pub fn move_script(&mut self, from_alias: &str, new_alias: &str, force: bool) -> PierResult<()> {
234 if !force {
235 ensure!(
236 !&self.config.scripts.contains_key(new_alias),
237 AliasAlreadyExists { alias: new_alias }
238 );
239 }
240
241 let script = self
242 .config
243 .scripts
244 .remove(from_alias)
245 .context(AliasNotFound {
246 alias: &from_alias.to_string(),
247 })?
248 .clone();
249
250 println!(
251 "Move from alias {} to new alias {}",
252 &from_alias.to_string(),
253 &new_alias.to_string()
254 );
255
256 self.config.scripts.insert(new_alias.to_string(), script);
257
258 Ok(())
259 }
260
261 pub fn list_scripts(
263 &self,
264 tags: Option<Vec<String>>,
265 cmd_full: bool,
266 cmd_width: Option<usize>,
267 ) -> PierResult<()> {
268 let width = match (cmd_width, self.config.default.command_width) {
269 (Some(width), _) => width,
270 (None, Some(width)) => width,
271 (None, None) => FALLBACK_COMMAND_DISPLAY_WIDTH,
272 };
273 ensure!(!self.config.scripts.is_empty(), NoScriptsExists);
274
275 let mut table = Table::new();
276
277 table.set_format(*COOL_FORMAT);
278 table.set_titles(row![
280 Fc -> "Alias",
281 Fc -> "Tags",
282 Fc -> "Command",
283 Fc -> "Description",
284 ]);
285
286 for (alias, script) in self.config.scripts.iter() {
287 let shbang = script.command.starts_with("#!");
288 let descp = match &script.description.as_ref() {
289 Some(d) => d,
290 None => "",
291 };
292
293 match (&tags, &script.tags) {
294 (Some(list_tags), Some(script_tags)) => {
295
296 for tag in list_tags {
297 if script_tags.contains(tag) {
298
299 if shbang {
300 table.add_row(row![
301 FY -> &alias,
302 Fg -> script_tags.join(","),
303 Fm -> "#! script",
304 Fw -> descp,
305 ]);
306 } else {
307 table.add_row(row![
308 FY -> &alias,
309 Fg -> script_tags.join(","),
310 Fb -> script.display_command(cmd_full, width),
311 Fw -> descp,
312 ]);
313 }
314
315 continue;
316 }
317 }
318 }
319 (None, Some(script_tags)) => {
320 if shbang {
321 table.add_row(row![
322 FY -> &alias,
323 Fg -> script_tags.join(","),
324 Fm -> "#! script",
325 Fw -> descp,
326 ]);
327 } else {
328 table.add_row(row![
329 FY -> &alias,
330 Fg -> script_tags.join(","),
331 Fb -> script.display_command(cmd_full, width),
332 Fw -> descp,
333 ]);
334 }
335
336 continue;
337 }
338 (None, None) => {
339 if shbang {
340 table.add_row(row![
341 FY -> &alias,
342 Fg -> "",
343 Fm -> "#! script",
344 Fw -> descp,
345 ]);
346 } else {
347 table.add_row(row![
348 FY -> &alias,
349 Fg -> "",
350 Fb -> script.display_command(cmd_full, width),
351 Fw -> descp,
352 ]);
353 }
354
355 continue;
356 }
357 _ => (),
358 };
359 }
360
361 table.print_tty(true);
363
364 Ok(())
365 }
366
367 pub fn run_script(&self, alias: &str, args: Vec<String>) -> PierResult<ExitStatus> {
369 let script = self.fetch_script(alias)?;
370 let interpreter = match self.config.default.interpreter {
371 Some(ref interpreter) => interpreter.clone(),
372 None => fallback_shell(),
373 };
374
375 if self.verbose {
376 println!("Starting script \"{}\"", alias);
377 println!("-------------------------");
378 };
379
380 let cmd = match script.has_shebang() {
381 true => script.run_with_shebang(args)?,
382 false => script.run_with_cli_interpreter(&interpreter, args)?,
383 };
384
385 let stdout = String::from_utf8_lossy(&cmd.stdout);
386 let stderr = String::from_utf8_lossy(&cmd.stderr);
387
388 if stdout.len() > 0 {
389 println!("{}", stdout);
390 };
391 if stderr.len() > 0 {
392 eprintln!("{}", stderr);
393 };
394
395 if self.verbose {
396 println!("-------------------------");
397 println!("Script complete");
398 };
399
400 Ok(cmd.status)
401 }
402}
403
404pub fn open_editor(content: Option<&str>) -> PierResult<String> {
405 let edited_text = scrawl::editor::new()
406 .contents(match content {
407 Some(txt) => txt,
408 None => "",
409 })
410 .open()
411 .context(EditorError)?;
412
413 Ok(edited_text)
414}