1use colored::Colorize;
9use indicatif::{ProgressBar, ProgressStyle};
10use std::path::Path;
11use std::time::Duration;
12
13pub fn clean_path<P: AsRef<Path>>(path: P) -> String {
15 let s = path.as_ref().display().to_string();
16 let s = if let Some(stripped) = s.strip_prefix(r"\\?\UNC\") {
20 format!(r"\\{stripped}")
21 } else if let Some(stripped) = s.strip_prefix(r"\\?\") {
22 stripped.to_string()
23 } else {
24 s
25 };
26 let s = if let Some(stripped) = s.strip_prefix("/private/var/") {
27 format!("/var/{stripped}")
28 } else if let Some(stripped) = s.strip_prefix("/private/tmp/") {
29 format!("/tmp/{stripped}")
30 } else {
31 s
32 };
33 s.replace("//", "/")
34}
35
36pub fn create_spinner(msg: &'static str) -> ProgressBar {
38 let pb = ProgressBar::new_spinner();
39 pb.set_style(
40 ProgressStyle::default_spinner()
41 .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
42 .template("{spinner:.cyan} {msg}")
43 .expect("Invalid progress bar template"),
44 );
45 pb.set_message(msg);
46 pb.enable_steady_tick(Duration::from_millis(80));
47 pb
48}
49
50fn highlight_code_spans(msg: &str) -> String {
58 if !msg.contains('`') {
59 return msg.to_string();
60 }
61 let mut out = String::with_capacity(msg.len() + 16);
62 let mut rest = msg;
63 while let Some(start) = rest.find('`') {
64 let Some(len) = rest[start + 1..].find('`') else {
65 break;
66 };
67 out.push_str(&rest[..start]);
68 out.push('`');
69 out.push_str(&rest[start + 1..start + 1 + len].cyan().to_string());
70 out.push('`');
71 rest = &rest[start + len + 2..];
72 }
73 out.push_str(rest);
74 out
75}
76
77pub fn print_success(msg: &str) {
79 println!("{} {}", "✓".green().bold(), highlight_code_spans(msg));
80}
81
82pub fn print_warning(msg: &str) {
88 eprintln!("{} {}", "⚠".yellow().bold(), highlight_code_spans(msg));
89}
90
91pub fn print_error(msg: &str) {
93 eprintln!("{} {}", "✗".red().bold(), highlight_code_spans(msg));
94}
95
96pub fn print_info(msg: &str) {
98 println!("{} {}", "→".blue().bold(), highlight_code_spans(msg));
99}
100
101pub fn print_notice(msg: &str) {
108 eprintln!("{} {}", "→".blue().bold(), highlight_code_spans(msg));
109}
110
111pub fn print_header(msg: &str) {
113 println!("\n{}", msg.cyan().bold().underline());
114}
115
116pub fn format_bytes_styled(bytes: u64) -> String {
118 format_bytes(bytes).green().bold().to_string()
119}
120
121pub fn styled_path<P: AsRef<Path>>(path: P) -> String {
123 clean_path(path).cyan().to_string()
124}
125
126pub fn print_banner() {
128 let art = format!(
129 r#"
130 ___ _____ __ __ ____ ____ _ _ _ _ _____
131| _ \ | ____|\ \ / / | _ \| _ \| | | | \ | | ____|
132| | | || _| \ \ / / | |_) | |_) | | | | \| | _|
133| |_| || |___ \ V / | __/| _ <| |_| | |\ | |___
134|____/ |_____| \_/ |_| |_| \_\\___/|_| \_|_____| v{}
135"#,
136 crate::constants::VERSION
137 );
138 println!("{}", art.truecolor(64, 224, 208).bold());
139}
140
141pub fn print_attribution() {
150 use std::io::IsTerminal;
151 if std::io::stdout().is_terminal() {
152 println!("{}", crate::constants::ATTRIBUTION_LINE.dimmed());
153 }
154}
155
156pub fn plural<'a>(count: usize, one: &'a str, many: &'a str) -> &'a str {
161 if count == 1 { one } else { many }
162}
163
164pub fn format_bytes(bytes: u64) -> String {
166 use humansize::{BINARY, format_size};
167 format_size(bytes, BINARY)
168}
169
170pub fn shared_note(shared_bytes: u64, adapter: &str) -> String {
178 if shared_bytes == 0 {
179 String::new()
180 } else {
181 format!(
182 " (+{} hardlinked into the {adapter} store — not counted, the store keeps them)",
183 format_bytes(shared_bytes)
184 )
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn test_format_bytes() {
194 assert_eq!(format_bytes(0), "0 B");
195 assert_eq!(format_bytes(1024), "1 KiB");
196 assert_eq!(format_bytes(1024 * 1024), "1 MiB");
197 assert_eq!(format_bytes(1024 * 1024 * 1024), "1 GiB");
198 }
199
200 #[test]
201 fn code_spans_survive_highlighting_verbatim_when_color_is_off() {
202 colored::control::set_override(false);
206 assert_eq!(
207 highlight_code_spans("run `devp setup` again"),
208 "run `devp setup` again"
209 );
210 assert_eq!(highlight_code_spans("no spans here"), "no spans here");
211 assert_eq!(
212 highlight_code_spans("odd `tick remains"),
213 "odd `tick remains"
214 );
215 assert_eq!(
216 highlight_code_spans("`a` and `b`, plus `stray"),
217 "`a` and `b`, plus `stray"
218 );
219 colored::control::unset_override();
220 }
221
222 #[test]
223 fn test_clean_path() {
224 assert_eq!(clean_path(r"\\?\C:\Users\krish"), r"C:\Users\krish");
225 assert_eq!(
226 clean_path(r"\\?\UNC\server\share\repo"),
227 r"\\server\share\repo"
228 );
229 assert_eq!(clean_path(r"/private/var/tmp/repo"), r"/var/tmp/repo");
230 assert_eq!(clean_path(r"//home//user//repo"), r"/home/user/repo");
231 }
232}