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