1use {
109 anstream::{print, println},
110 anyhow::{anyhow, Result},
111 clap::ValueEnum,
112 owo_colors::{OwoColorize, Rgb, Style},
113 rayon::prelude::*,
114 std::io::{Read, Write},
115};
116
117#[derive(Clone, Debug, PartialEq, Eq)]
120pub enum Pipe {
121 Null,
122 Stdout,
123 Stderr,
124 String(Option<String>),
125}
126
127impl Pipe {
128 pub fn string() -> Pipe {
129 Pipe::String(None)
130 }
131}
132
133pub fn style(s: &str) -> Result<Style> {
137 let mut r = Style::new();
138 for i in s.split('+') {
139 if let Some(color) = i.strip_prefix('#') {
140 r = r.color(html(color)?);
141 } else if let Some(color) = i.strip_prefix("on-#") {
142 r = r.on_color(html(color)?);
143 } else {
144 match i {
145 "black" => r = r.black(),
146 "red" => r = r.red(),
147 "green" => r = r.green(),
148 "yellow" => r = r.yellow(),
149 "blue" => r = r.blue(),
150 "magenta" => r = r.magenta(),
151 "purple" => r = r.purple(),
152 "cyan" => r = r.cyan(),
153 "white" => r = r.white(),
154 "bold" => r = r.bold(),
156 "italic" => r = r.italic(),
157 "dimmed" => r = r.dimmed(),
158 "underline" => r = r.underline(),
159 "blink" => r = r.blink(),
160 "blink_fast" => r = r.blink_fast(),
161 "reversed" => r = r.reversed(),
162 "hidden" => r = r.hidden(),
163 "strikethrough" => r = r.strikethrough(),
164 "bright-black" => r = r.bright_black(),
166 "bright-red" => r = r.bright_red(),
167 "bright-green" => r = r.bright_green(),
168 "bright-yellow" => r = r.bright_yellow(),
169 "bright-blue" => r = r.bright_blue(),
170 "bright-magenta" => r = r.bright_magenta(),
171 "bright-purple" => r = r.bright_purple(),
172 "bright-cyan" => r = r.bright_cyan(),
173 "bright-white" => r = r.bright_white(),
174 "on-black" => r = r.on_black(),
176 "on-red" => r = r.on_red(),
177 "on-green" => r = r.on_green(),
178 "on-yellow" => r = r.on_yellow(),
179 "on-blue" => r = r.on_blue(),
180 "on-magenta" => r = r.on_magenta(),
181 "on-purple" => r = r.on_purple(),
182 "on-cyan" => r = r.on_cyan(),
183 "on-white" => r = r.on_white(),
184 "on-bright-black" => r = r.on_bright_black(),
186 "on-bright-red" => r = r.on_bright_red(),
187 "on-bright-green" => r = r.on_bright_green(),
188 "on-bright-yellow" => r = r.on_bright_yellow(),
189 "on-bright-blue" => r = r.on_bright_blue(),
190 "on-bright-magenta" => r = r.on_bright_magenta(),
191 "on-bright-purple" => r = r.on_bright_purple(),
192 "on-bright-cyan" => r = r.on_bright_cyan(),
193 "on-bright-white" => r = r.on_bright_white(),
194 _ => return Err(anyhow!("Invalid style spec: {s:?}!")),
196 }
197 }
198 }
199 Ok(r)
200}
201
202fn html(rrggbb: &str) -> Result<Rgb> {
203 let r = u8::from_str_radix(&rrggbb[0..2], 16)?;
204 let g = u8::from_str_radix(&rrggbb[2..4], 16)?;
205 let b = u8::from_str_radix(&rrggbb[4..6], 16)?;
206 Ok(Rgb(r, g, b))
207}
208
209#[derive(Clone, Debug, Default, ValueEnum)]
210pub enum ColorOverride {
211 #[default]
212 Auto,
213 Always,
214 Never,
215}
216
217impl ColorOverride {
218 pub fn init(&self) {
219 match self {
220 ColorOverride::Always => anstream::ColorChoice::Always.write_global(),
221 ColorOverride::Never => anstream::ColorChoice::Never.write_global(),
222 ColorOverride::Auto => {}
223 }
224 }
225}
226
227struct Prefix {
230 style: Style,
231}
232
233impl std::fmt::Display for Prefix {
234 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
235 self.style.fmt_prefix(f)
236 }
237}
238
239fn print_prefix(style: Style) {
240 print!("{}", Prefix { style });
241}
242
243struct Suffix {
246 style: Style,
247}
248
249impl std::fmt::Display for Suffix {
250 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
251 self.style.fmt_suffix(f)
252 }
253}
254
255fn print_suffix(style: Style) {
256 print!("{}", Suffix { style });
257}
258
259#[derive(Clone, Debug)]
308pub struct Shell {
309 pub shell: Option<String>,
310
311 pub dry_run: bool,
312 pub sync: bool,
313 pub print: bool,
314 pub color: ColorOverride,
315
316 pub fence: String,
317 pub info: String,
318 pub prompt: String,
319
320 pub fence_style: Style,
321 pub info_style: Style,
322 pub prompt_style: Style,
323 pub command_style: Style,
324 pub error_style: Style,
325}
326
327impl Default for Shell {
328 fn default() -> Shell {
330 Shell {
331 shell: Some(String::from("sh -c")),
332
333 dry_run: false,
334 sync: true,
335 print: true,
336 color: ColorOverride::default(),
337
338 fence: String::from("```"),
339 info: String::from("text"),
340 prompt: String::from("$ "),
341
342 fence_style: style("#555555").expect("style"),
343 info_style: style("#555555").expect("style"),
344 prompt_style: style("#555555").expect("style"),
345 command_style: style("#00ffff+bold").expect("style"),
346 error_style: style("#ff0000+bold+italic").expect("style"),
347 }
348 }
349}
350
351impl Shell {
352 pub fn run(&self, commands: &[Command]) -> Vec<Command> {
354 if self.sync {
355 if self.print {
356 self.print_fence(0);
357 println!("{}", self.info.style(self.info_style));
358 }
359
360 let mut r = vec![];
361 let mut error = None;
362
363 for (i, command) in commands.iter().enumerate() {
364 if i > 0 && self.print && !self.dry_run {
365 println!();
366 }
367
368 let result = self.run1(command);
369
370 if let Some(code) = &result.code {
371 if !result.codes.contains(code) {
372 error = Some(format!(
373 "**Command `{}` exited with code: `{code}`!**",
374 result.command,
375 ));
376 }
377 } else if !self.dry_run {
378 error = Some(format!(
379 "**Command `{}` was killed by a signal!**",
380 result.command,
381 ));
382 }
383
384 r.push(result);
385
386 if error.is_some() {
387 break;
388 }
389 }
390
391 if self.print {
392 self.print_fence(2);
393
394 if let Some(error) = error {
395 println!("{}\n", error.style(self.error_style));
396 }
397 }
398
399 r
400 } else {
401 commands
402 .par_iter()
403 .map(|command| self.run1(command))
404 .collect()
405 }
406 }
407
408 pub fn run1(&self, command: &Command) -> Command {
410 if self.print {
411 if !self.dry_run {
412 print!("{}", self.prompt.style(self.prompt_style));
413 }
414
415 println!(
416 "{}",
417 command
418 .command
419 .replace(" && ", " \\\n&& ")
420 .replace(" || ", " \\\n|| ")
421 .replace("; ", "; \\\n")
422 .style(self.command_style),
423 );
424 }
425
426 if self.dry_run {
427 return command.clone();
428 }
429
430 self.core(command)
431 }
432
433 pub fn pipe1(&self, command: &str) -> String {
435 let command = Command {
436 command: command.to_string(),
437 stdout: Pipe::string(),
438 ..Default::default()
439 };
440
441 let result = self.core(&command);
442
443 if let Pipe::String(Some(stdout)) = &result.stdout {
444 stdout.to_string()
445 } else {
446 String::new()
447 }
448 }
449
450 pub fn run1_async(&self, command: &Command) -> std::process::Child {
452 let (prog, args) = self.prepare(&command.command);
453
454 let mut cmd = std::process::Command::new(prog);
455 cmd.args(&args);
456
457 if matches!(command.stdin, Pipe::String(_)) {
458 cmd.stdin(std::process::Stdio::piped());
459 }
460
461 if matches!(command.stdout, Pipe::String(_) | Pipe::Null) {
462 cmd.stdout(std::process::Stdio::piped());
463 }
464
465 if matches!(command.stderr, Pipe::String(_) | Pipe::Null) {
466 cmd.stderr(std::process::Stdio::piped());
467 }
468
469 if self.print {
470 if let Pipe::String(Some(s)) = &command.stdin {
471 self.print_fence(0);
472 println!("{}", command.command.style(self.info_style));
473 println!("{s}");
474 self.print_fence(2);
475 self.print_fence(0);
476 println!("{}", self.info.style(self.info_style));
477 }
478 }
479
480 let mut child = cmd.spawn().unwrap();
481
482 if let Pipe::String(Some(s)) = &command.stdin {
483 let mut stdin = child.stdin.take().unwrap();
484 stdin.write_all(s.as_bytes()).unwrap();
485 }
486
487 child
488 }
489
490 pub fn core(&self, command: &Command) -> Command {
492 let mut child = self.run1_async(command);
493
494 let mut r = command.clone();
495
496 r.code = match child.wait() {
497 Ok(status) => status.code(),
498 Err(_e) => None,
499 };
500
501 if matches!(command.stdout, Pipe::String(_)) {
502 let mut stdout = String::new();
503 child.stdout.unwrap().read_to_string(&mut stdout).unwrap();
504 r.stdout = Pipe::String(Some(stdout));
505 }
506
507 if matches!(command.stderr, Pipe::String(_)) {
508 let mut stderr = String::new();
509 child.stderr.unwrap().read_to_string(&mut stderr).unwrap();
510 r.stderr = Pipe::String(Some(stderr));
511 }
512
513 if self.print {
514 if let Pipe::String(Some(_s)) = &command.stdin {
515 self.print_fence(2);
516 }
517 }
518
519 r
520 }
521
522 fn prepare(&self, command: &str) -> (String, Vec<String>) {
524 if let Some(s) = &self.shell {
525 let mut args = shlex::split(s).unwrap();
526 let prog = args.remove(0);
527 args.push(command.to_string());
528 (prog, args)
529 } else {
530 let mut args = shlex::split(command).unwrap();
532 let prog = args.remove(0);
533 (prog, args)
534 }
535 }
536
537 pub fn print_fence(&self, newlines: usize) {
539 print!(
540 "{}{}",
541 self.fence.style(self.fence_style),
542 "\n".repeat(newlines),
543 );
544 }
545
546 pub fn interactive_prompt(&self, previous: bool) {
548 if previous {
549 self.print_fence(2);
550 }
551
552 self.print_fence(0);
553 println!("{}", self.info.style(self.info_style));
554 print!("{}", self.prompt.style(self.prompt_style));
555
556 print_prefix(self.command_style);
558 std::io::stdout().flush().expect("flush");
559 }
560
561 pub fn interactive_prompt_reset(&self) {
563 print_suffix(self.command_style);
564 std::io::stdout().flush().expect("flush");
565 }
566
567 pub fn run_str(&self, commands: &[&str]) -> Vec<Command> {
569 self.run(&commands.iter().map(|x| Command::new(x)).collect::<Vec<_>>())
570 }
571}
572
573#[derive(Clone, Debug, PartialEq, Eq)]
576pub struct Command {
577 pub command: String,
578 pub stdin: Pipe,
579 pub codes: Vec<i32>,
580 pub stdout: Pipe,
581 pub stderr: Pipe,
582 pub code: Option<i32>,
583}
584
585impl Default for Command {
586 fn default() -> Command {
587 Command {
588 command: Default::default(),
589 stdin: Pipe::Null,
590 codes: vec![0],
591 stdout: Pipe::Stdout,
592 stderr: Pipe::Stderr,
593 code: Default::default(),
594 }
595 }
596}
597
598impl Command {
599 pub fn new(command: &str) -> Command {
600 Command {
601 command: command.to_string(),
602 ..Default::default()
603 }
604 }
605}