Skip to main content

devela/sys/os/term/grid/
color.rs

1// devela::sys::os::term::grid::color
2//
3//! Defines , [`TermColorKind`], [`TermColorMode`], [`TermColor`], [`TermColors`].
4//
5
6use crate::{AnsiColor, AnsiColor3, AnsiColor8};
7
8#[doc = crate::_tags!(term color)]
9/// The stored representation of a terminal color.
10#[doc = crate::_doc_meta!{location("sys/os/term")}]
11#[repr(u8)]
12#[must_use]
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
14pub enum TermColorKind {
15    /// Uses the terminal's default color.
16    #[default]
17    Default = 0,
18    /// Uses an indexed terminal palette color.
19    Indexed = 1,
20    /// Uses a 24-bit RGB color.
21    Rgb = 2,
22    /// Reserved for future extension.
23    Reserved = 3,
24}
25impl TermColorKind {
26    /// Creates a color kind from its packed representation.
27    pub const fn from_u8(value: u8) -> Self {
28        match value {
29            0 => Self::Default,
30            1 => Self::Indexed,
31            2 => Self::Rgb,
32            _ => Self::Reserved,
33        }
34    }
35}
36
37#[doc = crate::_tags!(term color)]
38/// The composition mode of one terminal color.
39#[doc = crate::_doc_meta!{location("sys/os/term")}]
40#[repr(u8)]
41#[must_use]
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
43pub enum TermColorMode {
44    /// Replaces the color beneath it.
45    #[default]
46    Opaque = 0,
47    /// Blends with the color beneath it.
48    Blend = 1,
49    /// Preserves the color beneath it.
50    Transparent = 2,
51    /// Selects a contrasting color from the resolved background.
52    Contrast = 3,
53}
54impl TermColorMode {
55    /// Creates a color mode from its packed representation.
56    pub const fn from_u8(value: u8) -> Self {
57        match value {
58            0 => Self::Opaque,
59            1 => Self::Blend,
60            2 => Self::Transparent,
61            _ => Self::Contrast,
62        }
63    }
64}
65
66crate::bitfield! {
67    #[doc = crate::_tags!(term color bit)]
68    /// A packed terminal color and its composition mode.
69    #[doc = crate::_doc_meta!{
70        location("sys/os/term"),
71        test_size_of(TermColor = 4|32),
72    }]
73    ///
74    /// The low 24 bits store either an RGB value or an indexed palette value.
75    /// The remaining defined bits select the representation and composition mode.
76    #[must_use]
77    pub struct TermColor(u32) {
78        /// RGB or indexed-color payload.
79        VALUE = 0..=23;
80        /// [`TermColorKind`] discriminant.
81        KIND = 24..=25;
82        /// [`TermColorMode`] discriminant.
83        MODE = 26..=27;
84    }
85    impl {
86        /// Uses the terminal's default color.
87        pub const DEFAULT: Self = Self::new();
88
89        /// Creates an opaque indexed terminal color.
90        pub const fn indexed(index: u8) -> Self {
91            Self::new().with_value(index as u32).with_kind(TermColorKind::Indexed as u32)
92        }
93        /// Creates an opaque 24-bit RGB terminal color.
94        pub const fn rgb(rgb: [u8; 3]) -> Self {
95            let [r, g, b] = rgb;
96            let value = ((r as u32) << 16) | ((g as u32) << 8) | b as u32;
97            Self::new().with_value(value).with_kind(TermColorKind::Rgb as u32)
98        }
99        /// Creates a color using the terminal default and the given mode.
100        pub const fn default_with_mode(mode: TermColorMode) -> Self {
101            Self::new().with_mode(mode as u32)
102        }
103        /// Returns the stored color representation.
104        pub const fn kind(self) -> TermColorKind {
105            TermColorKind::from_u8(self.get_kind() as u8)
106        }
107        /// Returns the color's composition mode.
108        pub const fn mode(self) -> TermColorMode {
109            TermColorMode::from_u8(self.get_mode() as u8)
110        }
111        /// Returns this color with the given composition mode.
112        pub const fn with_color_mode(self, mode: TermColorMode) -> Self {
113            self.with_mode(mode as u32)
114        }
115        /// Returns whether this uses the terminal's default color.
116        #[must_use]
117        pub const fn is_default(self) -> bool { matches!(self.kind(), TermColorKind::Default) }
118
119        /// Returns whether this uses an indexed terminal color.
120        #[must_use]
121        pub const fn is_indexed(self) -> bool { matches!(self.kind(), TermColorKind::Indexed) }
122
123        /// Returns whether this uses a 24-bit RGB color.
124        #[must_use]
125        pub const fn is_rgb(self) -> bool { matches!(self.kind(), TermColorKind::Rgb) }
126
127        /// Returns whether this color is opaque.
128        #[must_use]
129        pub const fn is_opaque(self) -> bool { matches!(self.mode(), TermColorMode::Opaque) }
130
131        /// Returns whether this color preserves the color beneath it.
132        #[must_use]
133        pub const fn is_transparent(self) -> bool {
134            matches!(self.mode(), TermColorMode::Transparent)
135        }
136        /// Returns the indexed palette value, when applicable.
137        #[must_use]
138        pub const fn index(self) -> Option<u8> {
139            if self.is_indexed() { Some(self.get_value() as u8) } else { None }
140        }
141        /// Returns the RGB components, when applicable.
142        #[must_use]
143        pub const fn rgb_components(self) -> Option<[u8; 3]> {
144            if self.is_rgb() {
145                let value = self.get_value();
146                Some([(value >> 16) as u8, (value >> 8) as u8, value as u8])
147            } else {
148                None
149            }
150        }
151        /// Returns whether all currently reserved bits are clear.
152        #[must_use]
153        pub const fn is_canonical(self) -> bool {
154            self.bits() & !Self::mask() == 0 && !matches!(self.kind(), TermColorKind::Reserved)
155        }
156        /// Returns this value with currently reserved bits cleared.
157        pub const fn canonicalized(self) -> Self { Self::from_bits(self.bits() & Self::mask()) }
158
159        /// Converts an ANSI color into a packed terminal color.
160        ///
161        /// Returns `None` for [`AnsiColor::None`], which represents absence
162        /// rather than a resolved terminal color.
163        #[must_use]
164        pub const fn from_ansi(color: AnsiColor) -> Option<Self> {
165            match color {
166                AnsiColor::None => None,
167                AnsiColor::Default => Some(Self::DEFAULT),
168                AnsiColor::Dark(color) => Some(Self::indexed(color as u8)),
169                AnsiColor::Bright(color) => Some(Self::indexed(color as u8 + 8)),
170                AnsiColor::Palette(color) => Some(Self::indexed(color.0)),
171                AnsiColor::Rgb(rgb) => Some(Self::rgb(rgb)),
172            }
173        }
174        /// Converts an opaque color into its ANSI representation.
175        ///
176        /// Non-opaque and reserved colors return `None`.
177        #[must_use]
178        pub const fn to_ansi(self) -> Option<AnsiColor> {
179            if !self.is_opaque() { return None; }
180            match self.kind() {
181                TermColorKind::Default => Some(AnsiColor::Default),
182                TermColorKind::Indexed => {
183                    let index = self.get_value() as u8;
184                    match index {
185                        0..=7 => Some(AnsiColor::Dark(AnsiColor3::from_u8(index))),
186                        8..=15 => {
187                            Some(AnsiColor::Bright(AnsiColor3::from_u8(index - 8)))
188                        }
189                        _ => Some(AnsiColor::Palette(AnsiColor8(index))),
190                    }
191                }
192                TermColorKind::Rgb => match self.rgb_components() {
193                    Some(rgb) => Some(AnsiColor::Rgb(rgb)),
194                    None => None,
195                },
196                TermColorKind::Reserved => None,
197            }
198        }
199    }
200}
201
202#[doc = crate::_tags!(term color)]
203/// Packed foreground and background terminal colors.
204#[doc = crate::_doc_meta!{
205    location("sys/os/term"),
206    test_size_of(TermColors = 8|64),
207}]
208///
209/// The foreground occupies the low 32 bits and the background the high 32 bits.
210#[must_use]
211#[repr(transparent)]
212#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
213pub struct TermColors {
214    bits: u64,
215}
216#[rustfmt::skip]
217impl TermColors {
218    /// Terminal-default foreground and background colors.
219    pub const DEFAULT: Self = Self::new(TermColor::DEFAULT, TermColor::DEFAULT);
220
221    /// Creates a foreground and background color pair.
222    pub const fn new(fg: TermColor, bg: TermColor) -> Self {
223        Self { bits: fg.bits() as u64 | ((bg.bits() as u64) << 32) }
224    }
225    /// Creates a color pair from its raw representation.
226    pub const fn from_bits(bits: u64) -> Self { Self { bits } }
227
228    /// Returns the raw packed representation.
229    #[must_use]
230    pub const fn bits(self) -> u64 { self.bits }
231
232    /// Returns the foreground color.
233    pub const fn fg(self) -> TermColor { TermColor::from_bits(self.bits as u32) }
234    /// Returns the background color.
235    pub const fn bg(self) -> TermColor { TermColor::from_bits((self.bits >> 32) as u32) }
236
237    /// Returns this pair with a new foreground color.
238    pub const fn with_fg(self, fg: TermColor) -> Self { Self::new(fg, self.bg()) }
239
240    /// Returns this pair with a new background color.
241    pub const fn with_bg(self, bg: TermColor) -> Self { Self::new(self.fg(), bg) }
242
243    /// Replaces the foreground color.
244    pub const fn set_fg(&mut self, fg: TermColor) { self.bits = Self::new(fg, self.bg()).bits; }
245    /// Replaces the background color.
246    pub const fn set_bg(&mut self, bg: TermColor) { self.bits = Self::new(self.fg(), bg).bits; }
247
248    /// Returns this pair with foreground and background exchanged.
249    pub const fn swapped(self) -> Self { Self::new(self.bg(), self.fg()) }
250
251    /// Exchanges the foreground and background colors.
252    pub const fn swap(&mut self) { self.bits = self.bits.rotate_left(32); }
253
254    /// Returns whether both colors are canonical.
255    #[must_use]
256    pub const fn is_canonical(self) -> bool { self.fg().is_canonical() && self.bg().is_canonical() }
257
258    /// Returns this pair with both colors canonicalized.
259    pub const fn canonicalized(self) -> Self {
260        Self::new(self.fg().canonicalized(), self.bg().canonicalized())
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn default_representation_is_zero() {
270        assert_eq!(TermColor::DEFAULT.bits(), 0);
271        assert_eq!(TermColors::DEFAULT.bits(), 0);
272        assert!(TermColor::DEFAULT.is_default());
273        assert!(TermColor::DEFAULT.is_opaque());
274    }
275    #[test]
276    fn indexed_roundtrip() {
277        let color = TermColor::indexed(173);
278        assert!(color.is_indexed());
279        assert_eq!(color.index(), Some(173));
280        assert_eq!(color.rgb_components(), None);
281        assert!(color.is_canonical());
282    }
283    #[test]
284    fn rgb_roundtrip() {
285        let color = TermColor::rgb([0x12, 0x34, 0x56]);
286        assert!(color.is_rgb());
287        assert_eq!(color.rgb_components(), Some([0x12, 0x34, 0x56]));
288        assert_eq!(color.get_value(), 0x12_34_56);
289        assert!(color.is_canonical());
290    }
291    #[test]
292    fn composition_mode_preserves_color() {
293        let color = TermColor::rgb([10, 20, 30]).with_color_mode(TermColorMode::Transparent);
294        assert_eq!(color.rgb_components(), Some([10, 20, 30]));
295        assert!(color.is_transparent());
296    }
297    #[test]
298    fn paired_colors() {
299        let fg = TermColor::indexed(7);
300        let bg = TermColor::rgb([1, 2, 3]);
301        let colors = TermColors::new(fg, bg);
302        assert_eq!(colors.fg(), fg);
303        assert_eq!(colors.bg(), bg);
304        assert_eq!(colors.swapped(), TermColors::new(bg, fg));
305    }
306    #[test]
307    fn ansi_roundtrip() {
308        let values = [
309            AnsiColor::Default,
310            AnsiColor::Dark(AnsiColor3::Red),
311            AnsiColor::Bright(AnsiColor3::Blue),
312            AnsiColor::Palette(AnsiColor8(203)),
313            AnsiColor::Rgb([10, 20, 30]),
314        ];
315        for ansi in values {
316            let color = TermColor::from_ansi(ansi).unwrap();
317            assert_eq!(color.to_ansi(), Some(ansi));
318        }
319        assert_eq!(TermColor::from_ansi(AnsiColor::None), None);
320    }
321    #[test]
322    fn reserved_bits_are_not_canonical() {
323        let color = TermColor::from_bits(0xF000_0000);
324        assert!(!color.is_canonical());
325        assert_eq!(color.canonicalized(), TermColor::DEFAULT);
326    }
327}