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}