together_rs/
terminal.rs

1use dialoguer::{theme::ColorfulTheme, MultiSelect};
2
3#[derive(Debug, clap::Parser)]
4#[clap(
5    name = "together",
6    author = "Michael Lawrence",
7    about = "Run multiple commands in parallel selectively by an interactive prompt."
8)]
9pub struct TogetherArgs {
10    #[clap(subcommand)]
11    pub command: Option<ArgsCommands>,
12
13    #[clap(short, long, help = "Ignore configuration file.")]
14    pub no_config: bool,
15
16    #[clap(short, long = "cwd", help = "Directory to run commands in.")]
17    pub working_directory: Option<String>,
18
19    #[clap(short, long, help = "Only run the startup commands.")]
20    pub init_only: bool,
21
22    #[clap(
23        short = 's',
24        long = "skip",
25        help = "Skip running the startup commands."
26    )]
27    pub no_init: bool,
28
29    #[clap(short, long = "quiet", help = "Quiet mode for startup commands.")]
30    pub quiet_startup: bool,
31
32    #[clap(
33        short,
34        long,
35        help = "Run all commands tagged under provided recipe(s). Use comma to separate multiple recipes.",
36        value_delimiter = ','
37    )]
38    pub recipes: Option<Vec<String>>,
39}
40
41#[derive(Debug, clap::Parser)]
42pub enum ArgsCommands {
43    #[clap(
44        name = "run",
45        about = "Run multiple commands in parallel selectively by an interactive prompt."
46    )]
47    Run(RunCommand),
48
49    #[clap(name = "rerun", about = "Rerun the last together session.")]
50    Rerun(RerunCommand),
51
52    #[clap(name = "load", about = "Run commands from a configuration file.")]
53    Load(LoadCommand),
54}
55
56#[derive(Debug, clap::Parser)]
57pub struct LoadCommand {
58    #[clap(required = true, help = "Configuration file path.")]
59    pub path: String,
60
61    #[clap(short, long, help = "Only run the startup commands.")]
62    pub init_only: bool,
63
64    #[clap(
65        short = 's',
66        long = "skip",
67        help = "Skip running the startup commands."
68    )]
69    pub no_init: bool,
70
71    #[clap(
72        short,
73        long,
74        help = "Run all commands tagged under provided recipe(s). Use comma to separate multiple recipes.",
75        value_delimiter = ','
76    )]
77    pub recipes: Option<Vec<String>>,
78}
79
80#[derive(Debug, clap::Parser)]
81pub struct RerunCommand {}
82
83#[derive(Debug, Clone, clap::Parser)]
84pub struct RunCommand {
85    #[clap(
86        last = true,
87        required = true,
88        help = "Commands to run. e.g. 'ls -l', 'echo hello'"
89    )]
90    pub commands: Vec<String>,
91
92    #[clap(short, long, help = "Run all commands without prompting.")]
93    pub all: bool,
94
95    #[clap(
96        short,
97        long,
98        help = "Exit on the first command that exits with a non-zero status."
99    )]
100    pub exit_on_error: bool,
101
102    #[clap(
103        short,
104        long,
105        help = "Quit the program when all commands have completed."
106    )]
107    pub quit_on_completion: bool,
108
109    #[clap(short, long, help = "Enable raw stdout/stderr output.")]
110    pub raw: bool,
111
112    #[clap(short, long, help = "Only run the startup commands.")]
113    pub init_only: bool,
114
115    #[clap(
116        short = 's',
117        long = "skip",
118        help = "Skip running the startup commands."
119    )]
120    pub no_init: bool,
121}
122
123pub struct Terminal;
124
125impl Terminal {
126    pub fn select_multiple<'a, T: std::fmt::Display>(
127        prompt: &'a str,
128        items: &'a [T],
129    ) -> Vec<&'a T> {
130        if items.is_empty() {
131            return vec![];
132        }
133
134        let mut opts_commands = vec![];
135        let defaults = items.iter().map(|_| false).collect::<Vec<_>>();
136        let multi_select = MultiSelect::with_theme(&ColorfulTheme::default())
137            .with_prompt(prompt)
138            .items(items)
139            .defaults(&defaults[..])
140            .interact();
141        let selections = multi_select.map_err(map_dialoguer_err).unwrap();
142        for index in selections {
143            opts_commands.push(&items[index]);
144        }
145        opts_commands
146    }
147    pub fn select_single<'a, T: std::fmt::Display>(
148        prompt: &'a str,
149        items: &'a [T],
150    ) -> Option<&'a T> {
151        if items.is_empty() {
152            return None;
153        }
154
155        let index = Self::select_single_index(prompt, items)?;
156        Some(&items[index])
157    }
158    pub fn select_single_index<'a, T: std::fmt::Display>(
159        prompt: &'a str,
160        items: &'a [T],
161    ) -> Option<usize> {
162        if items.is_empty() {
163            return None;
164        }
165
166        let index = dialoguer::Select::with_theme(&ColorfulTheme::default())
167            .with_prompt(prompt)
168            .items(items)
169            .interact_opt()
170            .map_err(map_dialoguer_err)
171            .unwrap()?;
172        Some(index)
173    }
174    pub fn select_ordered<'a, T: std::fmt::Display>(
175        prompt: &'a str,
176        items: &'a [T],
177    ) -> Option<Vec<&'a T>> {
178        if items.is_empty() {
179            return None;
180        }
181
182        let mut opts_commands = vec![];
183        let sort = dialoguer::Sort::with_theme(&ColorfulTheme::default())
184            .with_prompt(prompt)
185            .items(items)
186            .interact_opt()
187            .map_err(map_dialoguer_err)
188            .unwrap()?;
189        for index in sort {
190            opts_commands.push(&items[index]);
191        }
192        Some(opts_commands)
193    }
194    pub fn log(message: &str) {
195        // print message with green colorized prefix
196        crate::t_println!("{}[+] {}{}", "\x1b[32m", "\x1b[0m", message);
197    }
198    pub fn log_error(message: &str) {
199        // print message with red colorized prefix
200        crate::t_eprintln!("{}[!] {}{}", "\x1b[31m", "\x1b[0m", message);
201    }
202}
203
204fn map_dialoguer_err(err: dialoguer::Error) -> ! {
205    let dialoguer::Error::IO(io) = err;
206    match io.kind() {
207        std::io::ErrorKind::Interrupted | std::io::ErrorKind::BrokenPipe => {
208            std::process::exit(0);
209        }
210        _ => {
211            panic!("Unexpected error: {}", io);
212        }
213    }
214}
215
216pub mod stdout {
217    /// macro for logging like println! but with a carriage return
218    #[macro_export]
219    macro_rules! t_println {
220        () => {
221            ::std::print!("\r\n");
222        };
223        ($fmt:tt) => {
224            ::std::print!(concat!($fmt, "\r\n"));
225        };
226        ($fmt:tt, $($arg:tt)*) => {
227            ::std::print!(concat!($fmt, "\r\n"), $($arg)*);
228        };
229    }
230
231    /// macro for logging like eprintln! but with a carriage return
232    #[macro_export]
233    macro_rules! t_eprintln {
234        () => {
235            ::std::eprint!("\r\n");
236        };
237        ($fmt:tt) => {
238            ::std::eprint!(concat!($fmt, "\r\n"));
239        };
240        ($fmt:tt, $($arg:tt)*) => {
241            ::std::eprint!(concat!($fmt, "\r\n"), $($arg)*);
242        };
243    }
244}
245
246/// macro for logging like println! but with a green prefix
247#[macro_export]
248macro_rules! log {
249    ($($arg:tt)*) => {
250        $crate::terminal::Terminal::log(&format!($($arg)*));
251    };
252}
253
254/// macro for logging like eprintln! but with a red prefix
255#[macro_export]
256macro_rules! log_err {
257    ($($arg:tt)*) => {
258        $crate::terminal::Terminal::log_error(&format!($($arg)*));
259    };
260}