vtcode_core/utils/
ansi.rs

1use crate::ui::iocraft::{
2    IocraftHandle, IocraftSegment, convert_style as convert_to_iocraft_style, theme_from_styles,
3};
4use crate::ui::theme;
5use crate::utils::transcript;
6use anstream::{AutoStream, ColorChoice};
7use anstyle::{Reset, Style};
8use anstyle_query::{clicolor, clicolor_force, no_color, term_supports_color};
9use anyhow::Result;
10use std::io::{self, Write};
11
12/// Styles available for rendering messages
13#[derive(Clone, Copy)]
14pub enum MessageStyle {
15    Info,
16    Error,
17    Output,
18    Response,
19    Tool,
20    User,
21    Reasoning,
22}
23
24impl MessageStyle {
25    fn style(self) -> Style {
26        let styles = theme::active_styles();
27        match self {
28            Self::Info => styles.info,
29            Self::Error => styles.error,
30            Self::Output => styles.output,
31            Self::Response => styles.response,
32            Self::Tool => styles.tool,
33            Self::User => styles.user,
34            Self::Reasoning => styles.reasoning,
35        }
36    }
37
38    fn indent(self) -> &'static str {
39        match self {
40            Self::Response | Self::Tool | Self::Reasoning => "  ",
41            _ => "",
42        }
43    }
44}
45
46/// Renderer with deferred output buffering
47pub struct AnsiRenderer {
48    writer: AutoStream<io::Stdout>,
49    buffer: String,
50    color: bool,
51    sink: Option<IocraftSink>,
52}
53
54impl AnsiRenderer {
55    /// Create a new renderer for stdout
56    pub fn stdout() -> Self {
57        let color =
58            clicolor_force() || (!no_color() && clicolor().unwrap_or_else(term_supports_color));
59        let choice = if color {
60            ColorChoice::Auto
61        } else {
62            ColorChoice::Never
63        };
64        Self {
65            writer: AutoStream::new(std::io::stdout(), choice),
66            buffer: String::new(),
67            color,
68            sink: None,
69        }
70    }
71
72    /// Create a renderer that forwards output to an iocraft session handle
73    pub fn with_iocraft(handle: IocraftHandle) -> Self {
74        let mut renderer = Self::stdout();
75        renderer.sink = Some(IocraftSink::new(handle));
76        renderer
77    }
78
79    /// Push text into the buffer
80    pub fn push(&mut self, text: &str) {
81        self.buffer.push_str(text);
82    }
83
84    /// Flush the buffer with the given style
85    pub fn flush(&mut self, style: MessageStyle) -> Result<()> {
86        if let Some(sink) = &mut self.sink {
87            let indent = style.indent();
88            let line = self.buffer.clone();
89            sink.write_line(style.style(), indent, &line)?;
90            self.buffer.clear();
91            return Ok(());
92        }
93        let style = style.style();
94        if self.color {
95            writeln!(self.writer, "{style}{}{Reset}", self.buffer)?;
96        } else {
97            writeln!(self.writer, "{}", self.buffer)?;
98        }
99        self.writer.flush()?;
100        transcript::append(&self.buffer);
101        self.buffer.clear();
102        Ok(())
103    }
104
105    /// Convenience for writing a single line
106    pub fn line(&mut self, style: MessageStyle, text: &str) -> Result<()> {
107        let indent = style.indent();
108
109        if let Some(sink) = &mut self.sink {
110            sink.write_multiline(style.style(), indent, text)?;
111            return Ok(());
112        }
113
114        if text.contains('\n') {
115            let trailing_newline = text.ends_with('\n');
116            for line in text.lines() {
117                self.buffer.clear();
118                if !indent.is_empty() && !line.is_empty() {
119                    self.buffer.push_str(indent);
120                }
121                self.buffer.push_str(line);
122                self.flush(style)?;
123            }
124            if trailing_newline {
125                self.buffer.clear();
126                if !indent.is_empty() {
127                    self.buffer.push_str(indent);
128                }
129                self.flush(style)?;
130            }
131            Ok(())
132        } else {
133            self.buffer.clear();
134            if !indent.is_empty() && !text.is_empty() {
135                self.buffer.push_str(indent);
136            }
137            self.buffer.push_str(text);
138            self.flush(style)
139        }
140    }
141
142    /// Write styled text without a trailing newline
143    pub fn inline_with_style(&mut self, style: Style, text: &str) -> Result<()> {
144        if let Some(sink) = &mut self.sink {
145            sink.write_inline(style, text);
146            return Ok(());
147        }
148        if self.color {
149            write!(self.writer, "{style}{}{Reset}", text)?;
150        } else {
151            write!(self.writer, "{}", text)?;
152        }
153        self.writer.flush()?;
154        Ok(())
155    }
156
157    /// Write a line with an explicit style
158    pub fn line_with_style(&mut self, style: Style, text: &str) -> Result<()> {
159        if let Some(sink) = &mut self.sink {
160            sink.write_multiline(style, "", text)?;
161            return Ok(());
162        }
163        if self.color {
164            writeln!(self.writer, "{style}{}{Reset}", text)?;
165        } else {
166            writeln!(self.writer, "{}", text)?;
167        }
168        self.writer.flush()?;
169        transcript::append(text);
170        Ok(())
171    }
172
173    /// Write a raw line without styling
174    pub fn raw_line(&mut self, text: &str) -> Result<()> {
175        writeln!(self.writer, "{}", text)?;
176        self.writer.flush()?;
177        transcript::append(text);
178        Ok(())
179    }
180}
181
182struct IocraftSink {
183    handle: IocraftHandle,
184}
185
186impl IocraftSink {
187    fn new(handle: IocraftHandle) -> Self {
188        Self { handle }
189    }
190
191    fn style_to_segment(&self, style: Style, text: &str) -> IocraftSegment {
192        let mut text_style = convert_to_iocraft_style(style);
193        if text_style.color.is_none() {
194            let theme = theme_from_styles(&theme::active_styles());
195            text_style = text_style.merge_color(theme.foreground);
196        }
197        IocraftSegment {
198            text: text.to_string(),
199            style: text_style,
200        }
201    }
202
203    fn write_multiline(&mut self, style: Style, indent: &str, text: &str) -> Result<()> {
204        if text.is_empty() {
205            self.handle.append_line(Vec::new());
206            crate::utils::transcript::append("");
207            return Ok(());
208        }
209
210        let mut lines = text.split('\n').peekable();
211        let ends_with_newline = text.ends_with('\n');
212
213        while let Some(line) = lines.next() {
214            let mut content = String::new();
215            if !indent.is_empty() && !line.is_empty() {
216                content.push_str(indent);
217            }
218            content.push_str(line);
219            if content.is_empty() {
220                self.handle.append_line(Vec::new());
221                crate::utils::transcript::append("");
222            } else {
223                let segment = self.style_to_segment(style, &content);
224                self.handle.append_line(vec![segment]);
225                crate::utils::transcript::append(&content);
226            }
227        }
228
229        if ends_with_newline {
230            self.handle.append_line(Vec::new());
231            crate::utils::transcript::append("");
232        }
233
234        Ok(())
235    }
236
237    fn write_line(&mut self, style: Style, indent: &str, text: &str) -> Result<()> {
238        if text.is_empty() {
239            self.handle.append_line(Vec::new());
240            crate::utils::transcript::append("");
241            return Ok(());
242        }
243        let mut content = String::new();
244        if !indent.is_empty() {
245            content.push_str(indent);
246        }
247        content.push_str(text);
248        let segment = self.style_to_segment(style, &content);
249        self.handle.append_line(vec![segment]);
250        crate::utils::transcript::append(&content);
251        Ok(())
252    }
253
254    fn write_inline(&mut self, style: Style, text: &str) {
255        if text.is_empty() {
256            return;
257        }
258        let segment = self.style_to_segment(style, text);
259        self.handle.inline(segment);
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_styles_construct() {
269        let info = MessageStyle::Info.style();
270        assert_eq!(info, MessageStyle::Info.style());
271        let resp = MessageStyle::Response.style();
272        assert_eq!(resp, MessageStyle::Response.style());
273        let tool = MessageStyle::Tool.style();
274        assert_eq!(tool, MessageStyle::Tool.style());
275        let reasoning = MessageStyle::Reasoning.style();
276        assert_eq!(reasoning, MessageStyle::Reasoning.style());
277    }
278
279    #[test]
280    fn test_renderer_buffer() {
281        let mut r = AnsiRenderer::stdout();
282        r.push("hello");
283        assert_eq!(r.buffer, "hello");
284    }
285}