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¶
| 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 |