|
| 1 | +use crate::Color; |
| 2 | + |
| 3 | +/// Parses a color value that follows the `XParseColor` format. |
| 4 | +/// See https://www.x.org/releases/current/doc/libX11/libX11/libX11.html#Color_Strings |
| 5 | +/// for a reference of what `XParseColor` supports. |
| 6 | +/// |
| 7 | +/// Not all formats are supported, just the ones that are returned |
| 8 | +/// by the tested terminals. Feel free to open a PR if you encounter |
| 9 | +/// a terminal that returns a different format. |
| 10 | +pub(crate) fn xparsecolor(input: &str) -> Option<Color> { |
| 11 | + if let Some(stripped) = input.strip_prefix('#') { |
| 12 | + parse_sharp(stripped) |
| 13 | + } else if let Some(stripped) = input.strip_prefix("rgb:") { |
| 14 | + parse_rgb(stripped) |
| 15 | + } else { |
| 16 | + None |
| 17 | + } |
| 18 | +} |
| 19 | + |
| 20 | +/// From the `xparsecolor` man page: |
| 21 | +/// > For backward compatibility, an older syntax for RGB Device is supported, |
| 22 | +/// > but its continued use is not encouraged. The syntax is an initial sharp sign character |
| 23 | +/// > followed by a numeric specification, in one of the following formats: |
| 24 | +/// > |
| 25 | +/// > The R, G, and B represent single hexadecimal digits. |
| 26 | +/// > When fewer than 16 bits each are specified, they represent the most significant bits of the value |
| 27 | +/// > (unlike the `rgb:` syntax, in which values are scaled). |
| 28 | +/// > For example, the string `#3a7` is the same as `#3000a0007000`. |
| 29 | +fn parse_sharp(input: &str) -> Option<Color> { |
| 30 | + const NUM_COMPONENTS: usize = 3; |
| 31 | + let len = input.len(); |
| 32 | + if len % NUM_COMPONENTS == 0 && len <= NUM_COMPONENTS * 4 { |
| 33 | + let chunk_size = input.len() / NUM_COMPONENTS; |
| 34 | + let r = parse_channel_shifted(&input[0..chunk_size])?; |
| 35 | + let g = parse_channel_shifted(&input[chunk_size..chunk_size * 2])?; |
| 36 | + let b = parse_channel_shifted(&input[chunk_size * 2..])?; |
| 37 | + Some(Color { r, g, b }) |
| 38 | + } else { |
| 39 | + None |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +fn parse_channel_shifted(input: &str) -> Option<u16> { |
| 44 | + let value = u16::from_str_radix(input, 16).ok()?; |
| 45 | + Some(value << ((4 - input.len()) * 4)) |
| 46 | +} |
| 47 | + |
| 48 | +/// From the `xparsecolor` man page: |
| 49 | +/// > An RGB Device specification is identified by the prefix `rgb:` and conforms to the following syntax: |
| 50 | +/// > ```text |
| 51 | +/// > rgb:<red>/<green>/<blue> |
| 52 | +/// > |
| 53 | +/// > <red>, <green>, <blue> := h | hh | hhh | hhhh |
| 54 | +/// > h := single hexadecimal digits (case insignificant) |
| 55 | +/// > ``` |
| 56 | +/// > Note that *h* indicates the value scaled in 4 bits, |
| 57 | +/// > *hh* the value scaled in 8 bits, *hhh* the value scaled in 12 bits, |
| 58 | +/// > and *hhhh* the value scaled in 16 bits, respectively. |
| 59 | +fn parse_rgb(input: &str) -> Option<Color> { |
| 60 | + let mut parts = input.split('/'); |
| 61 | + let r = parse_channel_scaled(parts.next()?)?; |
| 62 | + let g = parse_channel_scaled(parts.next()?)?; |
| 63 | + let b = parse_channel_scaled(parts.next()?)?; |
| 64 | + if parts.next().is_none() { |
| 65 | + Some(Color { r, g, b }) |
| 66 | + } else { |
| 67 | + None |
| 68 | + } |
| 69 | +} |
| 70 | + |
| 71 | +fn parse_channel_scaled(input: &str) -> Option<u16> { |
| 72 | + let len = input.len(); |
| 73 | + if (1..=4).contains(&len) { |
| 74 | + let max = u32::pow(16, len as u32) - 1; |
| 75 | + let value = u32::from_str_radix(input, 16).ok()?; |
| 76 | + Some((u16::MAX as u32 * value / max) as u16) |
| 77 | + } else { |
| 78 | + None |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +#[cfg(test)] |
| 83 | +mod tests { |
| 84 | + use super::*; |
| 85 | + |
| 86 | + // Tests adapted from alacritty/vte: |
| 87 | + // https://github.com/alacritty/vte/blob/ed51aa19b7ad060f62a75ec55ebb802ced850b1a/src/ansi.rs#L2134 |
| 88 | + #[test] |
| 89 | + fn parses_valid_rgb_color() { |
| 90 | + assert_eq!( |
| 91 | + xparsecolor("rgb:f/e/d"), |
| 92 | + Some(Color { |
| 93 | + r: 0xffff, |
| 94 | + g: 0xeeee, |
| 95 | + b: 0xdddd, |
| 96 | + }) |
| 97 | + ); |
| 98 | + assert_eq!( |
| 99 | + xparsecolor("rgb:11/aa/ff"), |
| 100 | + Some(Color { |
| 101 | + r: 0x1111, |
| 102 | + g: 0xaaaa, |
| 103 | + b: 0xffff |
| 104 | + }) |
| 105 | + ); |
| 106 | + assert_eq!( |
| 107 | + xparsecolor("rgb:f/ed1/cb23"), |
| 108 | + Some(Color { |
| 109 | + r: 0xffff, |
| 110 | + g: 0xed1d, |
| 111 | + b: 0xcb23, |
| 112 | + }) |
| 113 | + ); |
| 114 | + assert_eq!( |
| 115 | + xparsecolor("rgb:ffff/0/0"), |
| 116 | + Some(Color { |
| 117 | + r: 0xffff, |
| 118 | + g: 0x0, |
| 119 | + b: 0x0 |
| 120 | + }) |
| 121 | + ); |
| 122 | + } |
| 123 | + |
| 124 | + #[test] |
| 125 | + fn fails_for_invalid_rgb_color() { |
| 126 | + assert!(xparsecolor("rgb:").is_none()); // Empty |
| 127 | + assert!(xparsecolor("rgb:f/f").is_none()); // Not enough channels |
| 128 | + assert!(xparsecolor("rgb:f/f/f/f").is_none()); // Too many channels |
| 129 | + assert!(xparsecolor("rgb:f//f").is_none()); // Empty channel |
| 130 | + assert!(xparsecolor("rgb:ffff/ffff/fffff").is_none()); // Too many digits for one channel |
| 131 | + } |
| 132 | + |
| 133 | + // Tests adapted from alacritty/vte: |
| 134 | + // https://github.com/alacritty/vte/blob/ed51aa19b7ad060f62a75ec55ebb802ced850b1a/src/ansi.rs#L2142 |
| 135 | + #[test] |
| 136 | + fn parses_valid_sharp_color() { |
| 137 | + assert_eq!( |
| 138 | + xparsecolor("#1af"), |
| 139 | + Some(Color { |
| 140 | + r: 0x1000, |
| 141 | + g: 0xa000, |
| 142 | + b: 0xf000, |
| 143 | + }) |
| 144 | + ); |
| 145 | + assert_eq!( |
| 146 | + xparsecolor("#1AF"), |
| 147 | + Some(Color { |
| 148 | + r: 0x1000, |
| 149 | + g: 0xa000, |
| 150 | + b: 0xf000, |
| 151 | + }) |
| 152 | + ); |
| 153 | + assert_eq!( |
| 154 | + xparsecolor("#11aaff"), |
| 155 | + Some(Color { |
| 156 | + r: 0x1100, |
| 157 | + g: 0xaa00, |
| 158 | + b: 0xff00 |
| 159 | + }) |
| 160 | + ); |
| 161 | + assert_eq!( |
| 162 | + xparsecolor("#110aa0ff0"), |
| 163 | + Some(Color { |
| 164 | + r: 0x1100, |
| 165 | + g: 0xaa00, |
| 166 | + b: 0xff00 |
| 167 | + }) |
| 168 | + ); |
| 169 | + assert_eq!( |
| 170 | + xparsecolor("#1100aa00ff00"), |
| 171 | + Some(Color { |
| 172 | + r: 0x1100, |
| 173 | + g: 0xaa00, |
| 174 | + b: 0xff00 |
| 175 | + }) |
| 176 | + ); |
| 177 | + assert_eq!( |
| 178 | + xparsecolor("#123456789ABC"), |
| 179 | + Some(Color { |
| 180 | + r: 0x1234, |
| 181 | + g: 0x5678, |
| 182 | + b: 0x9ABC |
| 183 | + }) |
| 184 | + ); |
| 185 | + } |
| 186 | + |
| 187 | + #[test] |
| 188 | + fn fails_for_invalid_sharp_color() { |
| 189 | + assert!(xparsecolor("#").is_none()); // Empty |
| 190 | + assert!(xparsecolor("#1234").is_none()); // Not divisible by three |
| 191 | + assert!(xparsecolor("#123456789ABCDEF").is_none()); // Too many components |
| 192 | + } |
| 193 | +} |
0 commit comments