Skip to content

ripgrep crates/printer/src/color.rs: Code Companion

Reference code for the Terminal Colors lecture. Sections correspond to the lecture document.


Section 1: Platform-Aware Default Colors

/// Returns a default set of color specifications.
///
/// This may change over time, but the color choices are meant to be fairly
/// conservative that work across terminal themes.
pub fn default_color_specs() -> Vec<UserColorSpec> {
    vec![
        // Conditional compilation: only one of these is included in the binary
        #[cfg(unix)]
        "path:fg:magenta".parse().unwrap(),
        #[cfg(windows)]
        "path:fg:cyan".parse().unwrap(),

        // These apply on all platforms
        "line:fg:green".parse().unwrap(),

        // Match gets two specifications: color AND style
        // Later specs don't replace earlier ones—they merge
        "match:fg:red".parse().unwrap(),
        "match:style:bold".parse().unwrap(),
    ]
}

The #[cfg(...)] attributes are evaluated at compile time, not runtime. The .parse().unwrap() calls are safe here because these are hardcoded valid specifications—a panic would indicate a bug in the defaults themselves.


Section 2: A Domain-Specific Error Type

/// An error that can occur when parsing color specifications.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ColorError {
    /// This occurs when an unrecognized output type is used.
    UnrecognizedOutType(String),      // e.g., "pth" instead of "path"
    /// This occurs when an unrecognized spec type is used.
    UnrecognizedSpecType(String),     // e.g., "color" instead of "fg"
    /// This occurs when an unrecognized color name is used.
    UnrecognizedColor(String, String), // Carries both the invalid input and the message
    /// This occurs when an unrecognized style attribute is used.
    UnrecognizedStyle(String),        // e.g., "bolder" instead of "bold"
    /// This occurs when the format of a color specification is invalid.
    InvalidFormat(String),            // Wrong number of colons, missing parts
}

impl std::fmt::Display for ColorError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match *self {
            ColorError::UnrecognizedOutType(ref name) => write!(
                f,
                "unrecognized output type '{}'. Choose from: \
                 path, line, column, match, highlight.",
                name,
            ),
            ColorError::UnrecognizedSpecType(ref name) => write!(
                f,
                "unrecognized spec type '{}'. Choose from: \
                 fg, bg, style, none.",
                name,
            ),
            // Each message includes the invalid input AND valid alternatives
            ColorError::UnrecognizedStyle(ref name) => write!(
                f,
                "unrecognized style attribute '{}'. Choose from: \
                 nobold, bold, nointense, intense, nounderline, \
                 underline, noitalic, italic.",
                name,
            ),
            // ... other variants
        }
    }
}

Each variant stores the problematic input string, enabling actionable error messages. The Display implementation constructs complete sentences that guide users toward valid input.


Section 3: The Two-Layer Specification Model

/// A merged set of color specifications.
///
/// This set represents the various color types supported by the printers.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ColorSpecs {
    path: ColorSpec,      // Ready-to-use ColorSpec from termcolor
    line: ColorSpec,
    column: ColorSpec,
    matched: ColorSpec,
    highlight: ColorSpec,
}

/// A single color specification provided by the user.
///
/// ## Format
/// The format is a triple: `{type}:{attribute}:{value}`
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UserColorSpec {
    ty: OutType,          // WHERE the color applies (path, line, etc.)
    value: SpecValue,     // WHAT to do (set fg, set bg, apply style, clear)
}

UserColorSpec preserves user intent as structured data. ColorSpecs is the merged result—the printer asks "what color for paths?" and gets a direct answer without understanding the merge logic.


Section 4: Internal Type Vocabulary

/// The set of configurable portions of ripgrep's output.
#[derive(Clone, Debug, Eq, PartialEq)]
enum OutType {
    Path,       // File paths in output
    Line,       // Line numbers
    Column,     // Column numbers
    Match,      // The matched text itself
    Highlight,  // Entire lines containing matches
}

/// The specification type (what kind of modification).
#[derive(Clone, Debug, Eq, PartialEq)]
enum SpecType {
    Fg,     // Foreground color
    Bg,     // Background color
    Style,  // Text style (bold, italic, etc.)
    None,   // Clear all styling
}

/// The actual value given by the specification.
#[derive(Clone, Debug, Eq, PartialEq)]
enum SpecValue {
    None,           // Instruction to clear
    Fg(Color),      // A foreground color to set
    Bg(Color),      // A background color to set
    Style(Style),   // A style modification
}

/// The set of available styles for use in the terminal.
#[derive(Clone, Debug, Eq, PartialEq)]
enum Style {
    Bold,
    NoBold,         // Negations allow unsetting styles
    Intense,
    NoIntense,
    Underline,
    NoUnderline,
    Italic,
    NoItalic,
}

SpecType is used during parsing, while SpecValue represents the parsed result. The Style enum includes both positive and negative variants, enabling users to override defaults.


Section 5: The Merge Strategy

impl ColorSpecs {
    /// Create color specifications from a list of user supplied specifications.
    pub fn new(specs: &[UserColorSpec]) -> ColorSpecs {
        let mut merged = ColorSpecs::default();  // Start empty
        for spec in specs {
            // Route each spec to the appropriate field
            match spec.ty {
                OutType::Path => spec.merge_into(&mut merged.path),
                OutType::Line => spec.merge_into(&mut merged.line),
                OutType::Column => spec.merge_into(&mut merged.column),
                OutType::Match => spec.merge_into(&mut merged.matched),
                OutType::Highlight => spec.merge_into(&mut merged.highlight),
            }
        }
        merged
    }

