1use std::borrow::Cow;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Color {
13 Rgb(u8, u8, u8),
16 Indexed(u8),
18 Named(NamedColor),
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum NamedColor {
25 Black,
26 Red,
27 Green,
28 Yellow,
29 Blue,
30 Magenta,
31 Cyan,
32 White,
33 BrightBlack,
34 BrightRed,
35 BrightGreen,
36 BrightYellow,
37 BrightBlue,
38 BrightMagenta,
39 BrightCyan,
40 BrightWhite,
41}
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub struct Style {
45 pub fg: Option<Color>,
46 pub bg: Option<Color>,
47 pub bold: bool,
48 pub dim: bool,
49 pub italic: bool,
50 pub underline: bool,
51 pub strike: bool,
52 pub reverse: bool,
53}
54
55impl Style {
56 pub const PLAIN: Style = Style {
57 fg: None,
58 bg: None,
59 bold: false,
60 dim: false,
61 italic: false,
62 underline: false,
63 strike: false,
64 reverse: false,
65 };
66
67 pub const fn fg(color: Color) -> Self {
68 Style {
69 fg: Some(color),
70 ..Style::PLAIN
71 }
72 }
73
74 pub const fn bold() -> Self {
75 Style {
76 bold: true,
77 ..Style::PLAIN
78 }
79 }
80
81 pub const fn with_bold(mut self) -> Self {
82 self.bold = true;
83 self
84 }
85
86 pub const fn with_dim(mut self) -> Self {
87 self.dim = true;
88 self
89 }
90
91 pub const fn with_italic(mut self) -> Self {
92 self.italic = true;
93 self
94 }
95
96 pub const fn with_underline(mut self) -> Self {
97 self.underline = true;
98 self
99 }
100
101 pub const fn with_strike(mut self) -> Self {
102 self.strike = true;
103 self
104 }
105
106 pub const fn with_fg(mut self, color: Color) -> Self {
107 self.fg = Some(color);
108 self
109 }
110
111 pub fn over(self, other: Style) -> Style {
115 Style {
116 fg: other.fg.or(self.fg),
117 bg: other.bg.or(self.bg),
118 bold: self.bold || other.bold,
119 dim: self.dim || other.dim,
120 italic: self.italic || other.italic,
121 underline: self.underline || other.underline,
122 strike: self.strike || other.strike,
123 reverse: self.reverse || other.reverse,
124 }
125 }
126
127 pub fn is_plain(&self) -> bool {
128 *self == Style::PLAIN
129 }
130}
131
132#[derive(Clone, Debug, PartialEq)]
134pub struct Segment<'a> {
135 pub text: Cow<'a, str>,
136 pub style: Style,
137 pub link: Option<Cow<'a, str>>,
140}
141
142impl<'a> Segment<'a> {
143 pub fn new(text: impl Into<Cow<'a, str>>, style: Style) -> Self {
144 Segment {
145 text: text.into(),
146 style,
147 link: None,
148 }
149 }
150
151 pub fn plain(text: impl Into<Cow<'a, str>>) -> Self {
152 Segment::new(text, Style::PLAIN)
153 }
154
155 pub fn with_link(mut self, href: impl Into<Cow<'a, str>>) -> Self {
156 self.link = Some(href.into());
157 self
158 }
159}
160
161#[derive(Clone, Debug, Default, PartialEq)]
164pub struct Line<'a> {
165 pub segments: Vec<Segment<'a>>,
166 pub width: usize,
167}
168
169impl<'a> Line<'a> {
170 pub fn empty() -> Self {
171 Line::default()
172 }
173
174 pub fn from_segments(segments: Vec<Segment<'a>>, width: usize) -> Self {
175 Line { segments, width }
176 }
177
178 pub fn to_plain_string(&self) -> String {
180 self.segments.iter().map(|s| s.text.as_ref()).collect()
181 }
182
183 pub fn is_blank(&self) -> bool {
184 self.segments
185 .iter()
186 .all(|s| s.text.chars().all(char::is_whitespace))
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn over_accumulates_attributes_and_prefers_the_new_color() {
196 let base = Style::bold().with_fg(Color::Named(NamedColor::Red));
197 let top = Style::PLAIN
198 .with_italic()
199 .with_fg(Color::Named(NamedColor::Blue));
200 let merged = base.over(top);
201 assert!(merged.bold, "the base's bold must survive");
202 assert!(merged.italic);
203 assert_eq!(merged.fg, Some(Color::Named(NamedColor::Blue)));
204 }
205
206 #[test]
207 fn over_keeps_the_base_color_when_the_new_one_is_unset() {
208 let base = Style::fg(Color::Named(NamedColor::Red));
209 let merged = base.over(Style::bold());
210 assert_eq!(merged.fg, Some(Color::Named(NamedColor::Red)));
211 assert!(merged.bold);
212 }
213
214 #[test]
215 fn to_plain_string_concatenates() {
216 let line = Line::from_segments(
217 vec![Segment::plain("ab"), Segment::new("cd", Style::bold())],
218 4,
219 );
220 assert_eq!(line.to_plain_string(), "abcd");
221 }
222}