pier/
lib.rs

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
17// Creates a Result type that return PierError by default
18pub type PierResult<T, E = PierError> = ::std::result::Result<T, E>;
19
20/// Main library interface
21#[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    /// Wrapper to write the configuration to path.
51    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    /// Create new pier directly from path.
89    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    /// Create new pier from what might be a path, otherwise use the first existing default path.
98    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    /// Fetches a script that matches the alias
110    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    /// Edits a script that matches the alias
125    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    /// Removes a script that matches the alias
144    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    /// Adds a script that matches the alias
160    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    /// Prints only the aliases in current config file that matches tags.
178    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    /// Copy an alias a script that matches the alias
205    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        // TODO: refactor the line below.
212        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    /// Move a script that matches the alias to another alias
233    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    /// Prints a terminal table of the scripts in current config file that matches tags.
262    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        // cyan titles
279        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        // forced color explicitly. works in pipes
362        table.print_tty(true);
363
364        Ok(())
365    }
366
367    /// Runs a script and print stdout and stderr of the command.
368    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}