vtcode_core/utils/
ansi.rs1use crate::ui::theme;
2use crate::utils::transcript;
3use anstream::{AutoStream, ColorChoice};
4use anstyle::{Reset, Style};
5use anstyle_query::{clicolor, clicolor_force, no_color, term_supports_color};
6use anyhow::Result;
7use std::io::{self, Write};
8
9#[derive(Clone, Copy)]
11pub enum MessageStyle {
12 Info,
13 Error,
14 Output,
15 Response,
16 Tool,
17 User,
18 Reasoning,
19}
20
21impl MessageStyle {
22 fn style(self) -> Style {
23 let styles = theme::active_styles();
24 match self {
25 Self::Info => styles.info,
26 Self::Error => styles.error,
27 Self::Output => styles.output,
28 Self::Response => styles.response,
29 Self::Tool => styles.tool,
30 Self::User => styles.user,
31 Self::Reasoning => styles.reasoning,
32 }
33 }
34
35 fn indent(self) -> &'static str {
36 match self {
37 Self::Response | Self::Tool | Self::Reasoning => " ",
38 _ => "",
39 }
40 }
41}
42
43pub struct AnsiRenderer {
45 writer: AutoStream<io::Stdout>,
46 buffer: String,
47 color: bool,
48}
49
50impl AnsiRenderer {
51 pub fn stdout() -> Self {
53 let color =
54 clicolor_force() || (!no_color() && clicolor().unwrap_or_else(term_supports_color));
55 let choice = if color {
56 ColorChoice::Auto
57 } else {
58 ColorChoice::Never
59 };
60 Self {
61 writer: AutoStream::new(std::io::stdout(), choice),
62 buffer: String::new(),
63 color,
64 }
65 }
66
67 pub fn push(&mut self, text: &str) {
69 self.buffer.push_str(text);
70 }
71
72 pub fn flush(&mut self, style: MessageStyle) -> Result<()> {
74 let style = style.style();
75 if self.color {
76 writeln!(self.writer, "{style}{}{Reset}", self.buffer)?;
77 } else {
78 writeln!(self.writer, "{}", self.buffer)?;
79 }
80 self.writer.flush()?;
81 transcript::append(&self.buffer);
82 self.buffer.clear();
83 Ok(())
84 }
85
86 pub fn line(&mut self, style: MessageStyle, text: &str) -> Result<()> {
88 let indent = style.indent();
89
90 if text.contains('\n') {
91 let trailing_newline = text.ends_with('\n');
92 for line in text.lines() {
93 self.buffer.clear();
94 if !indent.is_empty() && !line.is_empty() {
95 self.buffer.push_str(indent);
96 }
97 self.buffer.push_str(line);
98 self.flush(style)?;
99 }
100 if trailing_newline {
101 self.buffer.clear();
102 if !indent.is_empty() {
103 self.buffer.push_str(indent);
104 }
105 self.flush(style)?;
106 }
107 Ok(())
108 } else {
109 self.buffer.clear();
110 if !indent.is_empty() && !text.is_empty() {
111 self.buffer.push_str(indent);
112 }
113 self.buffer.push_str(text);
114 self.flush(style)
115 }
116 }
117
118 pub fn inline_with_style(&mut self, style: Style, text: &str) -> Result<()> {
120 if self.color {
121 write!(self.writer, "{style}{}{Reset}", text)?;
122 } else {
123 write!(self.writer, "{}", text)?;
124 }
125 self.writer.flush()?;
126 Ok(())
127 }
128
129 pub fn line_with_style(&mut self, style: Style, text: &str) -> Result<()> {
131 if self.color {
132 writeln!(self.writer, "{style}{}{Reset}", text)?;
133 } else {
134 writeln!(self.writer, "{}", text)?;
135 }
136 self.writer.flush()?;
137 transcript::append(text);
138 Ok(())
139 }
140
141 pub fn raw_line(&mut self, text: &str) -> Result<()> {
143 writeln!(self.writer, "{}", text)?;
144 self.writer.flush()?;
145 transcript::append(text);
146 Ok(())
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_styles_construct() {
156 let info = MessageStyle::Info.style();
157 assert_eq!(info, MessageStyle::Info.style());
158 let resp = MessageStyle::Response.style();
159 assert_eq!(resp, MessageStyle::Response.style());
160 let tool = MessageStyle::Tool.style();
161 assert_eq!(tool, MessageStyle::Tool.style());
162 let reasoning = MessageStyle::Reasoning.style();
163 assert_eq!(reasoning, MessageStyle::Reasoning.style());
164 }
165
166 #[test]
167 fn test_renderer_buffer() {
168 let mut r = AnsiRenderer::stdout();
169 r.push("hello");
170 assert_eq!(r.buffer, "hello");
171 }
172}