1use crate::config::loader::SyntaxHighlightingConfig;
2use crate::ui::markdown::{MarkdownLine, MarkdownSegment, render_markdown_to_lines};
3use crate::ui::theme;
4use crate::ui::tui::{
5 InlineHandle, InlineMessageKind, InlineSegment, InlineTextStyle,
6 convert_style as convert_to_inline_style, theme_from_styles,
7};
8use crate::utils::transcript;
9use anstream::{AutoStream, ColorChoice};
10use anstyle::{Reset, Style};
11use anstyle_query::{clicolor, clicolor_force, no_color, term_supports_color};
12use anyhow::{Result, anyhow};
13use std::io::{self, Write};
14
15#[derive(Clone, Copy)]
17pub enum MessageStyle {
18 Info,
19 Error,
20 Output,
21 Response,
22 Tool,
23 ToolDetail,
24 Status,
25 McpStatus,
26 User,
27 Reasoning,
28}
29
30impl MessageStyle {
31 pub fn style(self) -> Style {
32 let styles = theme::active_styles();
33 match self {
34 Self::Info => styles.info,
35 Self::Error => styles.error,
36 Self::Output => styles.output,
37 Self::Response => styles.response,
38 Self::Tool => styles.tool,
39 Self::ToolDetail => styles.tool_detail,
40 Self::Status => styles.status,
41 Self::McpStatus => styles.mcp,
42 Self::User => styles.user,
43 Self::Reasoning => styles.reasoning,
44 }
45 }
46
47 pub fn indent(self) -> &'static str {
48 match self {
49 Self::Response | Self::Tool | Self::Reasoning => " ",
50 Self::ToolDetail => " ",
51 _ => "",
52 }
53 }
54}
55
56pub struct AnsiRenderer {
58 writer: AutoStream<io::Stdout>,
59 buffer: String,
60 color: bool,
61 sink: Option<InlineSink>,
62 last_line_was_empty: bool,
63 highlight_config: SyntaxHighlightingConfig,
64}
65
66impl AnsiRenderer {
67 pub fn stdout() -> Self {
69 let color =
70 clicolor_force() || (!no_color() && clicolor().unwrap_or_else(term_supports_color));
71 let choice = if color {
72 ColorChoice::Auto
73 } else {
74 ColorChoice::Never
75 };
76 Self {
77 writer: AutoStream::new(std::io::stdout(), choice),
78 buffer: String::new(),
79 color,
80 sink: None,
81 last_line_was_empty: false,
82 highlight_config: SyntaxHighlightingConfig::default(),
83 }
84 }
85
86 pub fn with_inline_ui(
88 handle: InlineHandle,
89 highlight_config: SyntaxHighlightingConfig,
90 ) -> Self {
91 let mut renderer = Self::stdout();
92 renderer.highlight_config = highlight_config;
93 renderer.sink = Some(InlineSink::new(handle));
94 renderer.last_line_was_empty = false;
95 renderer
96 }
97
98 pub fn set_highlight_config(&mut self, config: SyntaxHighlightingConfig) {
100 self.highlight_config = config;
101 }
102
103 pub fn was_previous_line_empty(&self) -> bool {
105 self.last_line_was_empty
106 }
107
108 fn message_kind(style: MessageStyle) -> InlineMessageKind {
109 match style {
110 MessageStyle::Info => InlineMessageKind::Info,
111 MessageStyle::Error => InlineMessageKind::Error,
112 MessageStyle::Output => InlineMessageKind::Pty,
113 MessageStyle::Response => InlineMessageKind::Agent,
114 MessageStyle::Tool | MessageStyle::ToolDetail => InlineMessageKind::Tool,
115 MessageStyle::Status | MessageStyle::McpStatus => InlineMessageKind::Info,
116 MessageStyle::User => InlineMessageKind::User,
117 MessageStyle::Reasoning => InlineMessageKind::Policy,
118 }
119 }
120
121 pub fn supports_streaming_markdown(&self) -> bool {
122 self.sink.is_some()
123 }
124
125 pub fn prefers_untruncated_output(&self) -> bool {
130 self.sink.is_some()
131 }
132
133 pub fn push(&mut self, text: &str) {
135 self.buffer.push_str(text);
136 }
137
138 pub fn flush(&mut self, style: MessageStyle) -> Result<()> {
140 if let Some(sink) = &mut self.sink {
141 let indent = style.indent();
142 let line = self.buffer.clone();
143 self.last_line_was_empty = line.is_empty() && indent.is_empty();
145 sink.write_line(style.style(), indent, &line, Self::message_kind(style))?;
146 self.buffer.clear();
147 return Ok(());
148 }
149 let style = style.style();
150 if self.color {
151 writeln!(self.writer, "{style}{}{Reset}", self.buffer)?;
152 } else {
153 writeln!(self.writer, "{}", self.buffer)?;
154 }
155 self.writer.flush()?;
156 transcript::append(&self.buffer);
157 self.last_line_was_empty = self.buffer.is_empty();
159 self.buffer.clear();
160 Ok(())
161 }
162
163 pub fn line(&mut self, style: MessageStyle, text: &str) -> Result<()> {
165 if matches!(style, MessageStyle::Response) {
166 return self.render_markdown(style, text);
167 }
168 let indent = style.indent();
169
170 if let Some(sink) = &mut self.sink {
171 sink.write_multiline(style.style(), indent, text, Self::message_kind(style))?;
172 return Ok(());
173 }
174
175 if text.contains('\n') {
176 let trailing_newline = text.ends_with('\n');
177 for line in text.lines() {
178 self.buffer.clear();
179 if !indent.is_empty() && !line.is_empty() {
180 self.buffer.push_str(indent);
181 }
182 self.buffer.push_str(line);
183 self.flush(style)?;
184 }
185 if trailing_newline {
186 self.buffer.clear();
187 if !indent.is_empty() {
188 self.buffer.push_str(indent);
189 }
190 self.flush(style)?;
191 }
192 Ok(())
193 } else {
194 self.buffer.clear();
195 if !indent.is_empty() && !text.is_empty() {
196 self.buffer.push_str(indent);
197 }
198 self.buffer.push_str(text);
199 self.flush(style)
200 }
201 }
202
203 pub fn inline_with_style(&mut self, style: MessageStyle, text: &str) -> Result<()> {
205 if let Some(sink) = &mut self.sink {
206 sink.write_inline(style.style(), text, Self::message_kind(style));
207 return Ok(());
208 }
209 let ansi_style = style.style();
210 if self.color {
211 write!(self.writer, "{ansi_style}{}{Reset}", text)?;
212 } else {
213 write!(self.writer, "{}", text)?;
214 }
215 self.writer.flush()?;
216 Ok(())
217 }
218
219 pub fn line_with_style(&mut self, style: Style, text: &str) -> Result<()> {
221 if let Some(sink) = &mut self.sink {
222 sink.write_multiline(style, "", text, InlineMessageKind::Info)?;
223 return Ok(());
224 }
225 if self.color {
226 writeln!(self.writer, "{style}{}{Reset}", text)?;
227 } else {
228 writeln!(self.writer, "{}", text)?;
229 }
230 self.writer.flush()?;
231 transcript::append(text);
232 Ok(())
233 }
234
235 pub fn line_if_not_empty(&mut self, style: MessageStyle) -> Result<()> {
237 if !self.was_previous_line_empty() {
238 self.line(style, "")
239 } else {
240 Ok(())
241 }
242 }
243
244 pub fn raw_line(&mut self, text: &str) -> Result<()> {
246 writeln!(self.writer, "{}", text)?;
247 self.writer.flush()?;
248 transcript::append(text);
249 Ok(())
250 }
251
252 fn render_markdown(&mut self, style: MessageStyle, text: &str) -> Result<()> {
253 let styles = theme::active_styles();
254 let base_style = style.style();
255 let indent = style.indent();
256 let highlight_cfg = if self.highlight_config.enabled {
257 Some(&self.highlight_config)
258 } else {
259 None
260 };
261 let mut lines = render_markdown_to_lines(text, base_style, &styles, highlight_cfg);
262 if lines.is_empty() {
263 lines.push(MarkdownLine::default());
264 }
265 for line in lines {
266 self.write_markdown_line(style, indent, line)?;
267 }
268 Ok(())
269 }
270
271 pub fn stream_markdown_response(
272 &mut self,
273 text: &str,
274 previous_line_count: usize,
275 ) -> Result<usize> {
276 let styles = theme::active_styles();
277 let style = MessageStyle::Response;
278 let base_style = style.style();
279 let indent = style.indent();
280 let highlight_cfg = if self.highlight_config.enabled {
281 Some(&self.highlight_config)
282 } else {
283 None
284 };
285 let mut lines = render_markdown_to_lines(text, base_style, &styles, highlight_cfg);
286 if lines.is_empty() {
287 lines.push(MarkdownLine::default());
288 }
289
290 if let Some(sink) = &mut self.sink {
291 let mut plain_lines = Vec::with_capacity(lines.len());
292 let mut prepared = Vec::with_capacity(lines.len());
293 for mut line in lines {
294 if !indent.is_empty() && !line.segments.is_empty() {
295 line.segments
296 .insert(0, MarkdownSegment::new(base_style, indent));
297 }
298 plain_lines.push(
299 line.segments
300 .iter()
301 .map(|segment| segment.text.clone())
302 .collect::<String>(),
303 );
304 prepared.push(line.segments);
305 }
306 sink.replace_lines(
307 previous_line_count,
308 &prepared,
309 &plain_lines,
310 Self::message_kind(style),
311 );
312 self.last_line_was_empty = prepared
313 .last()
314 .map(|segments| segments.is_empty())
315 .unwrap_or(true);
316 return Ok(prepared.len());
317 }
318
319 Err(anyhow!("stream_markdown_response requires an inline sink"))
320 }
321
322 fn write_markdown_line(
323 &mut self,
324 style: MessageStyle,
325 indent: &str,
326 mut line: MarkdownLine,
327 ) -> Result<()> {
328 if !indent.is_empty() && !line.segments.is_empty() {
329 line.segments
330 .insert(0, MarkdownSegment::new(style.style(), indent));
331 }
332
333 if let Some(sink) = &mut self.sink {
334 sink.write_segments(&line.segments, Self::message_kind(style))?;
335 self.last_line_was_empty = line.is_empty();
336 return Ok(());
337 }
338
339 let mut plain = String::new();
340 if self.color {
341 for segment in &line.segments {
342 write!(
343 self.writer,
344 "{style}{}{Reset}",
345 segment.text,
346 style = segment.style
347 )?;
348 plain.push_str(&segment.text);
349 }
350 writeln!(self.writer)?;
351 } else {
352 for segment in &line.segments {
353 write!(self.writer, "{}", segment.text)?;
354 plain.push_str(&segment.text);
355 }
356 writeln!(self.writer)?;
357 }
358 self.writer.flush()?;
359 transcript::append(&plain);
360 self.last_line_was_empty = plain.trim().is_empty();
361 Ok(())
362 }
363}
364
365struct InlineSink {
366 handle: InlineHandle,
367}
368
369impl InlineSink {
370 fn new(handle: InlineHandle) -> Self {
371 Self { handle }
372 }
373
374 fn resolve_fallback_style(&self, style: Style) -> InlineTextStyle {
375 let mut text_style = convert_to_inline_style(style);
376 if text_style.color.is_none() {
377 let theme = theme_from_styles(&theme::active_styles());
378 text_style = text_style.merge_color(theme.foreground);
379 }
380 text_style
381 }
382
383 fn style_to_segment(&self, style: Style, text: &str) -> InlineSegment {
384 let text_style = self.resolve_fallback_style(style);
385 InlineSegment {
386 text: text.to_string(),
387 style: text_style,
388 }
389 }
390
391 fn convert_plain_lines(
392 &self,
393 text: &str,
394 fallback: &InlineTextStyle,
395 ) -> (Vec<Vec<InlineSegment>>, Vec<String>) {
396 if text.is_empty() {
397 return (vec![Vec::new()], vec![String::new()]);
398 }
399
400 let mut converted_lines = Vec::new();
401 let mut plain_lines = Vec::new();
402
403 for line in text.split('\n') {
404 let mut segments = Vec::new();
405 if !line.is_empty() {
406 segments.push(InlineSegment {
407 text: line.to_string(),
408 style: fallback.clone(),
409 });
410 }
411 converted_lines.push(segments);
412 plain_lines.push(line.to_string());
413 }
414
415 if text.ends_with('\n') {
416 converted_lines.push(Vec::new());
417 plain_lines.push(String::new());
418 }
419
420 if converted_lines.is_empty() {
421 converted_lines.push(Vec::new());
422 plain_lines.push(String::new());
423 }
424
425 (converted_lines, plain_lines)
426 }
427
428 fn write_multiline(
429 &mut self,
430 style: Style,
431 indent: &str,
432 text: &str,
433 kind: InlineMessageKind,
434 ) -> Result<()> {
435 if text.is_empty() {
436 self.handle.append_line(kind, Vec::new());
437 crate::utils::transcript::append("");
438 return Ok(());
439 }
440
441 let fallback = self.resolve_fallback_style(style);
442 let (converted_lines, plain_lines) = self.convert_plain_lines(text, &fallback);
443
444 for (mut segments, mut plain) in converted_lines.into_iter().zip(plain_lines.into_iter()) {
445 if !indent.is_empty() && !plain.is_empty() {
446 segments.insert(
447 0,
448 InlineSegment {
449 text: indent.to_string(),
450 style: fallback.clone(),
451 },
452 );
453 plain.insert_str(0, indent);
454 }
455
456 if segments.is_empty() {
457 self.handle.append_line(kind, Vec::new());
458 } else {
459 self.handle.append_line(kind, segments);
460 }
461 crate::utils::transcript::append(&plain);
462 }
463
464 Ok(())
465 }
466
467 fn write_line(
468 &mut self,
469 style: Style,
470 indent: &str,
471 text: &str,
472 kind: InlineMessageKind,
473 ) -> Result<()> {
474 self.write_multiline(style, indent, text, kind)
475 }
476
477 fn write_inline(&mut self, style: Style, text: &str, kind: InlineMessageKind) {
478 if text.is_empty() {
479 return;
480 }
481 let fallback = self.resolve_fallback_style(style);
482 let (converted_lines, _) = self.convert_plain_lines(text, &fallback);
483 let line_count = converted_lines.len();
484
485 for (index, segments) in converted_lines.into_iter().enumerate() {
486 let has_next = index + 1 < line_count;
487 if segments.is_empty() {
488 if has_next {
489 self.handle.inline(
490 kind,
491 InlineSegment {
492 text: "\n".to_string(),
493 style: fallback.clone(),
494 },
495 );
496 }
497 continue;
498 }
499
500 for mut segment in segments {
501 if has_next {
502 segment.text.push('\n');
503 }
504 self.handle.inline(kind, segment);
505 }
506 }
507 }
508
509 fn write_segments(
510 &mut self,
511 segments: &[MarkdownSegment],
512 kind: InlineMessageKind,
513 ) -> Result<()> {
514 let converted = self.convert_segments(segments);
515 let plain = segments
516 .iter()
517 .map(|segment| segment.text.clone())
518 .collect::<String>();
519 self.handle.append_line(kind, converted);
520 crate::utils::transcript::append(&plain);
521 Ok(())
522 }
523
524 fn convert_segments(&self, segments: &[MarkdownSegment]) -> Vec<InlineSegment> {
525 if segments.is_empty() {
526 return Vec::new();
527 }
528
529 let mut converted = Vec::with_capacity(segments.len());
530 for segment in segments {
531 if segment.text.is_empty() {
532 continue;
533 }
534 converted.push(self.style_to_segment(segment.style, &segment.text));
535 }
536 converted
537 }
538
539 fn replace_lines(
540 &mut self,
541 count: usize,
542 lines: &[Vec<MarkdownSegment>],
543 plain: &[String],
544 kind: InlineMessageKind,
545 ) {
546 let mut converted = Vec::with_capacity(lines.len());
547 for segments in lines {
548 converted.push(self.convert_segments(segments));
549 }
550 self.handle.replace_last(count, kind, converted);
551 crate::utils::transcript::replace_last(count, plain);
552 }
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558
559 #[test]
560 fn test_styles_construct() {
561 let info = MessageStyle::Info.style();
562 assert_eq!(info, MessageStyle::Info.style());
563 let resp = MessageStyle::Response.style();
564 assert_eq!(resp, MessageStyle::Response.style());
565 let tool = MessageStyle::Tool.style();
566 assert_eq!(tool, MessageStyle::Tool.style());
567 let reasoning = MessageStyle::Reasoning.style();
568 assert_eq!(reasoning, MessageStyle::Reasoning.style());
569 }
570
571 #[test]
572 fn test_renderer_buffer() {
573 let mut r = AnsiRenderer::stdout();
574 r.push("hello");
575 assert_eq!(r.buffer, "hello");
576 }
577}