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