1use atoi::FromRadix16;
2use std::{fmt::Display, io, str::FromStr};
3
4#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5pub struct ColorPoint {
6 pub color: Color,
7 pub transition: ColorTransition,
8 pub transition_time: u32,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
13pub enum ColorTransition {
14 None,
15 Linear,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub enum ColorProfile {
20 None,
21 Single(Color),
22 Multiple(Vec<ColorPoint>),
23}
24
25impl Default for ColorProfile {
26 fn default() -> Self {
27 Self::None
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub struct Color {
33 pub r: u8,
34 pub g: u8,
35 pub b: u8,
36}
37
38impl Color {
39 pub fn sysfs_rgb_string(&self, max_brightness: u32) -> String {
40 let Color { r, g, b } = *self;
41 if max_brightness == 255 {
42 format!("{r} {g} {b}")
43 } else {
44 let max = max_brightness as f32;
45 let scale = |value: u8| -> u32 { (value as f32 / 255.0 * max).clamp(0.0, max) as u32 };
46 format!("{} {} {}", scale(r), scale(g), scale(b))
47 }
48 }
49
50 pub fn sysfs_monochrome_string(&self, max_brightness: u32) -> String {
51 let Color { r, g, b } = *self;
52 let average = [r, g, b].into_iter().map(u16::from).sum::<u16>() / 3;
53 if max_brightness == 255 {
54 average.to_string()
55 } else {
56 let max = max_brightness as f32;
57 let value = (average as f32 / 255.0 * max).clamp(0.0, max) as u32;
58 value.to_string()
59 }
60 }
61
62 pub fn from_sysfs_rgb_value(values: [u32; 3], max_brightness: u32) -> Self {
63 if max_brightness == 255 {
64 let values: Vec<u8> = values
65 .into_iter()
66 .map(u8::try_from)
67 .map(Result::unwrap_or_default)
68 .collect();
69 Self {
70 r: values[0],
71 g: values[1],
72 b: values[2],
73 }
74 } else {
75 let max = max_brightness as f32;
76 let scale = |value: u32| -> u8 { (value as f32 / max * 255.0).clamp(0.0, 255.0) as u8 };
77 Self {
78 r: scale(values[0]),
79 g: scale(values[1]),
80 b: scale(values[2]),
81 }
82 }
83 }
84}
85
86impl Display for Color {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(f, "0x{:02X}{:02X}{:02X}", self.r, self.g, self.b)
89 }
90}
91
92impl FromStr for Color {
93 type Err = io::Error;
94
95 fn from_str(s: &str) -> Result<Self, Self::Err> {
96 if s.len() != 6 {
97 Err(io::Error::new(
98 io::ErrorKind::InvalidData,
99 "Incorrect length for 3x8-bit hexadecimal value",
100 ))
101 } else {
102 let r = u8::from_radix_16(s[0..2].as_bytes());
103 let g = u8::from_radix_16(s[2..4].as_bytes());
104 let b = u8::from_radix_16(s[4..6].as_bytes());
105
106 if r.1 == 2 && g.1 == 2 && b.1 == 2 {
107 Ok(Self {
108 r: r.0,
109 g: g.0,
110 b: b.0,
111 })
112 } else {
113 Err(io::Error::new(
114 io::ErrorKind::InvalidData,
115 "Incorrect value (not [0-9] or [A-F]) in hexadecimal value",
116 ))
117 }
118 }
119 }
120}
121
122#[cfg(test)]
123mod test {
124 use crate::color::Color;
125 use std::str::FromStr;
126
127 #[test]
128 fn color_from_string() {
129 let string = "000Fac";
130 let color = Color::from_str(string).unwrap();
131 assert_eq!(
132 color,
133 Color {
134 r: 0x00,
135 g: 0x0F,
136 b: 0xAC,
137 }
138 );
139 assert_eq!(color.to_string()[2..], string.to_ascii_uppercase());
140
141 Color::from_str("F00FF").unwrap_err();
142 Color::from_str("F").unwrap_err();
143 Color::from_str("INVLD!").unwrap_err();
144 }
145}