1use crate::config::loader::SyntaxHighlightingConfig;
2use crate::ui::iocraft::{
3 IocraftHandle, IocraftSegment, convert_style as convert_to_iocraft_style, theme_from_styles,
4};
5use crate::ui::markdown::{MarkdownLine, MarkdownSegment, render_markdown_to_lines};
6use crate::ui::theme;
7use crate::utils::transcript;
8use anstream::{AutoStream, ColorChoice};
9use anstyle::{Reset, Style};
10use anstyle_query::{clicolor, clicolor_force, no_color, term_supports_color};
11use anyhow::{Result, anyhow};
12use std::io::{self, Write};
13
14#[derive(Clone, Copy)]
16pub enum MessageStyle {
17 Info,
18 Error,
19 Output,
20 Response,
21 Tool,
22 User,
23 Reasoning,
24}
25
26impl MessageStyle {
27 pub fn style(self) -> Style {
28 let styles = theme::active_styles();
29 match self {
30 Self::Info => styles.info,
31 Self::Error => styles.error,
32 Self::Output => styles.output,
33 Self::Response => styles.response,
34 Self::Tool => styles.tool,
35 Self::User => styles.user,
36 Self::Reasoning => styles.reasoning,
37 }
38 }
39
40 pub fn indent(self) -> &'static str {
41 match self {
42 Self::Response | Self::Tool | Self::Reasoning => " ",
43 _ => "",
44 }
45 }
46}
47
48pub struct AnsiRenderer {
50 writer: AutoStream<io::Stdout>,
51 buffer: String,
52 color: bool,
53 sink: Option<IocraftSink>,
54 last_line_was_empty: bool,
55 highlight_config: SyntaxHighlightingConfig,
56}
57
58impl AnsiRenderer {
59 pub fn stdout() -> Self {
61 let color =
62 clicolor_force() || (!no_color() && clicolor().unwrap_or_else(term_supports_color));
63 let choice = if color {
64 ColorChoice::Auto
65 } else {
66 ColorChoice::Never
67 };
68 Self {
69 writer: AutoStream::new(std::io::stdout(), choice),
70 buffer: String::new(),
71 color,
72 sink: None,
73 last_line_was_empty: false,
74 highlight_config: SyntaxHighlightingConfig::default(),
75 }
76 }
77
78 pub fn with_iocraft(handle: IocraftHandle, highlight_config: SyntaxHighlightingConfig) -> Self {
80 let mut renderer = Self::stdout();
81 renderer.highlight_config = highlight_config;
82 renderer.sink = Some(IocraftSink::new(handle));
83 renderer.last_line_was_empty = false;
84 renderer
85 }
86
87 pub fn set_highlight_config(&mut self, config: SyntaxHighlightingConfig) {
89 self.highlight_config = config;
90 }
91
92 pub fn was_previous_line_empty(&self) -> bool {
94 self.last_line_was_empty
95 }
96
97 pub fn supports_streaming_markdown(&self) -> bool {
98 self.sink.is_some()
99 }
100
101 pub fn push(&mut self, text: &str) {
103 self.buffer.push_str(text);
104 }
105
106 pub fn flush(&mut self, style: MessageStyle) -> Result<()> {
108 if let Some(sink) = &mut self.sink {
109 let indent = style.indent();
110 let line = self.buffer.clone();
111 self.last_line_was_empty = line.is_empty() && indent.is_empty();
113 sink.write_line(style.style(), indent, &line)?;
114 self.buffer.clear();
115 return Ok(());
116 }
117 let style = style.style();
118 if self.color {
119 writeln!(self.writer, "{style}{}{Reset}", self.buffer)?;
120 } else {
121 writeln!(self.writer, "{}", self.buffer)?;
122 }
123 self.writer.flush()?;
124 transcript::append(&self.buffer);
125 self.last_line_was_empty = self.buffer.is_empty();
127 self.buffer.clear();
128 Ok(())
129 }
130
131 pub fn line(&mut self, style: MessageStyle, text: &str) -> Result<()> {
133 if matches!(style, MessageStyle::Response) {
134 return self.render_markdown(style, text);
135 }
136 let indent = style.indent();
137
138 if let Some(sink) = &mut self.sink {
139 sink.write_multiline(style.style(), indent, text)?;
140 return Ok(());
141 }
142
143 if text.contains('\n') {
144 let trailing_newline = text.ends_with('\n');
145 for line in text.lines() {
146 self.buffer.clear();
147 if !indent.is_empty() && !line.is_empty() {
148 self.buffer.push_str(indent);
149 }
150 self.buffer.push_str(line);
151 self.flush(style)?;
152 }
153 if trailing_newline {
154 self.buffer.clear();
155 if !indent.is_empty() {
156 self.buffer.push_str(indent);
157 }
158 self.flush(style)?;
159 }
160 Ok(())
161 } else {
162 self.buffer.clear();
163 if !indent.is_empty() && !text.is_empty() {
164 self.buffer.push_str(indent);
165 }
166 self.buffer.push_str(text);
167 self.flush(style)
168 }
169 }
170
171 pub fn inline_with_style(&mut self, style: Style, text: &str) -> Result<()> {
173 if let Some(sink) = &mut self.sink {
174 sink.write_inline(style, text);
175 return Ok(());
176 }
177 if self.color {
178 write!(self.writer, "{style}{}{Reset}", text)?;
179 } else {
180 write!(self.writer, "{}", text)?;
181 }
182 self.writer.flush()?;
183 Ok(())
184 }
185
186 pub fn line_with_style(&mut self, style: Style, text: &str) -> Result<()> {
188 if let Some(sink) = &mut self.sink {
189 sink.write_multiline(style, "", text)?;
190 return Ok(());
191 }
192 if self.color {
193 writeln!(self.writer, "{style}{}{Reset}", text)?;
194 } else {
195 writeln!(self.writer, "{}", text)?;
196 }
197 self.writer.flush()?;
198 transcript::append(text);
199 Ok(())
200 }
201
202 pub fn line_if_not_empty(&mut self, style: MessageStyle) -> Result<()> {
204 if !self.was_previous_line_empty() {
205 self.line(style, "")
206 } else {
207 Ok(())
208 }
209 }
210
211 pub fn raw_line(&mut self, text: &str) -> Result<()> {
213 writeln!(self.writer, "{}", text)?;
214 self.writer.flush()?;
215 transcript::append(text);
216 Ok(())
217 }
218
219 fn render_markdown(&mut self, style: MessageStyle, text: &str) -> Result<()> {
220 let styles = theme::active_styles();
221 let base_style = style.style();
222 let indent = style.indent();
223 let highlight_cfg = if self.highlight_config.enabled {
224 Some(&self.highlight_config)
225 } else {
226 None
227 };
228 let mut lines = render_markdown_to_lines(text, base_style, &styles, highlight_cfg);
229 if lines.is_empty() {
230 lines.push(MarkdownLine::default());
231 }
232 for line in lines {
233 self.write_markdown_line(style, indent, line)?;
234 }
235 Ok(())
236 }
237
238 pub fn stream_markdown_response(
239 &mut self,
240 text: &str,
241 previous_line_count: usize,
242 ) -> Result<usize> {
243 let styles = theme::active_styles();
244 let style = MessageStyle::Response;
245 let base_style = style.style();
246 let indent = style.indent();
247 let highlight_cfg = if self.highlight_config.enabled {
248 Some(&self.highlight_config)
249 } else {
250 None
251 };
252 let mut lines = render_markdown_to_lines(text, base_style, &styles, highlight_cfg);
253 if lines.is_empty() {
254 lines.push(MarkdownLine::default());
255 }
256
257 if let Some(sink) = &mut self.sink {
258 let mut plain_lines = Vec::with_capacity(lines.len());
259 let mut prepared = Vec::with_capacity(lines.len());
260 for mut line in lines {
261 if !indent.is_empty() && !line.segments.is_empty() {
262 line.segments
263 .insert(0, MarkdownSegment::new(base_style, indent));
264 }
265 plain_lines.push(
266 line.segments
267 .iter()
268 .map(|segment| segment.text.clone())
269 .collect::<String>(),
270 );
271 prepared.push(line.segments);
272 }
273 sink.replace_lines(previous_line_count, &prepared, &plain_lines);
274 self.last_line_was_empty = prepared
275 .last()
276 .map(|segments| segments.is_empty())
277 .unwrap_or(true);
278 return Ok(prepared.len());
279 }
280
281 Err(anyhow!("stream_markdown_response requires an iocraft sink"))
282 }
283
284 fn write_markdown_line(
285 &mut self,
286 style: MessageStyle,
287 indent: &str,
288 mut line: MarkdownLine,
289 ) -> Result<()> {
290 if !indent.is_empty() && !line.segments.is_empty() {
291 line.segments
292 .insert(0, MarkdownSegment::new(style.style(), indent));
293 }
294
295 if let Some(sink) = &mut self.sink {
296 sink.write_segments(&line.segments)?;
297 self.last_line_was_empty = line.is_empty();
298 return Ok(());
299 }
300
301 let mut plain = String::new();
302 if self.color {
303 for segment in &line.segments {
304 write!(
305 self.writer,
306 "{style}{}{Reset}",
307 segment.text,
308 style = segment.style
309 )?;
310 plain.push_str(&segment.text);
311 }
312 writeln!(self.writer)?;
313 } else {
314 for segment in &line.segments {
315 write!(self.writer, "{}", segment.text)?;
316 plain.push_str(&segment.text);
317 }
318 writeln!(self.writer)?;
319 }
320 self.writer.flush()?;
321 transcript::append(&plain);
322 self.last_line_was_empty = plain.trim().is_empty();
323 Ok(())
324 }
325}
326
327struct IocraftSink {
328 handle: IocraftHandle,
329}
330
331impl IocraftSink {
332 fn new(handle: IocraftHandle) -> Self {
333 Self { handle }
334 }
335
336 fn style_to_segment(&self, style: Style, text: &str) -> IocraftSegment {
337 let mut text_style = convert_to_iocraft_style(style);
338 if text_style.color.is_none() {
339 let theme = theme_from_styles(&theme::active_styles());
340 text_style = text_style.merge_color(theme.foreground);
341 }
342 IocraftSegment {
343 text: text.to_string(),
344 style: text_style,
345 }
346 }
347
348 fn write_multiline(&mut self, style: Style, indent: &str, text: &str) -> Result<()> {
349 if text.is_empty() {
350 self.handle.append_line(Vec::new());
351 crate::utils::transcript::append("");
352 return Ok(());
353 }
354
355 let mut lines = text.split('\n').peekable();
356 let ends_with_newline = text.ends_with('\n');
357
358 while let Some(line) = lines.next() {
359 let mut content = String::new();
360 if !indent.is_empty() && !line.is_empty() {
361 content.push_str(indent);
362 }
363 content.push_str(line);
364 if content.is_empty() {
365 self.handle.append_line(Vec::new());
366 crate::utils::transcript::append("");
367 } else {
368 let segment = self.style_to_segment(style, &content);
369 self.handle.append_line(vec![segment]);
370 crate::utils::transcript::append(&content);
371 }
372 }
373
374 if ends_with_newline {
375 self.handle.append_line(Vec::new());
376 crate::utils::transcript::append("");
377 }
378
379 Ok(())
380 }
381
382 fn write_line(&mut self, style: Style, indent: &str, text: &str) -> Result<()> {
383 if text.is_empty() {
384 self.handle.append_line(Vec::new());
385 crate::utils::transcript::append("");
386 return Ok(());
387 }
388 let mut content = String::new();
389 if !indent.is_empty() {
390 content.push_str(indent);
391 }
392 content.push_str(text);
393 let segment = self.style_to_segment(style, &content);
394 self.handle.append_line(vec![segment]);
395 crate::utils::transcript::append(&content);
396 Ok(())
397 }
398
399 fn write_inline(&mut self, style: Style, text: &str) {
400 if text.is_empty() {
401 return;
402 }
403 let segment = self.style_to_segment(style, text);
404 self.handle.inline(segment);
405 }
406
407 fn write_segments(&mut self, segments: &[MarkdownSegment]) -> Result<()> {
408 let converted = self.convert_segments(segments);
409 let plain = segments
410 .iter()
411 .map(|segment| segment.text.clone())
412 .collect::<String>();
413 self.handle.append_line(converted);
414 crate::utils::transcript::append(&plain);
415 Ok(())
416 }
417
418 fn convert_segments(&self, segments: &[MarkdownSegment]) -> Vec<IocraftSegment> {
419 if segments.is_empty() {
420 return Vec::new();
421 }
422
423 let mut converted = Vec::with_capacity(segments.len());
424 for segment in segments {
425 if segment.text.is_empty() {
426 continue;
427 }
428 converted.push(self.style_to_segment(segment.style, &segment.text));
429 }
430 converted
431 }
432
433 fn replace_lines(&mut self, count: usize, lines: &[Vec<MarkdownSegment>], plain: &[String]) {
434 let mut converted = Vec::with_capacity(lines.len());
435 for segments in lines {
436 converted.push(self.convert_segments(segments));
437 }
438 self.handle.replace_last(count, converted);
439 crate::utils::transcript::replace_last(count, plain);
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn test_styles_construct() {
449 let info = MessageStyle::Info.style();
450 assert_eq!(info, MessageStyle::Info.style());
451 let resp = MessageStyle::Response.style();
452 assert_eq!(resp, MessageStyle::Response.style());
453 let tool = MessageStyle::Tool.style();
454 assert_eq!(tool, MessageStyle::Tool.style());
455 let reasoning = MessageStyle::Reasoning.style();
456 assert_eq!(reasoning, MessageStyle::Reasoning.style());
457 }
458
459 #[test]
460 fn test_renderer_buffer() {
461 let mut r = AnsiRenderer::stdout();
462 r.push("hello");
463 assert_eq!(r.buffer, "hello");
464 }
465}