Skip to main content

bra/
output.rs

1use std::io::{self, IsTerminal};
2use std::path::Path;
3
4use anyhow::Error;
5use console::style;
6
7pub fn print_path(path: &Path) {
8    println!("{}", path.display());
9}
10
11pub fn print_success(message: &str) {
12    println!(
13        "{}",
14        style_stdout(message, |text| style(text).green().bold().to_string())
15    );
16}
17
18pub fn print_info(message: &str) {
19    println!(
20        "{}",
21        style_stdout(message, |text| style(text).cyan().to_string())
22    );
23}
24
25pub fn print_section(message: &str) {
26    println!(
27        "{}",
28        style_stdout(message, |text| style(text).green().bold().to_string())
29    );
30}
31
32pub fn print_list_entry(prefix: &str, message: &str, current: bool) {
33    let marker = if current {
34        style_stdout(prefix, |text| style(text).green().bold().to_string())
35    } else {
36        prefix.to_owned()
37    };
38    println!("{marker} {message}");
39}
40
41pub fn print_error(error: &Error) {
42    let mut chain = error.chain();
43    let Some(first) = chain.next() else {
44        return;
45    };
46
47    let first_message = clean_message(&first.to_string());
48    eprintln!(
49        "{} {}",
50        style_stderr("error:", |text| style(text).red().bold().to_string()),
51        first_message
52    );
53
54    let mut previous = first_message;
55    for cause in chain {
56        let message = clean_message(&cause.to_string());
57        if message.is_empty() || message == previous {
58            continue;
59        }
60        eprintln!(
61            "  {} {}",
62            style_stderr("caused by:", |text| style(text).yellow().to_string()),
63            message
64        );
65        previous = message;
66    }
67}
68
69pub fn clean_message(message: &str) -> String {
70    let mut current = message.trim();
71
72    loop {
73        let lowered = current.to_ascii_lowercase();
74        if let Some(rest) = lowered.strip_prefix("error:") {
75            let index = current.len() - rest.len();
76            current = current[index..].trim();
77            continue;
78        }
79        if let Some(rest) = lowered.strip_prefix("fatal:") {
80            let index = current.len() - rest.len();
81            current = current[index..].trim();
82            continue;
83        }
84        break;
85    }
86
87    current.to_owned()
88}
89
90fn style_stdout<F>(message: &str, style: F) -> String
91where
92    F: FnOnce(&str) -> String,
93{
94    if io::stdout().is_terminal() {
95        style(message)
96    } else {
97        message.to_owned()
98    }
99}
100
101fn style_stderr<F>(message: &str, style: F) -> String
102where
103    F: FnOnce(&str) -> String,
104{
105    if io::stderr().is_terminal() {
106        style(message)
107    } else {
108        message.to_owned()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn removes_error_prefixes() {
118        assert_eq!(
119            clean_message("error: No such remote 'origin'"),
120            "No such remote 'origin'"
121        );
122        assert_eq!(clean_message("fatal: bad revision"), "bad revision");
123        assert_eq!(clean_message("error: fatal: bad revision"), "bad revision");
124    }
125}