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, InlineListItem, InlineListSelection, InlineMessageKind, InlineSegment,
6 InlineTextStyle, 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 supports_inline_ui(&self) -> bool {
134 self.sink.is_some()
135 }
136
137 pub fn show_list_modal(
138 &mut self,
139 title: &str,
140 lines: Vec<String>,
141 items: Vec<InlineListItem>,
142 selected: Option<InlineListSelection>,
143 ) {
144 if let Some(sink) = &self.sink {
145 sink.show_list_modal(title.to_string(), lines, items, selected);
146 }
147 }
148
149 pub fn close_modal(&mut self) {
150 if let Some(sink) = &self.sink {
151 sink.close_modal();
152 }
153 }
154
155 pub fn push(&mut self, text: &str) {
157 self.buffer.push_str(text);
158 }
159
160 pub fn flush(&mut self, style: MessageStyle) -> Result<()> {
162 if let Some(sink) = &mut self.sink {
163 let indent = style.indent();
164 let line = self.buffer.clone();
165 self.last_line_was_empty = line.is_empty() && indent.is_empty();
167 sink.write_line(style.style(), indent, &line, Self::message_kind(style))?;
168 self.buffer.clear();
169 return Ok(());
170 }
171 let style = style.style();
172 if self.color {
173 writeln!(self.writer, "{style}{}{Reset}", self.buffer)?;
174 } else {
175 writeln!(self.writer, "{}", self.buffer)?;
176 }
177 self.writer.flush()?;
178 transcript::append(&self.buffer);
179 self.last_line_was_empty = self.buffer.is_empty();
181 self.buffer.clear();
182 Ok(())
183 }
184
185 pub fn line(&mut self, style: MessageStyle, text: &str) -> Result<()> {
187 if matches!(style, MessageStyle::Response) {
188 return self.render_markdown(style, text);
189 }
190 let indent = style.indent();
191
192 if let Some(sink) = &mut self.sink {
193 sink.write_multiline(style.style(), indent, text, Self::message_kind(style))?;
194 return Ok(());
195 }
196
197 if text.contains('\n') {
198 let trailing_newline = text.ends_with('\n');
199 for line in text.lines() {
200 self.buffer.clear();
201 if !indent.is_empty() && !line.is_empty() {
202 self.buffer.push_str(indent);
203 }
204 self.buffer.push_str(line);
205 self.flush(style)?;
206 }
207 if trailing_newline {
208 self.buffer.clear();
209 if !indent.is_empty() {
210 self.buffer.push_str(indent);
211 }
212 self.flush(style)?;
213 }
214 Ok(())
215 } else {
216 self.buffer.clear();
217 if !indent.is_empty() && !text.is_empty() {
218 self.buffer.push_str(indent);
219 }
220 self.buffer.push_str(text);
221 self.flush(style)
222 }
223 }
224
225 pub fn inline_with_style(&mut self, style: MessageStyle, text: &str) -> Result<()> {
227 if let Some(sink) = &mut self.sink {
228 sink.write_inline(style.style(), text, Self::message_kind(style));
229 return Ok(());
230 }
231 let ansi_style = style.style();
232 if self.color {
233 write!(self.writer, "{ansi_style}{}{Reset}", text)?;
234 } else {
235 write!(self.writer, "{}", text)?;
236 }
237 self.writer.flush()?;
238 Ok(())
239 }
240
241 pub fn line_with_style(&mut self, style: Style, text: &str) -> Result<()> {
243 if let Some(sink) = &mut self.sink {
244 sink.write_multiline(style, "", text, InlineMessageKind::Info)?;
245 return Ok(());
246 }
247 if self.color {
248 writeln!(self.writer, "{style}{}{Reset}", text)?;
249 } else {
250 writeln!(self.writer, "{}", text)?;
251 }
252 self.writer.flush()?;
253 transcript::append(text);
254 Ok(())
255 }
256
257 pub fn line_if_not_empty(&mut self, style: MessageStyle) -> Result<()> {
259 if !self.was_previous_line_empty() {
260 self.line(style, "")
261 } else {
262 Ok(())
263 }
264 }
265
266 pub fn raw_line(&mut self, text: &str) -> Result<()> {
268 writeln!(self.writer, "{}", text)?;
269 self.writer.flush()?;
270 transcript::append(text);
271 Ok(())
272 }
273
274 fn render_markdown(&mut self, style: MessageStyle, text: &str) -> Result<()> {
275 let styles = theme::active_styles();
276 let base_style = style.style();
277 let indent = style.indent();
278 let highlight_cfg = if self.highlight_config.enabled {
279 Some(&self.highlight_config)
280 } else {
281 None
282 };
283 let mut lines = render_markdown_to_lines(text, base_style, &styles, highlight_cfg);
284 if lines.is_empty() {
285 lines.push(MarkdownLine::default());
286 }
287 for line in lines {
288 self.write_markdown_line(style, indent, line)?;
289 }
290 Ok(())
291 }
292
293 pub fn stream_markdown_response(
294 &mut self,
295 text: &str,
296 previous_line_count: usize,
297 ) -> Result<usize> {
298 let styles = theme::active_styles();
299 let style = MessageStyle::Response;
300 let base_style = style.style();
301 let indent = style.indent();
302 let highlight_cfg = if self.highlight_config.enabled {
303 Some(&self.highlight_config)
304 } else {
305 None
306 };
307 let mut lines = render_markdown_to_lines(text, base_style, &styles, highlight_cfg);
308 if lines.is_empty() {
309 lines.push(MarkdownLine::default());
310 }
311
312 if let Some(sink) = &mut self.sink {
313 let mut plain_lines = Vec::with_capacity(lines.len());
314 let mut prepared = Vec::with_capacity(lines.len());
315 for mut line in lines {
316 if !indent.is_empty() && !line.segments.is_empty() {
317 line.segments
318 .insert(0, MarkdownSegment::new(base_style, indent));
319 }
320 plain_lines.push(
321 line.segments
322 .iter()
323 .map(|segment| segment.text.clone())
324 .collect::<String>(),
325 );
326 prepared.push(line.segments);
327 }
328 sink.replace_lines(
329 previous_line_count,
330 &prepared,
331 &plain_lines,
332 Self::message_kind(style),
333 );
334 self.last_line_was_empty = prepared
335 .last()
336 .map(|segments| segments.is_empty())
337 .unwrap_or(true);
338 return Ok(prepared.len());
339 }
340
341 Err(anyhow!("stream_markdown_response requires an inline sink"))
342 }
343
344 fn write_markdown_line(
345 &mut self,
346 style: MessageStyle,
347 indent: &str,
348 mut line: MarkdownLine,
349 ) -> Result<()> {
350 if !indent.is_empty() && !line.segments.is_empty() {
351 line.segments
352 .insert(0, MarkdownSegment::new(style.style(), indent));
353 }
354
355 if let Some(sink) = &mut self.sink {
356 sink.write_segments(&line.segments, Self::message_kind(style))?;
357 self.last_line_was_empty = line.is_empty();
358 return Ok(());
359 }
360
361 let mut plain = String::new();
362 if self.color {
363 for segment in &line.segments {
364 write!(
365 self.writer,
366 "{style}{}{Reset}",
367 segment.text,
368 style = segment.style
369 )?;
370 plain.push_str(&segment.text);
371 }
372 writeln!(self.writer)?;
373 } else {
374 for segment in &line.segments {
375 write!(self.writer, "{}", segment.text)?;
376 plain.push_str(&segment.text);
377 }
378 writeln!(self.writer)?;
379 }
380 self.writer.flush()?;
381 transcript::append(&plain);
382 self.last_line_was_empty = plain.trim().is_empty();
383 Ok(())
384 }
385}
386
387struct InlineSink {
388 handle: InlineHandle,
389}
390
391impl InlineSink {
392 fn new(handle: InlineHandle) -> Self {
393 Self { handle }
394 }
395
396 fn show_list_modal(
397 &self,
398 title: String,
399 lines: Vec<String>,
400 items: Vec<InlineListItem>,
401 selected: Option<InlineListSelection>,
402 ) {
403 self.handle.show_list_modal(title, lines, items, selected);
404 }
405
406 fn close_modal(&self) {
407 self.handle.close_modal();
408 }
409
410 fn resolve_fallback_style(&self, style: Style) -> InlineTextStyle {
411 let mut text_style = convert_to_inline_style(style);
412 if text_style.color.is_none() {
413 let theme = theme_from_styles(&theme::active_styles());
414 text_style = text_style.merge_color(theme.foreground);
415 }
416 text_style
417 }
418
419 fn style_to_segment(&self, style: Style, text: &str) -> InlineSegment {
420 let text_style = self.resolve_fallback_style(style);
421 InlineSegment {
422 text: text.to_string(),
423 style: text_style,
424 }
425 }
426
427 fn convert_plain_lines(
428 &self,
429 text: &str,
430 fallback: &InlineTextStyle,
431 ) -> (Vec<Vec<InlineSegment>>, Vec<String>) {
432 if text.is_empty() {
433 return (vec![Vec::new()], vec![String::new()]);
434 }
435
436 let mut converted_lines = Vec::new();
437 let mut plain_lines = Vec::new();
438
439 for line in text.split('\n') {
440 let mut segments = Vec::new();
441 if !line.is_empty() {
442 segments.push(InlineSegment {
443 text: line.to_string(),
444 style: fallback.clone(),
445 });
446 }
447 converted_lines.push(segments);
448 plain_lines.push(line.to_string());
449 }
450
451 if text.ends_with('\n') {
452 converted_lines.push(Vec::new());
453 plain_lines.push(String::new());
454 }
455
456 if converted_lines.is_empty() {
457 converted_lines.push(Vec::new());
458 plain_lines.push(String::new());
459 }
460
461 (converted_lines, plain_lines)
462 }
463
464 fn write_multiline(
465 &mut self,
466 style: Style,
467 indent: &str,
468 text: &str,
469 kind: InlineMessageKind,
470 ) -> Result<()> {
471 if text.is_empty() {
472 self.handle.append_line(kind, Vec::new());
473 crate::utils::transcript::append("");
474 return Ok(());
475 }
476
477 let fallback = self.resolve_fallback_style(style);
478 let (converted_lines, plain_lines) = self.convert_plain_lines(text, &fallback);
479
480 for (mut segments, mut plain) in converted_lines.into_iter().zip(plain_lines.into_iter()) {
481 if !indent.is_empty() && !plain.is_empty() {
482 segments.insert(
483 0,
484 InlineSegment {
485 text: indent.to_string(),
486 style: fallback.clone(),
487 },
488 );
489 plain.insert_str(0, indent);
490 }
491
492 if segments.is_empty() {
493 self.handle.append_line(kind, Vec::new());
494 } else {
495 self.handle.append_line(kind, segments);
496 }
497 crate::utils::transcript::append(&plain);
498 }
499
500 Ok(())
501 }
502
503 fn write_line(
504 &mut self,
505 style: Style,
506 indent: &str,
507 text: &str,
508 kind: InlineMessageKind,
509 ) -> Result<()> {
510 self.write_multiline(style, indent, text, kind)
511 }
512
513 fn write_inline(&mut self, style: Style, text: &str, kind: InlineMessageKind) {
514 if text.is_empty() {
515 return;
516 }
517 let fallback = self.resolve_fallback_style(style);
518 let (converted_lines, _) = self.convert_plain_lines(text, &fallback);
519 let line_count = converted_lines.len();
520
521 for (index, segments) in converted_lines.into_iter().enumerate() {
522 let has_next = index + 1 < line_count;
523 if segments.is_empty() {
524 if has_next {
525 self.handle.inline(
526 kind,
527 InlineSegment {
528 text: "\n".to_string(),
529 style: fallback.clone(),
530 },
531 );
532 }
533 continue;
534 }
535
536 for mut segment in segments {
537 if has_next {
538 segment.text.push('\n');
539 }
540 self.handle.inline(kind, segment);
541 }
542 }
543 }
544
545 fn write_segments(
546 &mut self,
547 segments: &[MarkdownSegment],
548 kind: InlineMessageKind,
549 ) -> Result<()> {
550 let converted = self.convert_segments(segments);
551 let plain = segments
552 .iter()
553 .map(|segment| segment.text.clone())
554 .collect::<String>();
555 self.handle.append_line(kind, converted);
556 crate::utils::transcript::append(&plain);
557 Ok(())
558 }
559
560 fn convert_segments(&self, segments: &[MarkdownSegment]) -> Vec<InlineSegment> {
561 if segments.is_empty() {
562 return Vec::new();
563 }
564
565 let mut converted = Vec::with_capacity(segments.len());
566 for segment in segments {
567 if segment.text.is_empty() {
568 continue;
569 }
570 converted.push(self.style_to_segment(segment.style, &segment.text));
571 }
572 converted
573 }
574
575 fn replace_lines(
576 &mut self,
577 count: usize,
578 lines: &[Vec<MarkdownSegment>],
579 plain: &[String],
580 kind: InlineMessageKind,
581 ) {
582 let mut converted = Vec::with_capacity(lines.len());
583 for segments in lines {
584 converted.push(self.convert_segments(segments));
585 }
586 self.handle.replace_last(count, kind, converted);
587 crate::utils::transcript::replace_last(count, plain);
588 }
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 #[test]
596 fn test_styles_construct() {
597 let info = MessageStyle::Info.style();
598 assert_eq!(info, MessageStyle::Info.style());
599 let resp = MessageStyle::Response.style();
600 assert_eq!(resp, MessageStyle::Response.style());
601 let tool = MessageStyle::Tool.style();
602 assert_eq!(tool, MessageStyle::Tool.style());
603 let reasoning = MessageStyle::Reasoning.style();
604 assert_eq!(reasoning, MessageStyle::Reasoning.style());
605 }
606
607 #[test]
608 fn test_renderer_buffer() {
609 let mut r = AnsiRenderer::stdout();
610 r.push("hello");
611 assert_eq!(r.buffer, "hello");
612 }
613}