makeup_ansi/
lib.rs

1use eyre::Result;
2
3pub mod prelude {
4    pub use crate::{
5        Ansi, Colour, CursorStyle, CursorVisibility, DisplayEraseMode, LineEraseMode, SgrParameter,
6    };
7}
8
9/// Convert a string literal to an ANSI escape sequence.
10/// See: <https://github.com/crossterm-rs/crossterm/blob/7e1279edc57a668e98211043710022b2bfa4b3a8/src/macros.rs#L1-L6>
11#[macro_export]
12macro_rules! ansi {
13    ($( $l:expr ),*) => { concat!("\x1B[", $( $l ),*) };
14}
15
16/// ANSI escape sequences. Can be directly formatted into strings.
17#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
18pub enum Ansi {
19    // Cursor manipulation
20    /// Set the (x, y) cursor position.
21    CursorPosition(u64, u64),
22    /// Set the cursor style.
23    CursorStyle(CursorStyle),
24    /// Set the cursor visibility.
25    CursorVisibility(CursorVisibility),
26    /// Move the cursor up.
27    CursorUp(u64),
28    /// Move the cursor down.
29    CursorDown(u64),
30    /// Move the cursor left.
31    CursorLeft(u64),
32    /// Move the cursor right.
33    CursorRight(u64),
34    /// Move the cursor to the start of line `count` steps down.
35    CursorNextLine(u64),
36    /// Move the cursor to the start of line `count` steps up.
37    CursorPreviousLine(u64),
38    /// Move the cursor to the column `x`.
39    CursorHorizontalAbsolute(u64),
40    /// Save the current position of the cursor.
41    SaveCursorPosition,
42    /// Restore the position of the cursor.
43    RestoreCursorPosition,
44
45    // Text manipulation
46    /// Erase part of the current display.
47    EraseInDisplay(DisplayEraseMode),
48    /// Erase part of the current line.
49    EraseInLine(LineEraseMode),
50    /// Scroll the display up.
51    ScrollUp(u64),
52    /// Scroll the display down.
53    ScrollDown(u64),
54
55    // Terminal manipulation
56    /// Set the terminal size.
57    /// This is not supported on Windows.
58    TerminalSize(u64, u64),
59    /// Set the terminal title.
60    /// This is not supported on Windows.
61    TerminalTitle(String),
62    /// Set the terminal foreground colour.
63    /// This is not supported on Windows.
64    TerminalForegroundColour(Colour),
65    /// Set the terminal background colour.
66    /// This is not supported on Windows.
67    TerminalBackgroundColour(Colour),
68    /// Set attributes on the current terminal.
69    /// This is not supported on Windows.
70    /// See: <https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters>
71    Sgr(Vec<SgrParameter>),
72}
73
74impl Ansi {
75    /// Render this ANSI escape sequence into the given `Write`able.
76    pub fn render(&self, f: &mut impl std::fmt::Write) -> Result<()> {
77        match self {
78            // Cursor
79            Self::CursorPosition(x, y) => {
80                write!(f, ansi!("{};{}H"), y + 1, x + 1)
81            }
82            Self::CursorStyle(style) => match style {
83                CursorStyle::Block => {
84                    write!(f, ansi!("2 q"))
85                }
86                CursorStyle::Bar => {
87                    write!(f, ansi!("5 q"))
88                }
89                CursorStyle::HollowBlock => {
90                    write!(f, ansi!("2 q"))
91                }
92            },
93            Self::CursorVisibility(visibility) => match visibility {
94                CursorVisibility::Visible => {
95                    write!(f, ansi!("?25h"))
96                }
97                CursorVisibility::Invisible => {
98                    write!(f, ansi!("?25l"))
99                }
100            },
101            Self::CursorUp(count) => {
102                write!(f, ansi!("{}A"), count)
103            }
104            Self::CursorDown(count) => {
105                write!(f, ansi!("{}B"), count)
106            }
107            Self::CursorLeft(count) => {
108                write!(f, ansi!("{}D"), count)
109            }
110            Self::CursorRight(count) => {
111                write!(f, ansi!("{}C"), count)
112            }
113            Self::CursorNextLine(count) => {
114                write!(f, ansi!("{}E"), count)
115            }
116            Self::CursorPreviousLine(count) => {
117                write!(f, ansi!("{}F"), count)
118            }
119            Self::CursorHorizontalAbsolute(x) => {
120                write!(f, ansi!("{}G"), x + 1)
121            }
122            Self::SaveCursorPosition => {
123                write!(f, ansi!("s"))
124            }
125            Self::RestoreCursorPosition => {
126                write!(f, ansi!("u"))
127            }
128
129            // Terminal
130            Self::EraseInDisplay(mode) => match mode {
131                DisplayEraseMode::All => {
132                    write!(f, ansi!("2J"))
133                }
134                DisplayEraseMode::FromCursorToEnd => {
135                    write!(f, ansi!("0J"))
136                }
137                DisplayEraseMode::FromCursorToStart => {
138                    write!(f, ansi!("1J"))
139                }
140                DisplayEraseMode::ScrollbackBuffer => {
141                    write!(f, ansi!("3J"))
142                }
143            },
144            Self::EraseInLine(mode) => match mode {
145                LineEraseMode::All => {
146                    write!(f, ansi!("2K"))
147                }
148                LineEraseMode::FromCursorToEnd => {
149                    write!(f, ansi!("0K"))
150                }
151                LineEraseMode::FromCursorToStart => {
152                    write!(f, ansi!("1K"))
153                }
154            },
155            Self::ScrollUp(count) => {
156                write!(f, ansi!("{}S"), count)
157            }
158            Self::ScrollDown(count) => {
159                write!(f, ansi!("{}T"), count)
160            }
161            Self::TerminalSize(width, height) => {
162                write!(f, ansi!("8;{};{}t"), height, width)
163            }
164            Self::TerminalTitle(title) => {
165                write!(f, "\x1B]0;{title}\x07")
166            }
167            Self::TerminalForegroundColour(colour) => {
168                write!(f, ansi!("38;5;{}"), colour.index())
169            }
170            Self::TerminalBackgroundColour(colour) => {
171                write!(f, ansi!("48;5;{}"), colour.index())
172            }
173            Self::Sgr(attributes) => {
174                let mut first = true;
175                write!(f, ansi!(""))?;
176                for attribute in attributes {
177                    if first {
178                        first = false;
179                    } else {
180                        write!(f, ";")?;
181                    }
182                    match attribute {
183                        SgrParameter::Reset => {
184                            write!(f, "0")
185                        }
186                        SgrParameter::Bold => {
187                            write!(f, "1")
188                        }
189                        SgrParameter::Faint => {
190                            write!(f, "2")
191                        }
192                        SgrParameter::Italic => {
193                            write!(f, "3")
194                        }
195                        SgrParameter::Underline => {
196                            write!(f, "4")
197                        }
198                        SgrParameter::Blink => {
199                            write!(f, "5")
200                        }
201                        SgrParameter::RapidBlink => {
202                            write!(f, "6")
203                        }
204                        SgrParameter::ReverseVideo => {
205                            write!(f, "7")
206                        }
207                        SgrParameter::Conceal => {
208                            write!(f, "8")
209                        }
210                        SgrParameter::CrossedOut => {
211                            write!(f, "9")
212                        }
213                        SgrParameter::PrimaryFont => {
214                            write!(f, "10")
215                        }
216                        SgrParameter::AlternativeFont(idx) => {
217                            write!(f, "{}", 10 + idx)
218                        }
219                        SgrParameter::Fraktur => {
220                            write!(f, "20")
221                        }
222                        SgrParameter::DoubleUnderline => {
223                            write!(f, "21")
224                        }
225                        SgrParameter::NormalIntensity => {
226                            write!(f, "22")
227                        }
228                        SgrParameter::NotItalicOrBlackletter => {
229                            write!(f, "23")
230                        }
231                        SgrParameter::NotUnderlined => {
232                            write!(f, "24")
233                        }
234                        SgrParameter::SteadyCursor => {
235                            write!(f, "25")
236                        }
237                        SgrParameter::ProportionalSpacing => {
238                            write!(f, "26")
239                        }
240                        SgrParameter::NotReversed => {
241                            write!(f, "27")
242                        }
243                        SgrParameter::Reveal => {
244                            write!(f, "28")
245                        }
246                        SgrParameter::NotCrossedOut => {
247                            write!(f, "29")
248                        }
249                        SgrParameter::Framed => {
250                            write!(f, "51")
251                        }
252                        SgrParameter::Encircled => {
253                            write!(f, "52")
254                        }
255                        SgrParameter::Overlined => {
256                            write!(f, "53")
257                        }
258                        SgrParameter::NotFramedOrEncircled => {
259                            write!(f, "54")
260                        }
261                        SgrParameter::NotOverlined => {
262                            write!(f, "55")
263                        }
264                        SgrParameter::IdeogramUnderlineOrRightSideLine => {
265                            write!(f, "60")
266                        }
267                        SgrParameter::IdeogramDoubleUnderlineOrDoubleLineOnTheRightSide => {
268                            write!(f, "61")
269                        }
270                        SgrParameter::IdeogramOverlineOrLeftSideLine => {
271                            write!(f, "62")
272                        }
273                        SgrParameter::IdeogramDoubleOverlineOrDoubleLineOnTheLeftSide => {
274                            write!(f, "63")
275                        }
276                        SgrParameter::IdeogramStressMarking => {
277                            write!(f, "64")
278                        }
279                        SgrParameter::IdeogramAttributesOff => {
280                            write!(f, "65")
281                        }
282                        SgrParameter::ForegroundColour(colour) => {
283                            write!(f, "38;5;{}", colour.index())
284                        }
285                        SgrParameter::BackgroundColour(colour) => {
286                            write!(f, "48;5;{}", colour.index())
287                        }
288                        SgrParameter::HexForegroundColour(hex) => {
289                            let (r, g, b) = Self::rgb(hex);
290                            write!(f, "38;2;{r};{g};{b}")
291                        }
292                        SgrParameter::HexBackgroundColour(hex) => {
293                            let (r, g, b) = Self::rgb(hex);
294                            write!(f, "48;2;{r};{g};{b}")
295                        }
296                        SgrParameter::DefaultForegroundColour => {
297                            write!(f, "39")
298                        }
299                        SgrParameter::DefaultBackgroundColour => {
300                            write!(f, "49")
301                        }
302                        SgrParameter::DisableProportionalSpacing => {
303                            write!(f, "50")
304                        }
305                        SgrParameter::UnderlineColour(colour) => {
306                            write!(f, "58;5;{}", colour.index())
307                        }
308                        SgrParameter::HexUnderlineColour(hex) => {
309                            let (r, g, b) = Self::rgb(hex);
310                            write!(f, "58;2;{r};{g};{b}")
311                        }
312                        SgrParameter::DefaultUnderlineColour => {
313                            write!(f, "59")
314                        }
315                        SgrParameter::Superscript => {
316                            write!(f, "73")
317                        }
318                        SgrParameter::Subscript => {
319                            write!(f, "74")
320                        }
321                        SgrParameter::NotSuperscriptOrSubscript => {
322                            write!(f, "75")
323                        }
324                    }?;
325                }
326                write!(f, "m")
327            }
328        }
329        .map_err(|e| e.into())
330    }
331
332    /// Convert a hex colour to RGB.
333    fn rgb(hex: &u32) -> (u32, u32, u32) {
334        let r = (hex >> 16) & 0xFF;
335        let g = (hex >> 8) & 0xFF;
336        let b = hex & 0xFF;
337        (r, g, b)
338    }
339}
340
341impl std::fmt::Display for Ansi {
342    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343        self.render(f).map_err(|_| std::fmt::Error)
344    }
345}
346
347/// Terminal cursor styles.
348#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
349pub enum CursorStyle {
350    /// The cursor is a block.
351    Block,
352
353    /// The cursor is a bar.
354    Bar,
355
356    /// The cursor is a hollow block.
357    HollowBlock,
358}
359
360/// Terminal cursor visibility.
361#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
362pub enum CursorVisibility {
363    /// The cursor is visible.
364    Visible,
365
366    /// The cursor is invisible.
367    Invisible,
368}
369
370/// Default 8-bit colour palette.
371#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
372pub enum Colour {
373    /// Black.
374    Black,
375
376    /// Red.
377    Red,
378
379    /// Green.
380    Green,
381
382    /// Yellow.
383    Yellow,
384
385    /// Blue.
386    Blue,
387
388    /// Magenta.
389    Magenta,
390
391    /// Cyan.
392    Cyan,
393
394    /// White.
395    White,
396
397    /// Bright black.
398    BrightBlack,
399
400    /// Bright red.
401    BrightRed,
402
403    /// Bright green.
404    BrightGreen,
405
406    /// Bright yellow.
407    BrightYellow,
408
409    /// Bright blue.
410    BrightBlue,
411
412    /// Bright magenta.
413    BrightMagenta,
414
415    /// Bright cyan.
416    BrightCyan,
417
418    /// Bright white.
419    BrightWhite,
420}
421
422impl Colour {
423    /// Index in the enum.
424    pub fn index(&self) -> u64 {
425        *self as u64
426    }
427}
428
429/// Erase part or all of the current display.
430#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
431pub enum DisplayEraseMode {
432    /// Erase from the cursor to the end of the display.
433    FromCursorToEnd,
434
435    /// Erase from the cursor to the start of the display.
436    FromCursorToStart,
437
438    /// Erase the entire display.
439    All,
440
441    /// Erase the scrollback buffer.
442    ScrollbackBuffer,
443}
444
445/// Erase part or all of the current line. Does not move the cursor.
446#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
447pub enum LineEraseMode {
448    /// Erase from the cursor to the end of the line.
449    FromCursorToEnd,
450
451    /// Erase from the cursor to the start of the line.
452    FromCursorToStart,
453
454    /// Erase the entire line.
455    All,
456}
457
458/// See: <https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters>
459#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
460pub enum SgrParameter {
461    /// Reset all attributes.
462    Reset,
463
464    /// Bold.
465    Bold,
466
467    /// Faint.
468    Faint,
469
470    /// Italic.
471    Italic,
472
473    /// Underline.
474    Underline,
475
476    /// Blink.
477    Blink,
478
479    /// Rapid blink.
480    RapidBlink,
481
482    /// Reverse video (note: Wikipedia notes inconsistent behaviour). Also
483    /// known as "invert."
484    ReverseVideo,
485
486    /// Conceal / hide text (note: Wikipedia notes lack of wide support).
487    Conceal,
488
489    /// Crossed out. Not supported in Terminal.app.
490    CrossedOut,
491
492    /// Select the primary font.
493    PrimaryFont,
494
495    /// Select the alternative font at the given index (N-10).
496    AlternativeFont(u64),
497
498    /// Fraktur/Gothic mode (note: Wikipedia notes lack of wide support).
499    Fraktur,
500
501    /// Double underline. Note: On some systems, this may instead disable
502    /// `Bold`.
503    DoubleUnderline,
504
505    /// Normal intensity.
506    NormalIntensity,
507
508    /// Not italic or blackletter.
509    NotItalicOrBlackletter,
510
511    /// Not underlined.
512    NotUnderlined,
513
514    /// Steady cursor (not blinking).
515    SteadyCursor,
516
517    /// Proportional spacing.
518    /// Note: Wikipedia says:
519    /// > ITU T.61 and T.416, not known to be used on terminals.
520    ProportionalSpacing,
521
522    /// Not reversed.
523    /// Presumably undoes `ReverseVideo`, needs testing.
524    NotReversed,
525
526    /// Reveal concealed text.
527    /// Presumably undoes `Conceal`, needs testing.
528    Reveal,
529
530    /// Not crossed out.
531    NotCrossedOut,
532
533    /// Set foreground colour to the given colour.
534    ForegroundColour(Colour),
535
536    /// Set background colour to the given colour.
537    BackgroundColour(Colour),
538
539    /// Set the foreground colour to the given hex colour.
540    HexForegroundColour(u32),
541
542    /// Set the background colour to the given hex colour.
543    HexBackgroundColour(u32),
544
545    /// Presumably resets to the default foreground colour, needs testing.
546    DefaultForegroundColour,
547
548    /// Presumably resets to the default background colour, needs testing.
549    DefaultBackgroundColour,
550
551    /// Disable proportional spacing.
552    DisableProportionalSpacing,
553
554    /// Set the framing (encircled) attribute.
555    Framed,
556
557    /// Set the encircled attribute.
558    Encircled,
559
560    /// Set the overlined attribute.
561    /// Note: Not supported in Terminal.app.
562    /// Note: On some systems, this may instead enable `Bold`.
563    Overlined,
564
565    /// Not framed or encircled.
566    NotFramedOrEncircled,
567
568    /// Not overlined.
569    NotOverlined,
570
571    /// Set the underline colour.
572    /// Note: Not in standard, implemented in Kitty, VTE, mintty, iTerm2.
573    UnderlineColour(Colour),
574
575    /// Set the underline colour to the given hex colour.
576    /// Note: Not in standard, implemented in Kitty, VTE, mintty, iTerm2.
577    HexUnderlineColour(u32),
578
579    /// Set the underline colour to the default.
580    /// Note: Not in standard, implemented in Kitty, VTE, mintty, iTerm2.
581    DefaultUnderlineColour,
582
583    /// Ideogram underline or right side line.
584    IdeogramUnderlineOrRightSideLine,
585
586    /// Ideogram double underline or double line on the right side.
587    IdeogramDoubleUnderlineOrDoubleLineOnTheRightSide,
588
589    /// Ideogram overline or left side line.
590    IdeogramOverlineOrLeftSideLine,
591
592    /// Ideogram double overline or double line on the left side.
593    IdeogramDoubleOverlineOrDoubleLineOnTheLeftSide,
594
595    /// Ideogram stress marking.
596    IdeogramStressMarking,
597
598    /// Ideogram attributes off.
599    /// Resets:
600    /// - `IdeogramUnderlineOrRightSideLine`
601    /// - `IdeogramDoubleUnderlineOrDoubleLineOnTheRightSide`
602    /// - `IdeogramOverlineOrLeftSideLine`
603    /// - `IdeogramDoubleOverlineOrDoubleLineOnTheLeftSide`
604    /// - `IdeogramStressMarking`.
605    IdeogramAttributesOff,
606
607    /// Implemented only in mintty.
608    Superscript,
609
610    /// Implemented only in mintty.
611    Subscript,
612
613    /// Implemented only in mintty.
614    NotSuperscriptOrSubscript,
615}
616
617#[cfg(test)]
618mod tests {
619    use eyre::Result;
620
621    use super::{Ansi, DisplayEraseMode, SgrParameter};
622
623    #[test]
624    fn test_works_as_expected() -> Result<()> {
625        let mut buffer = String::new();
626        Ansi::CursorPosition(0, 0).render(&mut buffer)?;
627        assert_eq!("\u{1b}[1;1H", buffer);
628        buffer.clear();
629
630        Ansi::CursorDown(1).render(&mut buffer)?;
631        assert_eq!("\u{1b}[1B", buffer);
632        buffer.clear();
633
634        Ansi::CursorUp(1).render(&mut buffer)?;
635        assert_eq!("\u{1b}[1A", buffer);
636        buffer.clear();
637
638        Ansi::CursorLeft(1).render(&mut buffer)?;
639        assert_eq!("\u{1b}[1D", buffer);
640        buffer.clear();
641
642        Ansi::CursorRight(1).render(&mut buffer)?;
643        assert_eq!("\u{1b}[1C", buffer);
644        buffer.clear();
645
646        Ansi::CursorNextLine(1).render(&mut buffer)?;
647        assert_eq!("\u{1b}[1E", buffer);
648        buffer.clear();
649
650        Ansi::CursorPreviousLine(1).render(&mut buffer)?;
651        assert_eq!("\u{1b}[1F", buffer);
652        buffer.clear();
653
654        Ansi::CursorHorizontalAbsolute(1).render(&mut buffer)?;
655        assert_eq!("\u{1b}[2G", buffer);
656        buffer.clear();
657
658        Ansi::CursorPosition(1, 1).render(&mut buffer)?;
659        assert_eq!("\u{1b}[2;2H", buffer);
660        buffer.clear();
661
662        Ansi::EraseInDisplay(DisplayEraseMode::All).render(&mut buffer)?;
663        assert_eq!("\u{1b}[2J", buffer);
664        buffer.clear();
665
666        Ansi::Sgr(vec![SgrParameter::HexForegroundColour(0xDB325C)]).render(&mut buffer)?;
667        assert_eq!("\u{1b}[38;2;219;50;92m", buffer);
668        buffer.clear();
669
670        Ansi::Sgr(vec![SgrParameter::HexBackgroundColour(0xDB325C)]).render(&mut buffer)?;
671        assert_eq!("\u{1b}[48;2;219;50;92m", buffer);
672        buffer.clear();
673
674        Ok(())
675    }
676}