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}