use std::fmt;
use anstyle::{AnsiColor, Style};
use pmpx_engine::Plan;
pub const ERROR: Style = AnsiColor::Red.on_default().bold();
pub const ERROR_BODY: Style = AnsiColor::Red.on_default();
pub const PM: Style = AnsiColor::Green.on_default().bold();
pub const LABEL: Style = Style::new().bold();
pub const DIM: Style = Style::new().dimmed();
pub fn paint<T: fmt::Display>(style: Style, text: T) -> Painted<T> {
Painted { style, text }
}
pub fn padded(style: Style, width: usize, text: impl fmt::Display) -> Painted<String> {
paint(style, format!("{text:<width$}"))
}
pub fn label(width: usize, text: &str) -> Painted<String> {
padded(LABEL, width, text)
}
pub fn announce_starting(plan: &Plan) -> String {
let program = plan.program.to_string_lossy();
let args: String = plan
.args
.iter()
.map(|arg| format!(" {}", arg.to_string_lossy()))
.collect();
format!("{}{}{}", paint(DIM, "pmpx -> "), paint(PM, program), args,)
}
pub struct Painted<T> {
style: Style,
text: T,
}
impl<T: fmt::Display> fmt::Display for Painted<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{}{}",
self.style.render(),
self.text,
self.style.render_reset()
)
}
}
pub(crate) fn family_label(family: &str) -> &str {
match family {
"node" => "Node / frontend",
"rust" => "Rust",
"python" => "Python",
"go" => "Go",
"jvm" => "JVM",
"dotnet" => ".NET",
"php" => "PHP",
"ruby" => "Ruby",
other => other,
}
}
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use super::*;
#[test]
fn paint_keeps_the_text() {
assert!(paint(DIM, "x").to_string().contains('x'));
assert!(paint(ERROR, "boom").to_string().contains("boom"));
}
#[test]
fn padded_keeps_the_text_and_the_padding() {
assert!(padded(PM, 6, "pnpm").to_string().contains("pnpm "));
assert!(label(12, "Start").to_string().contains("Start "));
}
#[test]
fn announce_starting_keeps_every_part() {
let plan = Plan::new(OsString::from("cargo"))
.arg(OsString::from("test"))
.arg(OsString::from("--release"));
let line = announce_starting(&plan);
assert!(line.contains("pmpx -> "), "the dim prefix: {line}");
assert!(line.contains("cargo"), "the program: {line}");
assert!(line.contains("test"), "the first arg: {line}");
assert!(line.contains("--release"), "the second arg: {line}");
}
#[test]
fn announce_starting_with_no_args_omits_the_gap() {
let plan = Plan::new(OsString::from("cargo"));
let line = announce_starting(&plan);
let stripped = strip_ansi(&line);
assert_eq!(stripped, "pmpx -> cargo", "{stripped:?}");
}
fn strip_ansi(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(idx) = rest.find('\x1b') {
out.push_str(&rest[..idx]);
let after = &rest[idx + 1..];
if let Some(after_stripped) = after.strip_prefix('[') {
let end = after_stripped
.find(|ch: char| {
let code = ch as u32;
(0x40..=0x7e).contains(&code)
})
.map(|idx| idx + 1)
.unwrap_or(after_stripped.len());
rest = &after_stripped[end..];
continue;
}
rest = after;
}
out.push_str(rest);
out
}
}