tinterm 0.2.0

A powerful library for vibrant solid and gradient text with shimmer animations in terminal outputs.
Documentation
use crate::color::Color;

/// Trait for applying color gradients to text.
///
/// This trait provides methods to create smooth color transitions across text,
/// both for foreground and background colors. Gradients can be applied to
/// single lines or multi-line text with different behaviors.
///
/// # Examples
///
/// ```
/// use tinterm::{Gradient, Color};
///
/// // Simple gradient from red to blue
/// let gradient_text = "Hello World".gradient(Color::RED, Color::BLUE, None);
///
/// // Background gradient
/// let bg_gradient = "Background".gradient_bg(Color::GREEN, Color::YELLOW, None);
///
/// // Multi-line gradient with block mode
/// let multiline = "Line 1\nLine 2".gradient(Color::CYAN, Color::MAGENTA, Some(true));
/// ```
pub trait Gradient {
    /// Creates a foreground color gradient across the text.
    ///
    /// # Arguments
    ///
    /// * `start_color` - The starting color of the gradient
    /// * `end_color` - The ending color of the gradient
    /// * `block` - Controls multi-line behavior:
    ///   - `None` or `Some(false)`: Gradient continues across line breaks
    ///   - `Some(true)`: Each line gets its own complete gradient
    ///
    /// # Examples
    ///
    /// ```
    /// use tinterm::{Gradient, Color};
    ///
    /// let text = "Rainbow text";
    /// let gradient = text.gradient(Color::RED, Color::BLUE, None);
    /// ```
    fn gradient(&self, start_color: Color, end_color: Color, block: Option<bool>) -> String;

    /// Creates a background color gradient across the text.
    ///
    /// # Arguments
    ///
    /// * `start_color` - The starting color of the gradient
    /// * `end_color` - The ending color of the gradient
    /// * `block` - Controls multi-line behavior (same as [`gradient`](Gradient::gradient))
    ///
    /// # Examples
    ///
    /// ```
    /// use tinterm::{Gradient, Color};
    ///
    /// let text = "Gradient background";
    /// let bg_gradient = text.gradient_bg(Color::GREEN, Color::PURPLE, None);
    /// ```
    fn gradient_bg(&self, start_color: Color, end_color: Color, block: Option<bool>) -> String;
}

impl Gradient for str {
    fn gradient(&self, start_color: Color, end_color: Color, block: Option<bool>) -> String {
        apply_gradient(self, start_color, end_color, block.unwrap_or(false))
    }

    fn gradient_bg(&self, start_color: Color, end_color: Color, block: Option<bool>) -> String {
        apply_gradient_bg(self, start_color, end_color, block.unwrap_or(false))
    }
}

fn apply_gradient(text: &str, start_color: Color, end_color: Color, block: bool) -> String {
    if text.is_empty() {
        return String::new();
    }

    let mut result = String::new();
    let mut visible_chars = 0;
    let mut total_visible = 0;

    // First pass: count visible characters
    let mut chars = text.chars();
    while let Some(c) = chars.next() {
        if c == '\x1b' {
            for next in chars.by_ref() {
                if next == 'm' {
                    break;
                }
            }
            continue;
        }
        if c != '\n' {
            total_visible += 1;
        }
    }

    // Second pass: apply gradient
    let mut chars = text.chars().peekable();
    while let Some(c) = chars.next() {
        if c == '\x1b' {
            let mut seq = String::from(c);
            while let Some(&next) = chars.peek() {
                seq.push(next);
                chars.next();
                if next == 'm' {
                    break;
                }
            }

            // Only skip foreground color sequences
            if !seq.contains("[38;2;") {
                result.push_str(&seq);
            }
            continue;
        }

        if c == '\n' {
            result.push(c);
            if block {
                visible_chars = 0;
            }
            continue;
        }

        let progress = if total_visible > 1 {
            visible_chars as f32 / (total_visible - 1) as f32
        } else {
            0.0
        };

        let color = start_color.interpolate(&end_color, progress);
        result.push_str(&format!("\x1b[38;2;{};{};{}m", color.r, color.g, color.b));
        result.push(c);

        visible_chars += 1;
    }

    result.push_str("\x1b[39m"); // Reset foreground color
    result
}

fn apply_gradient_bg(text: &str, start_color: Color, end_color: Color, block: bool) -> String {
    if text.is_empty() {
        return String::new();
    }

    let mut result = String::new();
    let mut visible_chars = 0;
    let mut total_visible = 0;

    // First pass: count visible characters
    let mut chars = text.chars();
    while let Some(c) = chars.next() {
        if c == '\x1b' {
            for next in chars.by_ref() {
                if next == 'm' {
                    break;
                }
            }
            continue;
        }
        if c != '\n' {
            total_visible += 1;
        }
    }

    // Second pass: apply gradient
    let mut chars = text.chars().peekable();
    while let Some(c) = chars.next() {
        if c == '\x1b' {
            let mut seq = String::from(c);
            while let Some(&next) = chars.peek() {
                seq.push(next);
                chars.next();
                if next == 'm' {
                    break;
                }
            }

            // Only skip background color sequences
            if !seq.contains("[48;2;") {
                result.push_str(&seq);
            }
            continue;
        }

        if c == '\n' {
            result.push_str("\x1b[49m"); // Reset background color at the end of each line
            result.push(c);
            if block {
                visible_chars = 0;
            }
            continue;
        }

        let progress = if total_visible > 1 {
            visible_chars as f32 / (total_visible - 1) as f32
        } else {
            0.0
        };

        let color = start_color.interpolate(&end_color, progress);
        result.push_str(&format!("\x1b[48;2;{};{};{}m", color.r, color.g, color.b));
        result.push(c);

        visible_chars += 1;
    }

    result.push_str("\x1b[49m"); // Reset background color
    result
}