Skip to main content

lady_deirdre/format/
terminal.rs

1////////////////////////////////////////////////////////////////////////////////
2// This file is part of "Lady Deirdre", a compiler front-end foundation       //
3// technology.                                                                //
4//                                                                            //
5// This work is proprietary software with source-available code.              //
6//                                                                            //
7// To copy, use, distribute, or contribute to this work, you must agree to    //
8// the terms of the General License Agreement:                                //
9//                                                                            //
10// https://github.com/Eliah-Lakhin/lady-deirdre/blob/master/EULA.md           //
11//                                                                            //
12// The agreement grants a Basic Commercial License, allowing you to use       //
13// this work in non-commercial and limited commercial products with a total   //
14// gross revenue cap. To remove this commercial limit for one of your         //
15// products, you must acquire a Full Commercial License.                      //
16//                                                                            //
17// If you contribute to the source code, documentation, or related materials, //
18// you must grant me an exclusive license to these contributions.             //
19// Contributions are governed by the "Contributions" section of the General   //
20// License Agreement.                                                         //
21//                                                                            //
22// Copying the work in parts is strictly forbidden, except as permitted       //
23// under the General License Agreement.                                       //
24//                                                                            //
25// If you do not or cannot agree to the terms of this Agreement,              //
26// do not use this work.                                                      //
27//                                                                            //
28// This work is provided "as is", without any warranties, express or implied, //
29// except where such disclaimers are legally invalid.                         //
30//                                                                            //
31// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин).                 //
32// All rights reserved.                                                       //
33////////////////////////////////////////////////////////////////////////////////
34
35use std::cmp::Ordering;
36
37use crate::lexis::{SourceCode, Token, TokenBuffer};
38
39macro_rules! escape {
40    ($($code:expr)?) => {
41        concat!("\x1B[", $( $code, )? "m")
42    };
43}
44
45/// A configuration of
46/// the [CSI](https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences)
47/// style sequence.
48///
49/// In particular, through this object, you can specify text background and
50/// foreground colors, and text emphasis such as bold, italic, underlined or
51/// inverted style.
52///
53/// The Style API implemented as a builder to be used in a call-chain style,
54/// such as each function consumes the instance of this object and returns
55/// a new instance with the applied configuration option.
56///
57/// Since Style methods are const functions, you can construct and store an
58/// instance of Style in static.
59#[derive(Clone, Copy, PartialEq, Eq, Debug)]
60pub struct Style {
61    fg: Option<Color>,
62    bg: Option<Color>,
63    emphasis: Emphasis,
64}
65
66impl Default for Style {
67    #[inline(always)]
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl Style {
74    /// Creates an instance of Style without any style configurations.
75    #[inline(always)]
76    pub const fn new() -> Self {
77        Self {
78            fg: None,
79            bg: None,
80            emphasis: Emphasis::none(),
81        }
82    }
83
84    /// Sets the text foreground color to Black.
85    ///
86    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
87    /// for details.
88    #[inline(always)]
89    pub const fn black(self) -> Self {
90        self.fg(Color::Black)
91    }
92
93    /// Sets the text foreground color to Red.
94    ///
95    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
96    /// for details.
97    #[inline(always)]
98    pub const fn red(self) -> Self {
99        self.fg(Color::Red)
100    }
101
102    /// Sets the text foreground color to Green.
103    ///
104    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
105    /// for details.
106    #[inline(always)]
107    pub const fn green(self) -> Self {
108        self.fg(Color::Green)
109    }
110
111    /// Sets the text foreground color to Yellow.
112    ///
113    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
114    /// for details.
115    #[inline(always)]
116    pub const fn yellow(self) -> Self {
117        self.fg(Color::Yellow)
118    }
119
120    /// Sets the text foreground color to Blue.
121    ///
122    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
123    /// for details.
124    #[inline(always)]
125    pub const fn blue(self) -> Self {
126        self.fg(Color::Blue)
127    }
128
129    /// Sets the text foreground color to Magenta.
130    ///
131    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
132    /// for details.
133    #[inline(always)]
134    pub const fn magenta(self) -> Self {
135        self.fg(Color::Magenta)
136    }
137
138    /// Sets the text foreground color to Cyan.
139    ///
140    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
141    /// for details.
142    #[inline(always)]
143    pub const fn cyan(self) -> Self {
144        self.fg(Color::Cyan)
145    }
146
147    /// Sets the text foreground color to White.
148    ///
149    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
150    /// for details.
151    #[inline(always)]
152    pub const fn white(self) -> Self {
153        self.fg(Color::White)
154    }
155
156    /// Sets the text foreground color to Bright Black.
157    ///
158    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
159    /// for details.
160    #[inline(always)]
161    pub const fn bright_black(self) -> Self {
162        self.fg(Color::BrightBlack)
163    }
164
165    /// Sets the text foreground color to Bright Red.
166    ///
167    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
168    /// for details.
169    #[inline(always)]
170    pub const fn bright_red(self) -> Self {
171        self.fg(Color::BrightRed)
172    }
173
174    /// Sets the text foreground color to Bright Green.
175    ///
176    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
177    /// for details.
178    #[inline(always)]
179    pub const fn bright_green(self) -> Self {
180        self.fg(Color::BrightGreen)
181    }
182
183    /// Sets the text foreground color to Bright Yellow.
184    ///
185    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
186    /// for details.
187    #[inline(always)]
188    pub const fn bright_yellow(self) -> Self {
189        self.fg(Color::BrightYellow)
190    }
191
192    /// Sets the text foreground color to Bright Blue.
193    ///
194    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
195    /// for details.
196    #[inline(always)]
197    pub const fn bright_blue(self) -> Self {
198        self.fg(Color::BrightBlue)
199    }
200
201    /// Sets the text foreground color to Bright Magenta.
202    ///
203    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
204    /// for details.
205    #[inline(always)]
206    pub const fn bright_magenta(self) -> Self {
207        self.fg(Color::BrightMagenta)
208    }
209
210    /// Sets the text foreground color to Bright Cyan.
211    ///
212    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
213    /// for details.
214    #[inline(always)]
215    pub const fn bright_cyan(self) -> Self {
216        self.fg(Color::BrightCyan)
217    }
218
219    /// Sets the text foreground color to Bright White.
220    ///
221    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
222    /// for details.
223    #[inline(always)]
224    pub const fn bright_white(self) -> Self {
225        self.fg(Color::BrightWhite)
226    }
227
228    /// Sets the text foreground color to 8-bit RGB color.
229    ///
230    /// The scale of each parameter is from 0.0 to 1.0. Values outside of
231    /// this range will be clamped. The final 8-bit representation of the color
232    /// will be inferred to be as close to the floating-point components
233    /// representation as possible.
234    ///
235    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
236    /// for details.
237    #[inline(always)]
238    pub const fn rgb(self, red: f64, green: f64, blue: f64) -> Self {
239        self.fg(Color::RGB { red, green, blue })
240    }
241
242    /// Sets the text foreground color to 8-bit Grayscale color.
243    ///
244    /// The scale of the `shade` parameter is from 0.0 to 1.0. Values outside of
245    /// this range will be clamped. The final 8-bit representation of
246    /// the shade will be inferred to be as close to the floating-point shade
247    /// as possible.
248    ///
249    /// See [Ansi Color Table](https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit)
250    /// for details.
251    #[inline(always)]
252    pub const fn grayscale(self, shade: f64) -> Self {
253        self.fg(Color::Grayscale(shade))
254    }
255
256    /// Enables bold emphasis of the text.
257    #[inline(always)]
258    pub const fn bold(mut self) -> Self {
259        self.emphasis.bold = true;
260
261        self
262    }
263
264    /// Enables italic emphasis of the text.
265    #[inline(always)]
266    pub const fn italic(mut self) -> Self {
267        self.emphasis.italic = true;
268
269        self
270    }
271
272    /// Enables underlined emphasis of the text.
273    #[inline(always)]
274    pub const fn underline(mut self) -> Self {
275        self.emphasis.underline = true;
276
277        self
278    }
279
280    /// Enables inverted emphasis of the text.
281    #[inline(always)]
282    pub const fn invert(mut self) -> Self {
283        self.emphasis.invert = true;
284
285        self
286    }
287
288    /// Sets the text foreground color.
289    #[inline(always)]
290    pub const fn fg(mut self, color: Color) -> Self {
291        self.fg = Some(color);
292
293        self
294    }
295
296    /// Sets the text background color.
297    #[inline(always)]
298    pub const fn bg(mut self, color: Color) -> Self {
299        self.bg = Some(color);
300
301        self
302    }
303
304    pub(super) fn change(from: &Self, to: &Self, target: &mut String) {
305        if Emphasis::change(&from.emphasis, &to.emphasis, target) {
306            match (&from.fg, &to.fg) {
307                (Some(_), None) => Color::reset_fg(target),
308                (None, Some(to)) => to.apply_fg(target),
309                (Some(from), Some(to)) if from != to => to.apply_fg(target),
310                _ => (),
311            }
312
313            match (&from.bg, &to.bg) {
314                (Some(_), None) => Color::reset_bg(target),
315                (None, Some(to)) => to.apply_bg(target),
316                (Some(from), Some(to)) if from != to => to.apply_bg(target),
317                _ => (),
318            }
319
320            return;
321        }
322
323        if let Some(to) = &to.fg {
324            to.apply_fg(target);
325        }
326
327        if let Some(to) = &to.bg {
328            to.apply_bg(target);
329        }
330    }
331
332    #[inline(always)]
333    pub(super) fn no_emphasis(mut self) -> Self {
334        self.emphasis = Emphasis::none();
335
336        self
337    }
338}
339
340/// An extension of a string with functions that apply or erase
341/// [CSI](https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences)
342/// style sequences.
343///
344/// This trait is auto-implemented for any object which is `AsRef<str>`.
345pub trait TerminalString: AsRef<str> {
346    /// Returns a new string from this one by surrounding it with CSI sequences
347    /// that apply the specified `style` at the beginning of the string and
348    /// erase these styles in the end of the string.
349    fn apply(&self, style: Style) -> String {
350        let source = self.as_ref();
351        let mut target = String::with_capacity(source.len() + 20);
352
353        if let Some(color) = &style.fg {
354            color.apply_fg(&mut target);
355        }
356
357        if let Some(color) = &style.bg {
358            color.apply_bg(&mut target);
359        }
360
361        style.emphasis.apply(&mut target);
362
363        target.push_str(source);
364
365        if style.fg.is_some() || style.bg.is_some() || style.emphasis.is_some() {
366            reset_all(&mut target);
367        }
368
369        target
370    }
371
372    /// Returns a new string from this one, removing any valid CSI sequence from
373    /// the string content.
374    fn sanitize(&self) -> String {
375        let mut target = String::with_capacity(self.as_ref().len());
376
377        let buffer = TokenBuffer::<Escaped>::from(self);
378
379        for chunk in buffer.chunks(..) {
380            if chunk.token != Escaped::Text {
381                continue;
382            }
383
384            target.push_str(chunk.string);
385        }
386
387        target
388    }
389}
390
391impl<S: AsRef<str>> TerminalString for S {}
392
393/// An ANSI terminal color.
394///
395/// This object is capable of addressing 3-bit, 4-bit, and 8-bit
396/// [ANSI colors](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors).
397///
398/// This enum is a part of the [Style] interface.
399#[derive(Clone, Copy, Debug)]
400pub enum Color {
401    /// A 3-bit black color.
402    Black,
403
404    /// A 3-bit red color.
405    Red,
406
407    /// A 3-bit green color.
408    Green,
409
410    /// A 3-bit yellow color.
411    Yellow,
412
413    /// A 3-bit blue color.
414    Blue,
415
416    /// A 3-bit magenta color.
417    Magenta,
418
419    /// A 3-bit cyan color.
420    Cyan,
421
422    /// A 3-bit white color.
423    White,
424
425    /// A 4-bit bright black color.
426    BrightBlack,
427
428    /// A 4-bit bright red color.
429    BrightRed,
430
431    /// A 4-bit bright green color.
432    BrightGreen,
433
434    /// A 4-bit bright yellow color.
435    BrightYellow,
436
437    /// A 4-bit bright blue color.
438    BrightBlue,
439
440    /// A 4-bit bright magenta color.
441    BrightMagenta,
442
443    /// A 4-bit bright cyan color.
444    BrightCyan,
445
446    /// A 4-bit bright white color.
447    BrightWhite,
448
449    /// A 8-bit RGB color.
450    ///
451    /// The scale of each component is from 0.0 to 1.0. Values outside of
452    /// this range will be clamped. The final 8-bit representation of the color
453    /// will be inferred to be as close to the floating-point components
454    /// representation as possible.
455    RGB {
456        /// A red component of the RGB color in range from 0.0 to 1.0.
457        red: f64,
458
459        /// A green component of the RGB color in range from 0.0 to 1.0.
460        green: f64,
461
462        /// A blue component of the RGB color in range from 0.0 to 1.0.
463        blue: f64,
464    },
465
466    /// A 8-bit Grayscale color.
467    ///
468    /// The scale of the inner shade component is from 0.0 to 1.0. Values
469    /// outside of this range will be clamped. The final 8-bit representation of
470    /// the grayscale color will be inferred to be as close to
471    /// the floating-point shade as possible.
472    Grayscale(f64),
473}
474
475impl PartialEq for Color {
476    fn eq(&self, other: &Self) -> bool {
477        match (self, other) {
478            (Self::Black, Self::Black) => true,
479            (Self::Red, Self::Red) => true,
480            (Self::Green, Self::Green) => true,
481            (Self::Yellow, Self::Yellow) => true,
482            (Self::Blue, Self::Blue) => true,
483            (Self::Magenta, Self::Magenta) => true,
484            (Self::Cyan, Self::Cyan) => true,
485            (Self::White, Self::White) => true,
486            (Self::BrightBlack, Self::BrightBlack) => true,
487            (Self::BrightRed, Self::BrightRed) => true,
488            (Self::BrightGreen, Self::BrightGreen) => true,
489            (Self::BrightYellow, Self::BrightYellow) => true,
490            (Self::BrightBlue, Self::BrightBlue) => true,
491            (Self::BrightMagenta, Self::BrightMagenta) => true,
492            (Self::BrightCyan, Self::BrightCyan) => true,
493            (Self::BrightWhite, Self::BrightWhite) => true,
494
495            (
496                Self::RGB {
497                    red: this_red,
498                    green: this_green,
499                    blue: this_blue,
500                },
501                Self::RGB {
502                    red: other_red,
503                    green: other_green,
504                    blue: other_blue,
505                },
506            ) => match (
507                this_red.partial_cmp(other_red),
508                this_green.partial_cmp(other_green),
509                this_blue.partial_cmp(other_blue),
510            ) {
511                (Some(Ordering::Equal), Some(Ordering::Equal), Some(Ordering::Equal))
512                | (None, None, None) => true,
513                _ => false,
514            },
515
516            (Self::Grayscale(this), Self::Grayscale(other)) => match this.partial_cmp(other) {
517                Some(Ordering::Equal) | None => true,
518                _ => false,
519            },
520
521            _ => false,
522        }
523    }
524}
525
526impl Eq for Color {}
527
528impl Color {
529    #[inline]
530    fn apply_fg(&self, target: &mut String) {
531        macro_rules! escape_fg {
532            ($code:expr) => {
533                concat!("\x1B[38;5;", $code, "m")
534            };
535        }
536
537        match self {
538            Self::Black => target.push_str(escape_fg!(0)),
539            Self::Red => target.push_str(escape_fg!(1)),
540            Self::Green => target.push_str(escape_fg!(2)),
541            Self::Yellow => target.push_str(escape_fg!(3)),
542            Self::Blue => target.push_str(escape_fg!(4)),
543            Self::Magenta => target.push_str(escape_fg!(5)),
544            Self::Cyan => target.push_str(escape_fg!(6)),
545            Self::White => target.push_str(escape_fg!(7)),
546            Self::BrightBlack => target.push_str(escape_fg!(8)),
547            Self::BrightRed => target.push_str(escape_fg!(9)),
548            Self::BrightGreen => target.push_str(escape_fg!(10)),
549            Self::BrightYellow => target.push_str(escape_fg!(11)),
550            Self::BrightBlue => target.push_str(escape_fg!(12)),
551            Self::BrightMagenta => target.push_str(escape_fg!(13)),
552            Self::BrightCyan => target.push_str(escape_fg!(14)),
553            Self::BrightWhite => target.push_str(escape_fg!(15)),
554
555            Self::RGB { red, green, blue } => {
556                let red = ((red.clamp(0.0, 1.0) * 5.0) as u8).min(5);
557                let green = ((green.clamp(0.0, 1.0) * 5.0) as u8).min(5);
558                let blue = ((blue.clamp(0.0, 1.0) * 5.0) as u8).min(5);
559
560                target.push_str(&format!("\x1B[38;5;{}m", 36 * red + 6 * green + blue + 16));
561            }
562
563            Self::Grayscale(shade) => {
564                let shade = ((shade.clamp(0.0, 1.0) * 23.0) as u8).min(23);
565
566                target.push_str(&format!("\x1B[38;5;{}m", shade + 232));
567            }
568        }
569    }
570
571    #[inline]
572    fn apply_bg(&self, target: &mut String) {
573        macro_rules! escape_bg {
574            ($code:expr) => {
575                concat!("\x1B[48;5;", $code, "m")
576            };
577        }
578
579        match self {
580            Self::Black => target.push_str(escape_bg!(0)),
581            Self::Red => target.push_str(escape_bg!(1)),
582            Self::Green => target.push_str(escape_bg!(2)),
583            Self::Yellow => target.push_str(escape_bg!(3)),
584            Self::Blue => target.push_str(escape_bg!(4)),
585            Self::Magenta => target.push_str(escape_bg!(5)),
586            Self::Cyan => target.push_str(escape_bg!(6)),
587            Self::White => target.push_str(escape_bg!(7)),
588            Self::BrightBlack => target.push_str(escape_bg!(8)),
589            Self::BrightRed => target.push_str(escape_bg!(9)),
590            Self::BrightGreen => target.push_str(escape_bg!(10)),
591            Self::BrightYellow => target.push_str(escape_bg!(11)),
592            Self::BrightBlue => target.push_str(escape_bg!(12)),
593            Self::BrightMagenta => target.push_str(escape_bg!(13)),
594            Self::BrightCyan => target.push_str(escape_bg!(14)),
595            Self::BrightWhite => target.push_str(escape_bg!(15)),
596
597            Self::RGB { red, green, blue } => {
598                let red = ((red.clamp(0.0, 1.0) * 5.0) as u8).min(5);
599                let green = ((green.clamp(0.0, 1.0) * 5.0) as u8).min(5);
600                let blue = ((blue.clamp(0.0, 1.0) * 5.0) as u8).min(5);
601
602                target.push_str(&format!("\x1B[48;5;{}m", 36 * red + 6 * green + blue + 16));
603            }
604
605            Self::Grayscale(shade) => {
606                let shade = ((shade.clamp(0.0, 1.0) * 23.0) as u8).min(23);
607
608                target.push_str(&format!("\x1B[48;5;{}m", shade + 232));
609            }
610        }
611    }
612
613    #[inline(always)]
614    fn reset_fg(target: &mut String) {
615        target.push_str(escape!(39));
616    }
617
618    #[inline(always)]
619    fn reset_bg(target: &mut String) {
620        target.push_str(escape!(49));
621    }
622}
623
624#[derive(Clone, Copy, PartialEq, Eq, Token)]
625#[repr(u8)]
626pub(super) enum Escaped {
627    EOI = 0,
628    Text = 1,
629    #[rule("\x1B[" ['\x30'..'\x4F']* ['\x20'..'\x2F']* ['\x40'..'\x7E'])]
630    CSI,
631}
632
633#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)]
634struct Emphasis {
635    bold: bool,
636    italic: bool,
637    underline: bool,
638    invert: bool,
639}
640
641impl Emphasis {
642    #[inline(always)]
643    const fn none() -> Self {
644        Self {
645            bold: false,
646            italic: false,
647            underline: false,
648            invert: false,
649        }
650    }
651
652    #[inline(always)]
653    fn is_some(&self) -> bool {
654        self.bold || self.italic || self.underline || self.invert
655    }
656
657    #[inline(always)]
658    fn apply(&self, target: &mut String) {
659        if self.bold {
660            target.push_str(escape!(1));
661        }
662
663        if self.italic {
664            target.push_str(escape!(3));
665        }
666
667        if self.underline {
668            target.push_str(escape!(4));
669        }
670
671        if self.invert {
672            target.push_str(escape!(7));
673        }
674    }
675
676    #[inline(always)]
677    fn change(from: &Self, to: &Self, target: &mut String) -> bool {
678        if from.bold <= to.bold
679            && from.italic <= to.italic
680            && from.underline <= to.underline
681            && from.invert <= to.invert
682        {
683            if !from.bold && to.bold {
684                target.push_str(escape!(1));
685            }
686
687            if !from.italic && to.italic {
688                target.push_str(escape!(3));
689            }
690
691            if !from.underline && to.underline {
692                target.push_str(escape!(4));
693            }
694
695            if !from.invert && to.invert {
696                target.push_str(escape!(7));
697            }
698
699            return true;
700        }
701
702        reset_all(target);
703        to.apply(target);
704
705        false
706    }
707}
708
709#[inline(always)]
710fn reset_all(target: &mut String) {
711    target.push_str(escape!(0));
712}