    /// Create defaults WITH color (distinct from Default which has NO color).
    pub fn default_with_color() -> ColorSpecs {
        ColorSpecs::new(&default_color_specs())
    }
}

impl SpecValue {
    /// Merge this spec value into the given color specification.
    fn merge_into(&self, cspec: &mut ColorSpec) {
        match *self {
            // None clears EVERYTHING—the escape hatch
            SpecValue::None => cspec.clear(),
            // Colors are additive—set just the fg or bg
            SpecValue::Fg(ref color) => {
                cspec.set_fg(Some(color.clone()));
            }
            SpecValue::Bg(ref color) => {
                cspec.set_bg(Some(color.clone()));
            }
            // Styles are also additive
            SpecValue::Style(ref style) => match *style {
                Style::Bold => { cspec.set_bold(true); }
                Style::NoBold => { cspec.set_bold(false); }
                // ... other styles
            },
        }
    }
}

The key insight: merge_into modifies rather than replaces. Specifying match:fg:red then match:style:bold results in red bold text. Use match:none first to clear and start fresh.


Section 6: Parsing with FromStr

impl std::str::FromStr for UserColorSpec {
    type Err = ColorError;

    fn from_str(s: &str) -> Result<UserColorSpec, ColorError> {
        let pieces: Vec<&str> = s.split(':').collect();

        // Must have 2 or 3 parts: "type:none" or "type:attr:value"
        if pieces.len() <= 1 || pieces.len() > 3 {
            return Err(ColorError::InvalidFormat(s.to_string()));
        }

        // Each piece has its own FromStr—errors are specific
        let otype: OutType = pieces[0].parse()?;

        match pieces[1].parse()? {
            SpecType::None => {
                // "path:none" — no third component needed
                Ok(UserColorSpec { ty: otype, value: SpecValue::None })
            }
            SpecType::Style => {
                if pieces.len() < 3 {
                    return Err(ColorError::InvalidFormat(s.to_string()));
                }
                let style: Style = pieces[2].parse()?;
                Ok(UserColorSpec { ty: otype, value: SpecValue::Style(style) })
            }
            SpecType::Fg => {
                if pieces.len() < 3 {
                    return Err(ColorError::InvalidFormat(s.to_string()));
                }
                // Color parsing delegated to termcolor, errors wrapped
                let color: Color =
                    pieces[2].parse().map_err(ColorError::from_parse_error)?;
                Ok(UserColorSpec { ty: otype, value: SpecValue::Fg(color) })
            }
            // Bg follows same pattern as Fg...
        }
    }
}

impl std::str::FromStr for OutType {
    type Err = ColorError;

    fn from_str(s: &str) -> Result<OutType, ColorError> {
        // Case-insensitive: "PATH", "Path", "path" all work
        match &*s.to_lowercase() {
            "path" => Ok(OutType::Path),
            "line" => Ok(OutType::Line),
            "column" => Ok(OutType::Column),
            "match" => Ok(OutType::Match),
            "highlight" => Ok(OutType::Highlight),
            _ => Err(ColorError::UnrecognizedOutType(s.to_string())),
        }
    }
}

The ? operator propagates specific errors up the chain. The &*s.to_lowercase() pattern converts String to &str for matching while ensuring case-insensitive comparison.


Section 7: Applying Specifications to Terminal State

impl SpecValue {
    fn merge_into(&self, cspec: &mut ColorSpec) {
        match *self {
            SpecValue::None => cspec.clear(),

            SpecValue::Fg(ref color) => {
                // set_fg takes Option<Color>—Some sets, None would clear
                cspec.set_fg(Some(color.clone()));
            }
            SpecValue::Bg(ref color) => {
                cspec.set_bg(Some(color.clone()));
            }

            SpecValue::Style(ref style) => match *style {
                // Each style is a boolean toggle
                Style::Bold => { cspec.set_bold(true); }
                Style::NoBold => { cspec.set_bold(false); }
                Style::Intense => { cspec.set_intense(true); }
                Style::NoIntense => { cspec.set_intense(false); }
                Style::Underline => { cspec.set_underline(true); }
                Style::NoUnderline => { cspec.set_underline(false); }
                Style::Italic => { cspec.set_italic(true); }
                Style::NoItalic => { cspec.set_italic(false); }
            },
        }
    }
}

The termcolor::ColorSpec type accumulates settings. Each call modifies one aspect, leaving others unchanged—this is what enables the additive merge behavior.


Quick Reference

Color Specification Format

{type}:{attribute}:{value}
Component Valid Values
{type} path, line, column, match, highlight
{attribute} fg, bg, style, none
{value} (colors) black, blue, green, red, cyan, magenta, yellow, white, or N (0-255), or R,G,B
{value} (styles) bold, nobold, intense, nointense, underline, nounderline, italic, noitalic

Type Hierarchy

User Input ("path:fg:magenta")
        ↓ parse via FromStr
UserColorSpec { ty: OutType, value: SpecValue }
        ↓ merge via ColorSpecs::new()
ColorSpecs { path: ColorSpec, line: ColorSpec, ... }
        ↓ access via .path(), .matched(), etc.
termcolor::ColorSpec (used by printer)

Key Methods

Method Purpose
ColorSpecs::new(&[UserColorSpec]) Merge multiple specs into final config
ColorSpecs::default_with_color() Get platform-appropriate defaults
UserColorSpec::to_color_spec() Convert single spec to termcolor type
SpecValue::merge_into(&mut ColorSpec) Apply one spec additively