Skip to main content

cfgd_core/output/
raw.rs

1//! Raw renderers — diff, syntax_highlight, data_line.
2//!
3//! Raw renderers are exempt from the indent invariant because their content
4//! is multi-line and line-by-line indent would corrupt syntax/diff output.
5//! All three render at depth 0.
6
7use similar::{ChangeTag, TextDiff};
8use syntect::easy::HighlightLines;
9use syntect::highlighting::Style as SynStyle;
10use syntect::parsing::SyntaxSet;
11use syntect::util::as_24_bit_terminal_escaped;
12
13use super::renderer::{Renderer, Writer};
14
15impl Renderer {
16    /// Render a unified diff using `theme.diff_*` styles. Lines starting with
17    /// `+` are themed diff_add, `-` themed diff_remove, others diff_context.
18    /// Always at depth 0 (raw renderer).
19    pub fn render_diff(&self, w: &dyn Writer, old: &str, new: &str) {
20        let diff = TextDiff::from_lines(old, new);
21        for change in diff.iter_all_changes() {
22            let (sign, style) = match change.tag() {
23                ChangeTag::Insert => ("+", &self.theme.diff_add),
24                ChangeTag::Delete => ("-", &self.theme.diff_remove),
25                ChangeTag::Equal => (" ", &self.theme.diff_context),
26            };
27            let body = format!("{sign}{change}");
28            let body = body.trim_end_matches('\n');
29            let styled = style.apply_to(body).to_string();
30            w.write_line(&styled);
31        }
32    }
33
34    /// Render syntax-highlighted code. Caller passes the `lang` hint (e.g.,
35    /// "yaml", "rust", "json"); falls back to plain text on unknown.
36    /// Always at depth 0 (raw renderer).
37    pub fn render_syntax_highlight(
38        &self,
39        w: &dyn Writer,
40        code: &str,
41        lang: &str,
42        syntax_set: &SyntaxSet,
43        theme_set: &syntect::highlighting::ThemeSet,
44    ) {
45        let syntax = syntax_set
46            .find_syntax_by_token(lang)
47            .or_else(|| syntax_set.find_syntax_by_extension(lang))
48            .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
49        let Some(theme) = theme_set
50            .themes
51            .get("base16-ocean.dark")
52            .or_else(|| theme_set.themes.values().next())
53        else {
54            // No syntect themes available; emit unstyled lines.
55            for line in code.lines() {
56                w.write_line(line);
57            }
58            return;
59        };
60        let mut h = HighlightLines::new(syntax, theme);
61        for line in code.lines() {
62            let ranges: Vec<(SynStyle, &str)> =
63                h.highlight_line(line, syntax_set).unwrap_or_default();
64            let escaped = as_24_bit_terminal_escaped(&ranges, false);
65            w.write_line(&escaped);
66        }
67    }
68}
69
70impl super::Printer {
71    /// Diff renderer. Goes to stderr.
72    pub fn diff(&self, old: &str, new: &str) {
73        self.renderer
74            .render_diff(self.sink_stderr.as_ref(), old, new);
75    }
76
77    /// Syntax-highlighted code. Goes to stderr.
78    pub fn syntax_highlight(&self, code: &str, lang: &str) {
79        self.renderer.render_syntax_highlight(
80            self.sink_stderr.as_ref(),
81            code,
82            lang,
83            &self.syntax_set,
84            &self.theme_set,
85        );
86    }
87
88    /// Raw stdout line, no decoration, no indent. For `config get`-shaped
89    /// callers whose output is consumed by other programs.
90    pub fn data_line(&self, text: &str) {
91        self.sink_stdout.write_line(text);
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use std::sync::{Arc, Mutex};
98
99    use super::super::renderer::StringSink;
100    use super::super::{Theme, Verbosity};
101    use super::*;
102    use crate::output::strip_ansi;
103
104    #[test]
105    fn diff_marks_changed_lines() {
106        let buf = Arc::new(Mutex::new(String::new()));
107        let sink = StringSink(buf.clone());
108        let r = Renderer::new(Theme::default(), Verbosity::Normal);
109        r.render_diff(&sink, "a\nb\nc\n", "a\nB\nc\n");
110        let out = strip_ansi(&buf.lock().unwrap());
111        assert!(out.contains("-b"), "got: {out:?}");
112        assert!(out.contains("+B"), "got: {out:?}");
113    }
114
115    #[test]
116    fn syntax_highlight_renders_lines() {
117        let buf = Arc::new(Mutex::new(String::new()));
118        let sink = StringSink(buf.clone());
119        let r = Renderer::new(Theme::default(), Verbosity::Normal);
120        let ss = SyntaxSet::load_defaults_newlines();
121        let ts = syntect::highlighting::ThemeSet::load_defaults();
122        r.render_syntax_highlight(&sink, "let x = 1;\nlet y = 2;\n", "rs", &ss, &ts);
123        let out = buf.lock().unwrap();
124        let stripped = strip_ansi(&out);
125        assert!(
126            stripped.contains("let x"),
127            "stripped output missing 'let x': {stripped:?}"
128        );
129        assert!(
130            stripped.contains("let y"),
131            "stripped output missing 'let y': {stripped:?}"
132        );
133    }
134
135    #[test]
136    fn data_line_writes_to_stdout_raw() {
137        use super::super::Verbosity;
138        use super::super::printer::Printer;
139
140        let stdout_buf = Arc::new(Mutex::new(String::new()));
141        let stderr_buf = Arc::new(Mutex::new(String::new()));
142        let mut p = Printer::new(Verbosity::Normal);
143        // Swap in capture sinks.
144        p.sink_stdout = Arc::new(StringSink(stdout_buf.clone()));
145        p.sink_stderr = Arc::new(StringSink(stderr_buf.clone()));
146
147        p.data_line("raw payload");
148        p.flush();
149
150        let stdout = stdout_buf.lock().unwrap();
151        let stderr = stderr_buf.lock().unwrap();
152        // data_line is RAW: exact text on stdout, no decoration, no indent.
153        assert!(stdout.contains("raw payload"), "stdout got: {stdout:?}");
154        // And NOT routed through the section/indent system to stderr.
155        assert!(
156            !stderr.contains("raw payload"),
157            "leaked to stderr: {stderr:?}"
158        );
159    }
160}