use crate::io::AgentIO;
use anyhow::Result;
use async_trait::async_trait;
#[derive(Default)]
pub struct TerminalIO {
pub no_markdown: bool,
}
impl TerminalIO {
#[allow(dead_code)]
pub fn new(no_markdown: bool) -> Self {
Self { no_markdown }
}
}
fn render_markdown(text: &str, no_markdown: bool) -> String {
if no_markdown {
return text.to_owned();
}
if !console::Term::stderr().is_term() {
return text.to_owned();
}
let skin = termimad::MadSkin::default();
format!("{}", skin.term_text(text))
}
#[async_trait]
impl AgentIO for TerminalIO {
async fn show_status(&self, msg: &str) -> Result<()> {
let rendered = render_markdown(msg, self.no_markdown);
eprintln!("{}", rendered);
Ok(())
}
async fn show_tool_call(&self, tool_name: &str, args_preview: &str) -> Result<()> {
eprintln!(
" {} {} {} {}",
console::style("→").cyan().dim(),
console::style(tool_name).cyan(),
console::style("(").dim(),
console::style(args_preview).dim(),
);
Ok(())
}
async fn show_tool_result(&self, preview: &str, is_error: bool) -> Result<()> {
if is_error {
eprintln!(
" {} {}",
console::style("← error:").red().dim(),
console::style(preview).red().dim(),
);
} else {
eprintln!(
" {} {}",
console::style("←").dim(),
console::style(preview).dim(),
);
}
Ok(())
}
async fn write_error(&self, msg: &str) -> Result<()> {
eprintln!("{}", msg);
Ok(())
}
async fn confirm_destructive(&self, tool_name: &str, args_preview: &str) -> Result<bool> {
use std::io::Write;
eprint!(
" {} {} {} {} {} ",
console::style("⚠").yellow().bold(),
console::style(tool_name).yellow(),
console::style("(").dim(),
console::style(args_preview).yellow(),
console::style("[y/N]:").dim(),
);
let _ = std::io::stderr().flush();
let answer = tokio::task::spawn_blocking(|| {
let mut line = String::new();
std::io::stdin().read_line(&mut line).unwrap_or(0);
line.trim().to_lowercase()
})
.await
.unwrap_or_default();
Ok(answer == "y")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_markdown_no_markdown_flag() {
let input = "**bold** and *italic* and `code`";
let output = render_markdown(input, true);
assert_eq!(
output, input,
"should return text unchanged when no_markdown=true"
);
}
#[test]
fn test_render_markdown_empty_string() {
let output = render_markdown("", true);
assert_eq!(output, "");
}
#[test]
fn test_terminal_io_default_no_markdown_false() {
let io = TerminalIO::default();
assert!(!io.no_markdown, "default should have markdown enabled");
}
#[test]
fn test_terminal_io_new_no_markdown_true() {
let io = TerminalIO::new(true);
assert!(io.no_markdown);
}
#[test]
fn test_render_markdown_non_tty_returns_plain() {
let input = "# Heading\n**bold** text";
let output = render_markdown(input, false);
assert_eq!(output, input);
}
}