upstream-rs 2.6.0

Fetch package updates directly from the source.
Documentation
use console::{StyledObject, style};
use std::fmt;

pub fn title(text: impl fmt::Display) -> StyledObject<String> {
    style(text.to_string()).cyan().bold()
}

pub fn section(text: impl fmt::Display) -> StyledObject<String> {
    style(text.to_string()).bold()
}

pub fn meta(text: impl fmt::Display) -> StyledObject<String> {
    style(text.to_string()).dim()
}

pub fn success(text: impl fmt::Display) -> StyledObject<String> {
    style(text.to_string()).green()
}

pub fn warning(text: impl fmt::Display) -> StyledObject<String> {
    style(text.to_string()).yellow()
}

pub fn kv(label: &str, value: impl fmt::Display) {
    println!("  {:<13} {}", meta(format!("{label}:")), value);
}

pub fn action_note(text: impl fmt::Display) {
    println!("  {}", meta(text));
}

pub fn divider(width: usize) -> String {
    "-".repeat(width)
}

pub fn progress_bar(done: u64, total: u64, width: usize) -> String {
    if width == 0 {
        return "[]".to_string();
    }
    if total == 0 {
        return format!("[{}]", "?".repeat(width));
    }

    let capped = done.min(total);
    let filled = ((capped as u128 * width as u128) / total as u128) as usize;
    if filled >= width {
        return format!("[{}]", "=".repeat(width));
    }

    format!(
        "[{}>{}]",
        "=".repeat(filled),
        " ".repeat(width.saturating_sub(filled + 1))
    )
}

pub fn truncate_visible(value: &str, max: usize) -> String {
    if max == 0 {
        return String::new();
    }

    let mut chars = value.chars().peekable();
    let mut out = String::new();
    let mut visible = 0;
    let mut saw_escape = false;
    let mut truncated = false;

    while let Some(ch) = chars.next() {
        if ch == '\x1b' {
            saw_escape = true;
            out.push(ch);
            copy_ansi_escape(&mut chars, &mut out);
            continue;
        }

        if visible >= max {
            truncated = true;
            break;
        }

        out.push(ch);
        visible += 1;
    }

    if saw_escape && truncated {
        out.push_str("\x1b[0m");
    }

    out
}

fn copy_ansi_escape<I>(chars: &mut std::iter::Peekable<I>, out: &mut String)
where
    I: Iterator<Item = char>,
{
    match chars.next() {
        Some('[') => {
            out.push('[');
            for ch in chars.by_ref() {
                out.push(ch);
                if ('@'..='~').contains(&ch) {
                    break;
                }
            }
        }
        Some(']') => {
            out.push(']');
            while let Some(ch) = chars.next() {
                out.push(ch);
                if ch == '\x07' {
                    break;
                }
                if ch == '\x1b' && chars.peek() == Some(&'\\') {
                    out.push(chars.next().expect("peeked OSC terminator"));
                    break;
                }
            }
        }
        Some(ch) => out.push(ch),
        None => {}
    }
}

pub fn truncate_end(value: &str, max: usize) -> String {
    let char_count = value.chars().count();
    if char_count <= max {
        return value.to_string();
    }
    if max <= 3 {
        return ".".repeat(max);
    }

    let mut out = String::new();
    for ch in value.chars().take(max - 3) {
        out.push(ch);
    }
    out.push_str("...");
    out
}

pub fn truncate_middle(value: &str, max: usize) -> String {
    let char_count = value.chars().count();
    if char_count <= max {
        return value.to_string();
    }
    if max <= 3 {
        return ".".repeat(max);
    }

    let keep = max - 3;
    let prefix_len = keep / 2;
    let suffix_len = keep - prefix_len;
    let prefix: String = value.chars().take(prefix_len).collect();
    let suffix: String = value
        .chars()
        .rev()
        .take(suffix_len)
        .collect::<Vec<_>>()
        .into_iter()
        .rev()
        .collect();
    format!("{prefix}...{suffix}")
}

#[cfg(test)]
mod tests {
    use super::{progress_bar, truncate_visible};

    #[test]
    fn progress_bar_renders_fixed_width_ascii_progress() {
        assert_eq!(progress_bar(0, 100, 6), "[>     ]");
        assert_eq!(progress_bar(50, 100, 6), "[===>  ]");
        assert_eq!(progress_bar(100, 100, 6), "[======]");
        assert_eq!(progress_bar(0, 0, 6), "[??????]");
        assert_eq!(progress_bar(10, 100, 0), "[]");
    }

    #[test]
    fn truncate_visible_ignores_ansi_sequences() {
        let truncated = truncate_visible("\x1b[31mabcdef\x1b[0m", 3);

        assert_eq!(console::strip_ansi_codes(&truncated), "abc");
        assert!(truncated.starts_with("\x1b[31m"));
        assert!(truncated.ends_with("\x1b[0m"));
    }

    #[test]
    fn truncate_visible_keeps_short_colored_line_intact() {
        let line = "\x1b[1mbold\x1b[0m";

        assert_eq!(truncate_visible(line, 10), line);
    }

    #[test]
    fn truncate_visible_does_not_split_osc_sequences() {
        let truncated = truncate_visible(
            "\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\ tail",
            4,
        );

        assert!(truncated.contains("\x1b]8;;https://example.com\x1b\\"));
        assert!(truncated.contains("link"));
        assert!(truncated.contains("\x1b]8;;\x1b\\"));
        assert!(!truncated.contains("tail"));
    }
}