1 | //! This module permits to determine which ANSI sequences have to be added at a given position in |
2 | //! the format string, by saving the current tags in a "context". When a new tag is encountered, a |
3 | //! diff between the old state and the new state is performed to determine the right ANSI sequences |
4 | //! to add. |
5 | |
6 | use std::convert::TryFrom; |
7 | |
8 | use proc_macro2::Span; |
9 | |
10 | use crate::error::{Error, SpanError}; |
11 | |
12 | /// Stores all the current open tags encountered in the format string. |
13 | #[derive (Debug, PartialEq, Default)] |
14 | pub struct Context<'a>(Vec<ColorTag<'a>>); |
15 | |
16 | impl<'a> Context<'a> { |
17 | pub fn new() -> Self { |
18 | Self::default() |
19 | } |
20 | |
21 | /// Applies a group of tags to the current context, and returns a list of the terminfo |
22 | /// constants (available in the `color-print` package) to be added as named arguments at the |
23 | /// end of the format arguments. |
24 | /// |
25 | /// For each given tag: |
26 | /// - if the tag is an open tag, push it into the context; |
27 | /// - if it's a valid close tag, pop the last open tag. |
28 | #[cfg (feature = "terminfo" )] |
29 | pub fn terminfo_apply_tags( |
30 | &mut self, |
31 | tag_group: Vec<ColorTag<'a>>, |
32 | ) -> Result<Vec<String>, SpanError> { |
33 | let state_diff = self.apply_tags_and_get_diff(tag_group)?; |
34 | Ok(state_diff.terminfo_token_streams()) |
35 | } |
36 | |
37 | /// Applies a group of tags to the current context, and returns the ANSI sequences to be |
38 | /// added into the format string. |
39 | /// |
40 | /// For each given tag: |
41 | /// - if the tag is an open tag, push it into the context; |
42 | /// - if it's a valid close tag, pop the last open tag. |
43 | #[cfg (not(feature = "terminfo" ))] |
44 | pub fn ansi_apply_tags(&mut self, tag_group: Vec<ColorTag<'a>>) -> Result<String, SpanError> { |
45 | let state_diff = self.apply_tags_and_get_diff(tag_group)?; |
46 | Ok(state_diff.ansi_string()) |
47 | } |
48 | |
49 | /// Applies a group of tags to the current context, with no return on success. Used by the |
50 | /// macro [`untagged!()`]. |
51 | /// |
52 | /// For each given tag: |
53 | /// - if the tag is an open tag, push it into the context; |
54 | /// - if it's a valid close tag, pop the last open tag. |
55 | pub fn apply_tags(&mut self, tag_group: Vec<ColorTag<'a>>) -> Result<(), SpanError> { |
56 | self.apply_tags_and_get_diff(tag_group).map(|_| ()) |
57 | } |
58 | |
59 | /// Returns the actual color/style state, which is the result of the changes made by each tag |
60 | /// sequentially. |
61 | pub fn state(&self) -> State { |
62 | let mut state = State::default(); |
63 | for tag in &self.0 { |
64 | if let Some(ref color) = tag.change_set.foreground { |
65 | state.foreground = ExtColor::Color(color.clone()); |
66 | } |
67 | if let Some(ref color) = tag.change_set.background { |
68 | state.background = ExtColor::Color(color.clone()); |
69 | } |
70 | state.bold |= tag.change_set.bold; |
71 | state.dim |= tag.change_set.dim; |
72 | state.underline |= tag.change_set.underline; |
73 | state.italics |= tag.change_set.italics; |
74 | state.blink |= tag.change_set.blink; |
75 | state.strike |= tag.change_set.strike; |
76 | state.reverse |= tag.change_set.reverse; |
77 | state.conceal |= tag.change_set.conceal; |
78 | } |
79 | state |
80 | } |
81 | |
82 | #[allow (rustdoc::broken_intra_doc_links)] |
83 | /// Common code betwwen [Self::terminfo_apply_tag()] and [Self::ansi_apply_tag()]. |
84 | fn apply_tags_and_get_diff(&mut self, tags: Vec<ColorTag<'a>>) -> Result<StateDiff, SpanError> { |
85 | let old_state = self.state(); |
86 | |
87 | for tag in tags { |
88 | if tag.is_close { |
89 | let last_tag = self.0.last() |
90 | .ok_or_else(|| SpanError::new(Error::NoTagToClose, tag.span))?; |
91 | // If the tag is "void" (it is a "</>" tag), we don't need to check if the change |
92 | // sets are matching: |
93 | if !tag.change_set.is_void() && last_tag.change_set != tag.change_set { |
94 | let (last_src, src) = ( |
95 | // We can unwrap the last tag source, because we know that all the tags |
96 | // stored inside the context are *open tags*, and open tag are always taken |
97 | // from the source input: |
98 | last_tag.source.unwrap(), |
99 | // We can unwrap the source of the tag currently being processed, because |
100 | // we just checked above that the tag is not void, and non-void tags are |
101 | // always taken from the source input: |
102 | tag.source.unwrap(), |
103 | ); |
104 | return Err(SpanError::new( |
105 | Error::MismatchCloseTag(last_src.to_owned(), src.to_owned()), |
106 | tag.span, |
107 | )); |
108 | } |
109 | self.0.pop().unwrap(); |
110 | } else { |
111 | self.0.push(tag); |
112 | } |
113 | } |
114 | |
115 | let new_state = self.state(); |
116 | Ok(StateDiff::from_diff(&old_state, &new_state)) |
117 | } |
118 | } |
119 | |
120 | /// Describes the state of each color and style attributes at a given position in the format |
121 | /// string. Two states can be compared together by creating a [`StateDiff`] instance. |
122 | #[derive (Debug, PartialEq, Default)] |
123 | pub struct State { |
124 | foreground: ExtColor, |
125 | background: ExtColor, |
126 | bold: bool, |
127 | dim: bool, |
128 | underline: bool, |
129 | italics: bool, |
130 | blink: bool, |
131 | strike: bool, |
132 | reverse: bool, |
133 | conceal: bool, |
134 | } |
135 | |
136 | /// The result of the comparison between two [`State`]s. |
137 | /// |
138 | /// Each field is an [`Action`], which indicates if the given value has to be changed or left |
139 | /// unchanged in order to reach the new state. |
140 | #[derive (Debug)] |
141 | pub struct StateDiff { |
142 | foreground: Action<ExtColor>, |
143 | background: Action<ExtColor>, |
144 | bold: Action<bool>, |
145 | dim: Action<bool>, |
146 | underline: Action<bool>, |
147 | italics: Action<bool>, |
148 | blink: Action<bool>, |
149 | strike: Action<bool>, |
150 | reverse: Action<bool>, |
151 | conceal: Action<bool>, |
152 | } |
153 | |
154 | impl StateDiff { |
155 | /// Creates a new [`StateDiff`] by comparing two [`State`]s. |
156 | pub fn from_diff(old: &State, new: &State) -> Self { |
157 | StateDiff { |
158 | foreground: Action::from_diff(Some(old.foreground.clone()), Some(new.foreground.clone())), |
159 | background: Action::from_diff(Some(old.background.clone()), Some(new.background.clone())), |
160 | bold: Action::from_diff(Some(old.bold), Some(new.bold)), |
161 | dim: Action::from_diff(Some(old.dim), Some(new.dim)), |
162 | underline: Action::from_diff(Some(old.underline), Some(new.underline)), |
163 | italics: Action::from_diff(Some(old.italics), Some(new.italics)), |
164 | blink: Action::from_diff(Some(old.blink), Some(new.blink)), |
165 | strike: Action::from_diff(Some(old.strike), Some(new.strike)), |
166 | reverse: Action::from_diff(Some(old.reverse), Some(new.reverse)), |
167 | conceal: Action::from_diff(Some(old.conceal), Some(new.conceal)), |
168 | } |
169 | } |
170 | |
171 | /// Returns the list of terminfo constants (available in the `color-print` package) which have |
172 | /// to be used in order to reach the new state. |
173 | #[cfg (feature = "terminfo" )] |
174 | pub fn terminfo_token_streams(&self) -> Vec<String> { |
175 | let mut constants = vec![]; |
176 | |
177 | macro_rules! push_constant { |
178 | ($s:expr) => {{ |
179 | constants.push($s.to_owned()); |
180 | }}; |
181 | } |
182 | |
183 | let have_to_reset = or!( |
184 | matches!(self.foreground, Action::Change(ExtColor::Normal)), |
185 | matches!(self.background, Action::Change(ExtColor::Normal)), |
186 | matches!(self.bold, Action::Change(false)), |
187 | matches!(self.dim, Action::Change(false)), |
188 | matches!(self.blink, Action::Change(false)), |
189 | matches!(self.reverse, Action::Change(false)), |
190 | ); |
191 | |
192 | if have_to_reset { |
193 | push_constant!("CLEAR" ); |
194 | if let Some(ExtColor::Color(Color::Color16(color))) = self.foreground.actual_value() { |
195 | push_constant!(color.terminfo_constant(true)); |
196 | } |
197 | if let Some(ExtColor::Color(Color::Color16(color))) = self.background.actual_value() { |
198 | push_constant!(color.terminfo_constant(false)); |
199 | } |
200 | if matches!(self.bold.actual_value(), Some(true)) { |
201 | push_constant!("BOLD" ); |
202 | } |
203 | if matches!(self.dim.actual_value(), Some(true)) { |
204 | push_constant!("DIM" ); |
205 | } |
206 | if matches!(self.blink.actual_value(), Some(true)) { |
207 | push_constant!("BLINK" ); |
208 | } |
209 | if matches!(self.underline.actual_value(), Some(true)) { |
210 | push_constant!("UNDERLINE" ); |
211 | } |
212 | if matches!(self.italics.actual_value(), Some(true)) { |
213 | push_constant!("ITALICS" ); |
214 | } |
215 | if matches!(self.reverse.actual_value(), Some(true)) { |
216 | push_constant!("REVERSE" ); |
217 | } |
218 | } else { |
219 | if let Action::Change(ExtColor::Color(Color::Color16(ref color))) = self.foreground { |
220 | push_constant!(color.terminfo_constant(true)); |
221 | } |
222 | if let Action::Change(ExtColor::Color(Color::Color16(ref color))) = self.background { |
223 | push_constant!(color.terminfo_constant(false)); |
224 | } |
225 | if let Action::Change(true) = self.bold { |
226 | push_constant!("BOLD" ); |
227 | } |
228 | if let Action::Change(true) = self.dim { |
229 | push_constant!("DIM" ); |
230 | } |
231 | if let Action::Change(true) = self.blink { |
232 | push_constant!("BLINK" ); |
233 | } |
234 | if let Action::Change(true) = self.reverse { |
235 | push_constant!("REVERSE" ); |
236 | } |
237 | if let Action::Change(underline) = self.underline { |
238 | let constant = if underline { "UNDERLINE" } else { "NO_UNDERLINE" }; |
239 | push_constant!(constant); |
240 | } |
241 | if let Action::Change(italics) = self.italics { |
242 | let constant = if italics { "ITALICS" } else { "NO_ITALICS" }; |
243 | push_constant!(constant); |
244 | } |
245 | } |
246 | |
247 | constants |
248 | } |
249 | |
250 | /// Returns the ANSI sequence(s) which has to added to the format string in order to reach the |
251 | /// new state. |
252 | #[cfg (not(feature = "terminfo" ))] |
253 | pub fn ansi_string(&self) -> String { |
254 | use crate::ansi_constants::*; |
255 | |
256 | let mut output = String::new(); |
257 | |
258 | macro_rules! push_code { |
259 | ($($codes:expr),*) => { output.push_str(&generate_ansi_code(&[$($codes),*])) }; |
260 | } |
261 | |
262 | if let Action::Change(ref ext_color) = self.foreground { |
263 | match ext_color { |
264 | ExtColor::Normal => push_code!(DEFAULT_FOREGROUND), |
265 | ExtColor::Color(Color::Color16(color)) => match color.intensity { |
266 | Intensity::Normal => { |
267 | push_code!(SET_FOREGROUND_BASE + color.base_color.index()) |
268 | } |
269 | Intensity::Bright => { |
270 | push_code!(SET_BRIGHT_FOREGROUND_BASE + color.base_color.index()) |
271 | } |
272 | }, |
273 | ExtColor::Color(Color::Color256(color)) => { |
274 | push_code!(SET_FOREGROUND, 5, color.0); |
275 | }, |
276 | ExtColor::Color(Color::ColorRgb(color)) => { |
277 | push_code!(SET_FOREGROUND, 2, color.r, color.g, color.b); |
278 | }, |
279 | } |
280 | } |
281 | |
282 | if let Action::Change(ref ext_color) = self.background { |
283 | match ext_color { |
284 | ExtColor::Normal => push_code!(DEFAULT_BACKGROUND), |
285 | ExtColor::Color(Color::Color16(color)) => match color.intensity { |
286 | Intensity::Normal => { |
287 | push_code!(SET_BACKGROUND_BASE + color.base_color.index()) |
288 | } |
289 | Intensity::Bright => { |
290 | push_code!(SET_BRIGHT_BACKGROUND_BASE + color.base_color.index()) |
291 | } |
292 | }, |
293 | ExtColor::Color(Color::Color256(color)) => { |
294 | push_code!(SET_BACKGROUND, 5, color.0); |
295 | }, |
296 | ExtColor::Color(Color::ColorRgb(color)) => { |
297 | push_code!(SET_BACKGROUND, 2, color.r, color.g, color.b); |
298 | }, |
299 | } |
300 | } |
301 | |
302 | macro_rules! handle_attr { |
303 | ($attr:expr, $true_val:expr, $false_val:expr) => { |
304 | match $attr { |
305 | Action::Change(true) => push_code!($true_val), |
306 | Action::Change(false) => push_code!($false_val), |
307 | _ => (), |
308 | } |
309 | }; |
310 | } |
311 | |
312 | handle_attr!(self.bold, BOLD, NO_BOLD); |
313 | handle_attr!(self.dim, DIM, NO_BOLD); |
314 | handle_attr!(self.underline, UNDERLINE, NO_UNDERLINE); |
315 | handle_attr!(self.italics, ITALIC, NO_ITALIC); |
316 | handle_attr!(self.blink, BLINK, NO_BLINK); |
317 | handle_attr!(self.strike, STRIKE, NO_STRIKE); |
318 | handle_attr!(self.reverse, REVERSE, NO_REVERSE); |
319 | handle_attr!(self.conceal, CONCEAL, NO_CONCEAL); |
320 | |
321 | output |
322 | } |
323 | } |
324 | |
325 | /// The action to be performed on a given color/style attribute in order to reach a new state. |
326 | #[derive (Debug, PartialEq)] |
327 | pub enum Action<T> { |
328 | /// Nothing has to be done, because this value was never modified. |
329 | None, |
330 | /// This attribute has to be kept the same. |
331 | /// With the terminfo implementation, it's not possible to reset each style/color |
332 | /// independently, so we have to keep track of the values, even with the `Keep` variant. |
333 | Keep(T), |
334 | /// This attribute value has to be changed. |
335 | Change(T), |
336 | } |
337 | |
338 | #[cfg (feature = "terminfo" )] |
339 | impl<T> Action<T> { |
340 | pub fn actual_value(&self) -> Option<&T> { |
341 | match self { |
342 | Action::Keep(val) | Action::Change(val) => Some(val), |
343 | Action::None => None, |
344 | } |
345 | } |
346 | } |
347 | |
348 | impl<T> Action<T> |
349 | where |
350 | T: PartialEq, |
351 | { |
352 | /// Creates a new [`Action`]. |
353 | pub fn from_diff(old: Option<T>, new: Option<T>) -> Self { |
354 | let eq: bool = old == new; |
355 | match (old, new, eq) { |
356 | (Some(old_val: T), Some(_), true) | (Some(old_val: T), None, _) => Action::Keep(old_val), |
357 | (_, Some(new_val: T), _) => Action::Change(new_val), |
358 | _ => Action::None, |
359 | } |
360 | } |
361 | } |
362 | |
363 | /// A parsed color/style tag. |
364 | #[derive (Debug, Default)] |
365 | pub struct ColorTag<'a> { |
366 | /// Source of the tag in the format string. |
367 | pub source: Option<&'a str>, |
368 | /// Span of the tag in the format string. |
369 | pub span: Option<Span>, |
370 | /// Is it a close tag like `</red>`. |
371 | pub is_close: bool, |
372 | /// The changes that are implied by this tag. |
373 | pub change_set: ChangeSet, |
374 | } |
375 | |
376 | impl<'a> PartialEq for ColorTag<'a> { |
377 | fn eq(&self, other: &ColorTag<'a>) -> bool { |
378 | and!( |
379 | self.source == other.source, |
380 | self.is_close == other.is_close, |
381 | self.change_set == other.change_set, |
382 | ) |
383 | } |
384 | } |
385 | |
386 | impl<'a> ColorTag<'a> { |
387 | /// Creates a new close tag; only used in order to auto-close unclosed tags at the end of the |
388 | /// format string. |
389 | pub fn new_close() -> Self { |
390 | ColorTag { |
391 | source: None, |
392 | span: None, |
393 | is_close: true, |
394 | change_set: ChangeSet::default(), |
395 | } |
396 | } |
397 | |
398 | /// Sets the span of the tag. |
399 | pub fn set_span(&mut self, span: Span) { |
400 | self.span = Some(span); |
401 | } |
402 | } |
403 | |
404 | /// The changes that are implied by a tag. |
405 | #[derive (Debug, PartialEq, Default)] |
406 | pub struct ChangeSet { |
407 | /// If it is `Some`, then the foreground color has to be changed. |
408 | pub foreground: Option<Color>, |
409 | /// If it is `Some`, then the background color has to be changed. |
410 | pub background: Option<Color>, |
411 | /// If it is `true`, then the bold attribute has to be set (or unset for a close tag). |
412 | pub bold: bool, |
413 | /// If it is `true`, then the dim attribute has to be set (or unset for a close tag). |
414 | pub dim: bool, |
415 | /// If it is `true`, then the underline attribute has to be set (or unset for a close tag). |
416 | pub underline: bool, |
417 | /// If it is `true`, then the italics attribute has to be set (or unset for a close tag). |
418 | pub italics: bool, |
419 | /// If it is `true`, then the blink attribute has to be set (or unset for a close tag). |
420 | pub blink: bool, |
421 | /// If it is `true`, then the strike attribute has to be set (or unset for a close tag). |
422 | pub strike: bool, |
423 | /// If it is `true`, then the reverse attribute has to be set (or unset for a close tag). |
424 | pub reverse: bool, |
425 | /// If it is `true`, then the conceal attribute has to be set (or unset for a close tag). |
426 | pub conceal: bool, |
427 | } |
428 | |
429 | impl ChangeSet { |
430 | /// Checks if there is nothing to change (used to detect the `</>` tag). |
431 | pub fn is_void(&self) -> bool { |
432 | and!( |
433 | self.foreground.is_none(), |
434 | self.background.is_none(), |
435 | !self.bold, |
436 | !self.dim, |
437 | !self.underline, |
438 | !self.italics, |
439 | !self.blink, |
440 | !self.strike, |
441 | !self.reverse, |
442 | !self.conceal, |
443 | ) |
444 | } |
445 | } |
446 | |
447 | impl From<&[Change]> for ChangeSet { |
448 | fn from(changes: &[Change]) -> ChangeSet { |
449 | let mut change_set: ChangeSet = ChangeSet::default(); |
450 | for change: &Change in changes { |
451 | match change { |
452 | Change::Foreground(color: &Color) => change_set.foreground = Some(color.clone()), |
453 | Change::Background(color: &Color) => change_set.background = Some(color.clone()), |
454 | Change::Bold => change_set.bold = true, |
455 | Change::Dim => change_set.dim = true, |
456 | Change::Underline => change_set.underline = true, |
457 | Change::Italics => change_set.italics = true, |
458 | Change::Blink => change_set.blink = true, |
459 | Change::Strike => change_set.strike = true, |
460 | Change::Reverse => change_set.reverse = true, |
461 | Change::Conceal => change_set.conceal = true, |
462 | } |
463 | } |
464 | change_set |
465 | } |
466 | } |
467 | |
468 | /// A single change to be done inside a tag. Tags with multiple keywords like `<red;bold>` will |
469 | /// have multiple [`Change`]s. |
470 | #[derive (Debug, PartialEq, Clone)] |
471 | pub enum Change { |
472 | Foreground(Color), |
473 | Background(Color), |
474 | Bold, |
475 | Dim, |
476 | Underline, |
477 | Italics, |
478 | Blink, |
479 | Strike, |
480 | Reverse, |
481 | Conceal, |
482 | } |
483 | |
484 | impl TryFrom<&str> for Change { |
485 | type Error = (); |
486 | |
487 | /// Tries to convert a keyword like `red`, `bold` into a [`Change`] instance. |
488 | #[rustfmt::skip] |
489 | fn try_from(input: &str) -> Result<Self, Self::Error> { |
490 | macro_rules! color16 { |
491 | ($kind:ident $intensity:ident $base_color:ident) => { |
492 | Change::$kind(Color::Color16(Color16::new( |
493 | BaseColor::$base_color, |
494 | Intensity::$intensity, |
495 | ))) |
496 | }; |
497 | } |
498 | |
499 | let change = match input { |
500 | "s" | "strong" | "bold" | "em" => Change::Bold, |
501 | "dim" => Change::Dim, |
502 | "u" | "underline" => Change::Underline, |
503 | "i" | "italic" | "italics" => Change::Italics, |
504 | "blink" => Change::Blink, |
505 | "strike" => Change::Strike, |
506 | "reverse" | "rev" => Change::Reverse, |
507 | "conceal" | "hide" => Change::Conceal, |
508 | |
509 | "k" | "black" => color16!(Foreground Normal Black), |
510 | "r" | "red" => color16!(Foreground Normal Red), |
511 | "g" | "green" => color16!(Foreground Normal Green), |
512 | "y" | "yellow" => color16!(Foreground Normal Yellow), |
513 | "b" | "blue" => color16!(Foreground Normal Blue), |
514 | "m" | "magenta" => color16!(Foreground Normal Magenta), |
515 | "c" | "cyan" => color16!(Foreground Normal Cyan), |
516 | "w" | "white" => color16!(Foreground Normal White), |
517 | |
518 | "k!" | "black!" | "bright-black" => color16!(Foreground Bright Black), |
519 | "r!" | "red!" | "bright-red" => color16!(Foreground Bright Red), |
520 | "g!" | "green!" | "bright-green" => color16!(Foreground Bright Green), |
521 | "y!" | "yellow!" | "bright-yellow" => color16!(Foreground Bright Yellow), |
522 | "b!" | "blue!" | "bright-blue" => color16!(Foreground Bright Blue), |
523 | "m!" | "magenta!" | "bright-magenta" => color16!(Foreground Bright Magenta), |
524 | "c!" | "cyan!" | "bright-cyan" => color16!(Foreground Bright Cyan), |
525 | "w!" | "white!" | "bright-white" => color16!(Foreground Bright White), |
526 | |
527 | "K" | "bg-black" => color16!(Background Normal Black), |
528 | "R" | "bg-red" => color16!(Background Normal Red), |
529 | "G" | "bg-green" => color16!(Background Normal Green), |
530 | "Y" | "bg-yellow" => color16!(Background Normal Yellow), |
531 | "B" | "bg-blue" => color16!(Background Normal Blue), |
532 | "M" | "bg-magenta" => color16!(Background Normal Magenta), |
533 | "C" | "bg-cyan" => color16!(Background Normal Cyan), |
534 | "W" | "bg-white" => color16!(Background Normal White), |
535 | |
536 | "K!" | "bg-black!" | "bg-bright-black" => color16!(Background Bright Black), |
537 | "R!" | "bg-red!" | "bg-bright-red" => color16!(Background Bright Red), |
538 | "G!" | "bg-green!" | "bg-bright-green" => color16!(Background Bright Green), |
539 | "Y!" | "bg-yellow!" | "bg-bright-yellow" => color16!(Background Bright Yellow), |
540 | "B!" | "bg-blue!" | "bg-bright-blue" => color16!(Background Bright Blue), |
541 | "M!" | "bg-magenta!" | "bg-bright-magenta" => color16!(Background Bright Magenta), |
542 | "C!" | "bg-cyan!" | "bg-bright-cyan" => color16!(Background Bright Cyan), |
543 | "W!" | "bg-white!" | "bg-bright-white" => color16!(Background Bright White), |
544 | |
545 | _ => return Err(()), |
546 | }; |
547 | |
548 | Ok(change) |
549 | } |
550 | } |
551 | |
552 | /// Which "kind" of color has to be changed. |
553 | #[derive (Debug, PartialEq, Clone)] |
554 | pub enum ColorKind { |
555 | Background, |
556 | Foreground, |
557 | } |
558 | |
559 | impl ColorKind { |
560 | pub fn to_change(&self, color: Color) -> Change { |
561 | match self { |
562 | Self::Foreground => Change::Foreground(color), |
563 | Self::Background => Change::Background(color), |
564 | } |
565 | } |
566 | } |
567 | |
568 | /// An "extended" color, which can be either a real color or the "normal", default color. |
569 | #[derive (Debug, PartialEq, Clone)] |
570 | pub enum ExtColor { |
571 | Normal, |
572 | Color(Color), |
573 | } |
574 | |
575 | impl Default for ExtColor { |
576 | fn default() -> Self { |
577 | Self::Normal |
578 | } |
579 | } |
580 | |
581 | #[derive (Debug, PartialEq, Clone)] |
582 | pub enum Color { |
583 | Color16(Color16), |
584 | Color256(Color256), |
585 | ColorRgb(ColorRgb), |
586 | } |
587 | |
588 | /// A terminal color. |
589 | #[derive (Debug, PartialEq, Clone)] |
590 | pub struct Color16 { |
591 | base_color: BaseColor, |
592 | intensity: Intensity, |
593 | } |
594 | |
595 | impl Color16 { |
596 | pub fn new(base_color: BaseColor, intensity: Intensity) -> Self { |
597 | Self { base_color, intensity } |
598 | } |
599 | |
600 | /// Converts a color to a terminfo constant name (available in the `color-print` package). |
601 | #[cfg (feature = "terminfo" )] |
602 | pub fn terminfo_constant(&self, is_foreground: bool) -> String { |
603 | let mut constant = if is_foreground { |
604 | String::new() |
605 | } else { |
606 | "BG_" .to_string() |
607 | }; |
608 | |
609 | if matches!(self.intensity, Intensity::Bright) { |
610 | constant.push_str("BRIGHT_" ); |
611 | } |
612 | |
613 | constant.push_str(self.base_color.uppercase_str()); |
614 | |
615 | constant |
616 | } |
617 | } |
618 | |
619 | /// The intensity of a terminal color. |
620 | #[derive (Debug, PartialEq, Copy, Clone)] |
621 | pub enum Intensity { |
622 | Normal, |
623 | Bright, |
624 | } |
625 | |
626 | impl Intensity { |
627 | pub fn new(is_bright: bool) -> Self { |
628 | if is_bright { |
629 | Self::Bright |
630 | } else { |
631 | Self::Normal |
632 | } |
633 | } |
634 | } |
635 | |
636 | /// A "base" terminal color, which has to be completed with an [`Intensity`] in order to describe a |
637 | /// whole terminal color. |
638 | #[derive (Debug, PartialEq, Copy, Clone)] |
639 | pub enum BaseColor { |
640 | Black, |
641 | Red, |
642 | Green, |
643 | Yellow, |
644 | Blue, |
645 | Magenta, |
646 | Cyan, |
647 | White, |
648 | } |
649 | |
650 | impl BaseColor { |
651 | /// Return the index of a color, in the same ordering as the ANSI color sequences. |
652 | #[cfg (not(feature = "terminfo" ))] |
653 | pub fn index(&self) -> u8 { |
654 | match self { |
655 | Self::Black => 0, |
656 | Self::Red => 1, |
657 | Self::Green => 2, |
658 | Self::Yellow => 3, |
659 | Self::Blue => 4, |
660 | Self::Magenta => 5, |
661 | Self::Cyan => 6, |
662 | Self::White => 7, |
663 | } |
664 | } |
665 | |
666 | /// Used to generate terminfo constants, see [`Color16::terminfo_constant()`]. |
667 | #[cfg (feature = "terminfo" )] |
668 | pub fn uppercase_str(&self) -> &'static str { |
669 | match self { |
670 | Self::Black => "BLACK" , |
671 | Self::Red => "RED" , |
672 | Self::Green => "GREEN" , |
673 | Self::Yellow => "YELLOW" , |
674 | Self::Blue => "BLUE" , |
675 | Self::Magenta => "MAGENTA" , |
676 | Self::Cyan => "CYAN" , |
677 | Self::White => "WHITE" , |
678 | } |
679 | } |
680 | } |
681 | |
682 | /// A color in the 256-color palette. |
683 | #[derive (Debug, PartialEq, Clone)] |
684 | pub struct Color256(pub u8); |
685 | |
686 | /// An RGB color. |
687 | #[derive (Debug, PartialEq, Clone)] |
688 | pub struct ColorRgb { |
689 | pub r: u8, |
690 | pub g: u8, |
691 | pub b: u8, |
692 | } |
693 | |
694 | #[cfg (test)] |
695 | mod tests { |
696 | #[cfg (feature = "terminfo" )] |
697 | use super::*; |
698 | #[cfg (feature = "terminfo" )] |
699 | use crate::parse::color_tag; |
700 | |
701 | #[test ] |
702 | #[cfg (feature = "terminfo" )] |
703 | fn terminfo_apply_tag_to_context() { |
704 | let mut context = Context::new(); |
705 | |
706 | macro_rules! apply_tag { |
707 | ($s:expr) => { |
708 | context |
709 | .terminfo_apply_tags(vec![color_tag($s).unwrap().1]) |
710 | .unwrap() |
711 | }; |
712 | } |
713 | |
714 | let constants = apply_tag!("<r>" ); |
715 | assert_eq!(constants, ["RED" ]); |
716 | let constants = apply_tag!("</r>" ); |
717 | assert_eq!(constants, ["CLEAR" ]); |
718 | let constants = apply_tag!("<r>" ); |
719 | assert_eq!(constants, ["RED" ]); |
720 | let constants = apply_tag!("<s>" ); |
721 | assert_eq!(constants, ["BOLD" ]); |
722 | let constants = apply_tag!("</s>" ); |
723 | assert_eq!(constants, ["CLEAR" , "RED" ]); |
724 | let constants = apply_tag!("</r>" ); |
725 | assert_eq!(constants, ["CLEAR" ]); |
726 | } |
727 | |
728 | #[test ] |
729 | #[cfg (feature = "terminfo" )] |
730 | fn terminfo_apply_tag_to_context_2() { |
731 | let mut context = Context::new(); |
732 | |
733 | macro_rules! apply_tag { |
734 | ($s:expr) => { |
735 | context |
736 | .terminfo_apply_tags(vec![color_tag($s).unwrap().1]) |
737 | .unwrap() |
738 | }; |
739 | } |
740 | |
741 | let constants = apply_tag!("<r>" ); |
742 | assert_eq!(constants, ["RED" ]); |
743 | let constants = apply_tag!("<Y>" ); |
744 | assert_eq!(constants, ["BG_YELLOW" ]); |
745 | let constants = apply_tag!("<s>" ); |
746 | assert_eq!(constants, ["BOLD" ]); |
747 | let constants = apply_tag!("<u>" ); |
748 | assert_eq!(constants, ["UNDERLINE" ]); |
749 | let constants = apply_tag!("</u>" ); |
750 | assert_eq!(constants, ["NO_UNDERLINE" ]); |
751 | let constants = apply_tag!("</s>" ); |
752 | assert_eq!(constants, ["CLEAR" , "RED" , "BG_YELLOW" ]); |
753 | } |
754 | |
755 | #[test ] |
756 | #[cfg (feature = "terminfo" )] |
757 | fn terminfo_apply_tag_to_context_3() { |
758 | let mut context = Context::new(); |
759 | |
760 | macro_rules! apply_tag { |
761 | ($s:expr) => { |
762 | context.terminfo_apply_tags(vec![color_tag($s).unwrap().1]) |
763 | }; |
764 | } |
765 | |
766 | let res = apply_tag!("</r>" ); |
767 | assert_eq!(res, Err(SpanError::new(Error::NoTagToClose, None))); |
768 | apply_tag!("<r>" ).unwrap(); |
769 | let res = apply_tag!("</s>" ); |
770 | assert_eq!( |
771 | res, |
772 | Err(SpanError::new( |
773 | Error::MismatchCloseTag("<r>" .to_owned(), "</s>" .to_owned()), |
774 | None |
775 | )) |
776 | ); |
777 | } |
778 | } |
779 | |