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}