Skip to main content

rusty_bubbletea/
color.rs

1//! Cleanroom Rust port of upstream Go source file: `color.go`
2//! Upstream Target Tag / Version: `v2.0.8`
3//!
4//! <public-docs>
5//! # Color Messages & Requests
6//!
7//! Color requests (`RequestBackgroundColor`, `RequestForegroundColor`,
8//! `RequestCursorColor`) and response messages.
9//!
10//! The response messages wrap `rusty-ultraviolet`'s color events, which
11//! mirror the upstream `uv.ForegroundColorEvent` / `BackgroundColorEvent` /
12//! `CursorColorEvent` types (hex via OSC response parsing, darkness via the
13//! HSL-based `is_dark_color`).
14//! </public-docs>
15
16use crate::model::Cmd;
17
18/// The color value used by the color response messages.
19///
20/// This is an alias of the ultraviolet color event payload; use `to_hex` and
21/// `is_dark` through the message wrappers.
22pub type Color = rusty_x_ansi::color::RGBColor;
23
24/// RequestBackgroundColor is a command that requests the terminal background color.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct RequestBackgroundColorMsg;
27
28/// RequestBackgroundColor is a command that requests the terminal background color.
29pub fn request_background_color() -> Cmd {
30    Some(Box::new(|| Some(Box::new(RequestBackgroundColorMsg))))
31}
32
33/// RequestForegroundColorMsg is a message that requests the terminal foreground color.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct RequestForegroundColorMsg;
36
37/// RequestForegroundColor is a command that requests the terminal foreground color.
38pub fn request_foreground_color() -> Cmd {
39    Some(Box::new(|| Some(Box::new(RequestForegroundColorMsg))))
40}
41
42/// RequestCursorColorMsg is a message that requests the terminal cursor color.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct RequestCursorColorMsg;
45
46/// RequestCursorColor is a command that requests the terminal cursor color.
47pub fn request_cursor_color() -> Cmd {
48    Some(Box::new(|| Some(Box::new(RequestCursorColorMsg))))
49}
50
51/// ForegroundColorMsg represents a foreground color message. This message is
52/// emitted when the program requests the terminal foreground color with the
53/// `request_foreground_color` command.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct ForegroundColorMsg(pub Color);
56
57impl ForegroundColorMsg {
58    /// Returns the hex representation of the color.
59    pub fn to_hex(&self) -> String {
60        self.0.hex()
61    }
62
63    /// Returns whether the color is dark.
64    pub fn is_dark(&self) -> bool {
65        is_dark_color(self.0)
66    }
67}
68
69/// BackgroundColorMsg represents a background color message. This message is
70/// emitted when the program requests the terminal background color with the
71/// `request_background_color` command.
72///
73/// This is commonly used in `Model::init` to get the terminal background color
74/// for style definitions. For that you'll want to call `is_dark()` to determine
75/// if the color is dark or light. For example:
76///
77/// ```rust,ignore
78/// fn init(&self) -> Cmd { request_background_color() }
79///
80/// fn update(&mut self, msg: Box<dyn Msg>) -> Cmd {
81///     if let Some(bg) = msg.as_any().downcast_ref::<BackgroundColorMsg>() {
82///         self.styles = new_styles(bg.is_dark());
83///     }
84///     None
85/// }
86/// ```
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct BackgroundColorMsg(pub Color);
89
90impl BackgroundColorMsg {
91    /// Returns the hex representation of the color.
92    pub fn to_hex(&self) -> String {
93        self.0.hex()
94    }
95
96    /// Returns whether the color is dark.
97    pub fn is_dark(&self) -> bool {
98        is_dark_color(self.0)
99    }
100}
101
102/// CursorColorMsg represents a cursor color change message. This message is
103/// emitted when the program requests the terminal cursor color.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct CursorColorMsg(pub Color);
106
107impl CursorColorMsg {
108    /// Returns the hex representation of the color.
109    pub fn to_hex(&self) -> String {
110        self.0.hex()
111    }
112
113    /// Returns whether the color is dark.
114    pub fn is_dark(&self) -> bool {
115        is_dark_color(self.0)
116    }
117}
118
119/// is_dark_color returns whether the given color is dark, mirroring the
120/// upstream `uv.isDarkColor` (HSL lightness < 0.5).
121fn is_dark_color(c: rusty_x_ansi::color::RGBColor) -> bool {
122    let (_, _, l) = rgb_to_hsl(c.r, c.g, c.b);
123    l < 0.5
124}
125
126/// rgb_to_hsl converts an RGB triple to an HSL triple.
127fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f64, f64, f64) {
128    let rnot = f64::from(r) / 255.0;
129    let gnot = f64::from(g) / 255.0;
130    let bnot = f64::from(b) / 255.0;
131    let (cmax, cmin) = get_max_min(rnot, gnot, bnot);
132    let delta = cmax - cmin;
133    let l = (cmax + cmin) / 2.0;
134    let (h, s) = if delta == 0.0 {
135        (0.0, 0.0)
136    } else {
137        let h = if cmax == rnot {
138            60.0 * (((gnot - bnot) / delta).rem_euclid(6.0))
139        } else if cmax == gnot {
140            60.0 * (((bnot - rnot) / delta) + 2.0)
141        } else {
142            60.0 * (((rnot - gnot) / delta) + 4.0)
143        };
144        let h = if h < 0.0 { h + 360.0 } else { h };
145        let s = delta / (1.0 - (2.0 * l - 1.0).abs());
146        (h, s)
147    };
148    (h, round(s), round(l))
149}
150
151fn get_max_min(a: f64, b: f64, c: f64) -> (f64, f64) {
152    let (ma, mi) = if a > b { (a, b) } else { (b, a) };
153    if c > ma {
154        (c, mi)
155    } else if c < mi {
156        (ma, c)
157    } else {
158        (ma, mi)
159    }
160}
161
162fn round(x: f64) -> f64 {
163    (x * 1000.0).round() / 1000.0
164}