Skip to main content

uncurses/ansi/
sgr.rs

1//! Select Graphic Rendition (SGR) writer.
2//!
3//! ## Category
4//!
5//! SGR is the CSI `m` family used for rendition attributes such as reset,
6//! intensity, underline, foreground color, and background color. Higher-level
7//! style construction lives outside this module; this module writes the raw
8//! parameter grammar.
9//!
10//! ## Parameter conventions
11//!
12//! The outer slice in [`write_sgr`] becomes semicolon-separated primary
13//! parameters. Each inner slice becomes colon-separated subparameters, allowing
14//! both classic SGR and colon-form color parameters.
15//!
16//! ## Mode interaction
17//!
18//! SGR attributes are not enabled by a separate mode. They change the terminal's
19//! active rendition state until another SGR sequence resets or modifies it.
20
21use std::io::{self, Write};
22
23/// Write an SGR sequence using semicolon-separated groups and colon-separated subparameters.
24///
25/// The emitted format is `ESC [ <params> m`. The outer slice separates primary parameters with `;`; each non-empty inner slice is joined with `:`. An empty outer slice emits `ESC [ m` (SGR reset), and empty inner slices are skipped.
26///
27/// # Examples
28///
29/// ```rust,ignore
30/// // ESC [ 1 ; 31 ; 4 m — bold, red, underline
31/// write_sgr(w, &[&[1], &[31], &[4]])?;
32///
33/// // ESC [ 38 : 2 : 0 : 10 : 20 : 30 m — colon-form truecolor foreground
34/// write_sgr(w, &[&[38, 2, 0, 10, 20, 30]])?;
35///
36/// // ESC [ 0 ; 38 : 2 : 10 : 20 : 30 ; 1 m — mixed groups
37/// write_sgr(w, &[&[0], &[38, 2, 10, 20, 30], &[1]])?;
38/// ```
39pub fn write_sgr<W: Write>(w: &mut W, params: &[&[u16]]) -> io::Result<()> {
40    w.write_all(b"\x1b[")?;
41    let mut first = true;
42    for group in params {
43        if group.is_empty() {
44            continue;
45        }
46        if !first {
47            w.write_all(b";")?;
48        }
49        first = false;
50        for (i, p) in group.iter().enumerate() {
51            if i > 0 {
52                w.write_all(b":")?;
53            }
54            write!(w, "{p}")?;
55        }
56    }
57    w.write_all(b"m")
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn test_empty_is_reset() {
66        let mut buf = Vec::new();
67        write_sgr(&mut buf, &[]).unwrap();
68        assert_eq!(buf, b"\x1b[m");
69    }
70
71    #[test]
72    fn test_semicolon_only() {
73        let mut buf = Vec::new();
74        write_sgr(&mut buf, &[&[1], &[31], &[4]]).unwrap();
75        assert_eq!(buf, b"\x1b[1;31;4m");
76    }
77
78    #[test]
79    fn test_colon_subparams() {
80        let mut buf = Vec::new();
81        write_sgr(&mut buf, &[&[38, 2, 0, 10, 20, 30]]).unwrap();
82        assert_eq!(buf, b"\x1b[38:2:0:10:20:30m");
83    }
84
85    #[test]
86    fn test_mixed() {
87        let mut buf = Vec::new();
88        write_sgr(&mut buf, &[&[0], &[38, 2, 10, 20, 30], &[1]]).unwrap();
89        assert_eq!(buf, b"\x1b[0;38:2:10:20:30;1m");
90    }
91
92    #[test]
93    fn test_skip_empty_groups() {
94        let mut buf = Vec::new();
95        write_sgr(&mut buf, &[&[1], &[], &[4]]).unwrap();
96        assert_eq!(buf, b"\x1b[1;4m");
97    }
98}