Skip to main content

urushi/render/
settings.rs

1//! Explicit feature selection for one render operation.
2
3use crate::{TextAttributes, TextStyle, UnderlineStyleSet};
4use urushi_terminal::{ColorLevel, TerminalCapabilities};
5
6use super::palette::{quantize_to_ansi16, quantize_to_ansi256};
7
8/// The output features selected for one render operation.
9///
10/// The default is deliberately dumb: no escape-sequence-producing feature is
11/// selected. A terminal's detected maximum can be adopted with `From` and then
12/// narrowed with the named setters.
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
14pub struct RenderSettings {
15    color_level: ColorLevel,
16    attributes: TextAttributes,
17    underline_styles: UnderlineStyleSet,
18    underline_colors: bool,
19    hyperlinks: bool,
20}
21
22impl RenderSettings {
23    /// Selects every rendering feature without degrading logical styles.
24    ///
25    /// This is intended for serializers whose styles have already been
26    /// narrowed to the output's capabilities. Ordinary terminal output should
27    /// detect capabilities and convert them with [`From`].
28    pub const fn all() -> Self {
29        Self {
30            color_level: ColorLevel::TrueColor,
31            attributes: TextAttributes::all(),
32            underline_styles: UnderlineStyleSet::all(),
33            underline_colors: true,
34            hyperlinks: true,
35        }
36    }
37
38    pub const fn get_color_level(self) -> ColorLevel {
39        self.color_level
40    }
41
42    pub const fn get_attributes(self) -> TextAttributes {
43        self.attributes
44    }
45
46    pub const fn get_underline_styles(self) -> UnderlineStyleSet {
47        self.underline_styles
48    }
49
50    pub const fn get_underline_colors(self) -> bool {
51        self.underline_colors
52    }
53
54    pub const fn get_hyperlinks(self) -> bool {
55        self.hyperlinks
56    }
57
58    pub const fn color_level(mut self, color_level: ColorLevel) -> Self {
59        self.color_level = color_level;
60        self
61    }
62
63    pub const fn attributes(mut self, attributes: TextAttributes) -> Self {
64        self.attributes = attributes;
65        self
66    }
67
68    pub const fn underline_styles(mut self, styles: UnderlineStyleSet) -> Self {
69        self.underline_styles = styles;
70        self
71    }
72
73    pub const fn underline_colors(mut self, enabled: bool) -> Self {
74        self.underline_colors = enabled;
75        self
76    }
77
78    pub const fn hyperlinks(mut self, enabled: bool) -> Self {
79        self.hyperlinks = enabled;
80        self
81    }
82
83    pub const fn reset_color_level(mut self) -> Self {
84        self.color_level = ColorLevel::None;
85        self
86    }
87
88    pub const fn reset_attributes(mut self) -> Self {
89        self.attributes = TextAttributes::empty();
90        self
91    }
92
93    pub const fn reset_underline_styles(mut self) -> Self {
94        self.underline_styles = UnderlineStyleSet::empty();
95        self
96    }
97
98    pub const fn reset_underline_colors(mut self) -> Self {
99        self.underline_colors = false;
100        self
101    }
102
103    pub const fn reset_hyperlinks(mut self) -> Self {
104        self.hyperlinks = false;
105        self
106    }
107
108    /// Applies these selected features to one logical text style.
109    ///
110    /// This is useful to output backends that serialize runs themselves, such
111    /// as an interactive prompt renderer. Ordinary static output should call
112    /// [`render`](crate::render) for the whole resolved view.
113    pub fn resolve_text_style(self, style: &TextStyle) -> TextStyle {
114        let mut resolved = match self.color_level {
115            ColorLevel::None => style.clone().without_colors(),
116            ColorLevel::Ansi16 => style.clone().map_colors(quantize_to_ansi16),
117            ColorLevel::Ansi256 => style.clone().map_colors(quantize_to_ansi256),
118            ColorLevel::TrueColor => style.clone(),
119        };
120        resolved.attributes = resolved.attributes.intersection(self.attributes);
121        resolved.underline = resolved.underline.and_then(|underline| {
122            self.underline_styles
123                .contains(underline.get_style())
124                .then_some(if self.underline_colors {
125                    underline
126                } else {
127                    underline.reset_color()
128                })
129        });
130        if !self.hyperlinks {
131            resolved.hyperlink = None;
132        }
133        resolved.canonical()
134    }
135}
136
137impl From<TerminalCapabilities> for RenderSettings {
138    fn from(capabilities: TerminalCapabilities) -> Self {
139        Self::default()
140            .color_level(capabilities.color_level())
141            .attributes(capabilities.attributes())
142            .underline_styles(capabilities.underline_styles())
143            .underline_colors(capabilities.underline_colors())
144            .hyperlinks(capabilities.hyperlinks())
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::{Color, Hyperlink, TextAttribute, Underline, UnderlineStyle};
152
153    #[test]
154    fn default_is_plain_text() {
155        let style = TextStyle::new()
156            .foreground(Color::RED)
157            .bold()
158            .underline_style(UnderlineStyle::Curly)
159            .underline_color(Color::BLUE)
160            .hyperlink(Hyperlink::new("https://example.com"));
161
162        assert_eq!(
163            RenderSettings::default().resolve_text_style(&style),
164            TextStyle::new()
165        );
166    }
167
168    #[test]
169    fn settings_select_each_feature_axis_independently() {
170        let style = TextStyle::new()
171            .foreground(Color::Rgb(255, 0, 0))
172            .bold()
173            .italic()
174            .underline_style(UnderlineStyle::Curly)
175            .underline_color(Color::BLUE)
176            .hyperlink("https://example.com");
177        let settings = RenderSettings::default()
178            .color_level(ColorLevel::Ansi16)
179            .attributes(TextAttribute::Italic.into())
180            .underline_styles(UnderlineStyleSet::CURLY)
181            .underline_colors(false)
182            .hyperlinks(false);
183        let resolved = settings.resolve_text_style(&style);
184
185        assert_eq!(resolved.get_foreground(), Some(Color::BRIGHT_RED));
186        assert_eq!(resolved.get_attributes(), TextAttribute::Italic.into());
187        assert_eq!(
188            resolved.get_underline(),
189            Some(Underline::new(UnderlineStyle::Curly))
190        );
191        assert_eq!(resolved.get_hyperlink(), None);
192    }
193
194    #[test]
195    fn all_preserves_every_style_feature() {
196        let style = TextStyle::new()
197            .foreground(Color::Rgb(1, 2, 3))
198            .background(Color::Rgb(4, 5, 6))
199            .bold()
200            .italic()
201            .underline_style(UnderlineStyle::Curly)
202            .underline_color(Color::BLUE)
203            .hyperlink("https://example.com");
204
205        assert_eq!(RenderSettings::all().resolve_text_style(&style), style);
206    }
207
208    #[test]
209    fn reset_builders_restore_every_setting_default() {
210        let settings = RenderSettings::default()
211            .color_level(ColorLevel::TrueColor)
212            .attributes(TextAttributes::all())
213            .underline_styles(UnderlineStyleSet::all())
214            .underline_colors(true)
215            .hyperlinks(true)
216            .reset_color_level()
217            .reset_attributes()
218            .reset_underline_styles()
219            .reset_underline_colors()
220            .reset_hyperlinks();
221
222        assert_eq!(settings, RenderSettings::default());
223    }
224}