Skip to main content

rmux_core/style/
colour.rs

1use std::fmt;
2
3use crate::input::{
4    colour_join_rgb, Colour, COLOUR_DEFAULT, COLOUR_FLAG_256, COLOUR_FLAG_RGB, COLOUR_NONE,
5    COLOUR_TERMINAL,
6};
7
8use super::grammar::strip_prefix_ci;
9
10const ANSI_5_NAME: &str = concat!("mag", "enta");
11const ANSI_95_NAME: &str = concat!("bright", "mag", "enta");
12
13/// Parses a tmux colour string into a [`Colour`].
14pub fn parse_colour(value: &str) -> Result<Colour, ColourParseError> {
15    let trimmed = value.trim();
16    let invalid = || ColourParseError::Invalid(value.to_owned());
17
18    if trimmed.is_empty() {
19        return Err(invalid());
20    }
21
22    // #rrggbb hex.
23    if let Some(hex) = trimmed.strip_prefix('#') {
24        if hex.len() != 6 || !hex.chars().all(|ch| ch.is_ascii_hexdigit()) {
25            return Err(invalid());
26        }
27        let red = u8::from_str_radix(&hex[0..2], 16).map_err(|_| invalid())?;
28        let green = u8::from_str_radix(&hex[2..4], 16).map_err(|_| invalid())?;
29        let blue = u8::from_str_radix(&hex[4..6], 16).map_err(|_| invalid())?;
30        return Ok(colour_join_rgb(red, green, blue));
31    }
32
33    // colour0-colour255 / color0-color255.
34    if let Some(suffix) =
35        strip_prefix_ci(trimmed, "colour").or_else(|| strip_prefix_ci(trimmed, "color"))
36    {
37        let index = suffix.parse::<u16>().map_err(|_| invalid())?;
38        if index > 255 {
39            return Err(invalid());
40        }
41        return Ok(COLOUR_FLAG_256 | i32::from(index));
42    }
43
44    // Bare decimal 0-255 (256-palette index).
45    if trimmed.chars().all(|ch| ch.is_ascii_digit()) {
46        let index = trimmed.parse::<u16>().map_err(|_| invalid())?;
47        if index > 255 {
48            return Err(invalid());
49        }
50        return Ok(COLOUR_FLAG_256 | i32::from(index));
51    }
52
53    // Named colours and sentinels.
54    match trimmed.to_ascii_lowercase().as_str() {
55        "none" => Ok(COLOUR_NONE),
56        "default" => Ok(COLOUR_DEFAULT),
57        "terminal" => Ok(COLOUR_TERMINAL),
58        "black" => Ok(0),
59        "red" => Ok(1),
60        "green" => Ok(2),
61        "yellow" => Ok(3),
62        "blue" => Ok(4),
63        value if value == ANSI_5_NAME => Ok(5),
64        "cyan" => Ok(6),
65        "white" => Ok(7),
66        "brightblack" => Ok(90),
67        "brightred" => Ok(91),
68        "brightgreen" => Ok(92),
69        "brightyellow" => Ok(93),
70        "brightblue" => Ok(94),
71        value if value == ANSI_95_NAME => Ok(95),
72        "brightcyan" => Ok(96),
73        "brightwhite" => Ok(97),
74        _ => Err(invalid()),
75    }
76}
77
78/// Returns the canonical tmux string form for `colour`.
79#[must_use]
80pub fn colour_to_string(colour: Colour) -> String {
81    if colour == COLOUR_NONE {
82        return "none".to_owned();
83    }
84    if colour & COLOUR_FLAG_RGB != 0 {
85        let red = ((colour >> 16) & 0xff) as u8;
86        let green = ((colour >> 8) & 0xff) as u8;
87        let blue = (colour & 0xff) as u8;
88        return format!("#{red:02x}{green:02x}{blue:02x}");
89    }
90    if colour & COLOUR_FLAG_256 != 0 {
91        return format!("colour{}", colour & 0xff);
92    }
93
94    match colour {
95        0 => "black".to_owned(),
96        1 => "red".to_owned(),
97        2 => "green".to_owned(),
98        3 => "yellow".to_owned(),
99        4 => "blue".to_owned(),
100        5 => ANSI_5_NAME.to_owned(),
101        6 => "cyan".to_owned(),
102        7 => "white".to_owned(),
103        COLOUR_DEFAULT => "default".to_owned(),
104        COLOUR_TERMINAL => "terminal".to_owned(),
105        90 => "brightblack".to_owned(),
106        91 => "brightred".to_owned(),
107        92 => "brightgreen".to_owned(),
108        93 => "brightyellow".to_owned(),
109        94 => "brightblue".to_owned(),
110        95 => ANSI_95_NAME.to_owned(),
111        96 => "brightcyan".to_owned(),
112        97 => "brightwhite".to_owned(),
113        // Non-named palette indices outside the bright range.
114        10..=89 | 98..=255 => format!("colour{colour}"),
115        _ => "default".to_owned(),
116    }
117}
118
119/// Colour parse failure.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum ColourParseError {
122    /// The colour token was invalid.
123    Invalid(String),
124}
125
126impl fmt::Display for ColourParseError {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self {
129            Self::Invalid(value) => write!(formatter, "invalid colour: {value}"),
130        }
131    }
132}
133
134impl std::error::Error for ColourParseError {}