Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 44 additions & 59 deletions src/claude_code.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,11 @@
use crate::config::Config;
use crate::output;
use crate::provider::{AgentSession, BootstrapMessage, OutputChunk, ProviderError, StepMessage};
use serde::Deserialize;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio};

/// ANSI color codes for verbose output.
mod color {
// Label prefix — dim grey
pub const DIM: &str = "\x1b[38;5;243m";
// Content text — lighter grey
pub const LIGHT: &str = "\x1b[38;5;250m";
// Tool names — soft blue
pub const TOOL: &str = "\x1b[38;5;111m";
// Thinking — soft purple
pub const THINKING: &str = "\x1b[38;5;183m";
// Tool result — soft green
pub const RESULT: &str = "\x1b[38;5;151m";
// Message/prompt — soft yellow
pub const PROMPT: &str = "\x1b[38;5;223m";
// Launch command — soft cyan
pub const CMD: &str = "\x1b[38;5;152m";
// Separator — very dim
pub const SEP: &str = "\x1b[38;5;238m";
// Reset
pub const RESET: &str = "\x1b[0m";
}

/// Claude Code CLI provider adapter.
///
/// Implements the `AgentSession` trait by driving a single long-lived `claude`
Expand Down Expand Up @@ -167,25 +146,26 @@ impl ClaudeCodeAdapter {
.stderr(Stdio::piped());

if self.verbose {
let c = output::stderr_colors();
let args: Vec<_> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
eprintln!(
"{}[verbose]{} {}launch:{} {} {}{}",
color::DIM,
color::RESET,
color::DIM,
color::RESET,
color::CMD,
c.dim,
c.reset,
c.dim,
c.reset,
c.cmd,
args.join(" "),
color::RESET
c.reset
);
eprintln!(
"{} binary: {}{}",
color::DIM,
c.dim,
cmd.get_program().to_string_lossy(),
color::RESET
c.reset
);
}

Expand Down Expand Up @@ -226,16 +206,17 @@ impl ClaudeCodeAdapter {
let input_line = format_stream_input(message);

if self.verbose {
let c = output::stderr_colors();
eprintln!(
"{}[verbose]{} {}prompt ({} bytes):{}",
color::DIM,
color::RESET,
color::DIM,
c.dim,
c.reset,
c.dim,
message.len(),
color::RESET
c.reset
);
eprintln!("{}{}{}", color::PROMPT, message, color::RESET);
eprintln!("{}───{}", color::SEP, color::RESET);
eprintln!("{}{}{}", c.prompt, message, c.reset);
eprintln!("{}───{}", c.sep, c.reset);
}

stdin
Expand Down Expand Up @@ -360,6 +341,8 @@ impl<'a> Iterator for StreamTurnIterator<'a> {
return None;
}

let c = output::stderr_colors();

loop {
let mut line = String::new();
match self.reader.read_line(&mut line) {
Expand Down Expand Up @@ -427,25 +410,32 @@ impl<'a> Iterator for StreamTurnIterator<'a> {
.chars()
.take(12)
.collect::<String>();
eprintln!("{}[verbose]{} {}tool:{} {}{}{} {}{}{} {}({}){}", color::DIM, color::RESET, color::DIM, color::RESET, color::TOOL, name, color::RESET, color::LIGHT, input_preview, color::RESET, color::DIM, id_short, color::RESET);
eprintln!(
"{}[verbose]{} {}tool:{} {}{}{} {}{}{} {}({}){}",
c.dim,
c.reset,
c.dim,
c.reset,
c.tool,
name,
c.reset,
c.light,
input_preview,
c.reset,
c.dim,
id_short,
c.reset
);
}
}
"thinking" => {
if self.verbose {
if let Some(thinking) = &block.thinking {
eprintln!(
"{}[verbose]{} {}thinking:{}",
color::DIM,
color::RESET,
color::DIM,
color::RESET
);
eprintln!(
"{}{}{}",
color::THINKING,
thinking,
color::RESET
c.dim, c.reset, c.dim, c.reset
);
eprintln!("{}{}{}", c.thinking, thinking, c.reset);
}
}
}
Expand Down Expand Up @@ -479,20 +469,15 @@ impl<'a> Iterator for StreamTurnIterator<'a> {
.collect::<String>();
eprintln!(
"{}[verbose]{} {}result:{} {}({}){}",
color::DIM,
color::RESET,
color::DIM,
color::RESET,
color::DIM,
c.dim,
c.reset,
c.dim,
c.reset,
c.dim,
id_short,
color::RESET
);
eprintln!(
"{}{}{}",
color::RESULT,
result_text,
color::RESET
c.reset
);
eprintln!("{}{}{}", c.result, result_text, c.reset);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod discovery;
pub mod executor;
pub mod exit_code;
pub mod expand;
pub mod output;
pub mod provider;
pub mod report;
pub mod run;
Expand Down
17 changes: 12 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use bugatti::exit_code::{
EXIT_STEP_ERROR,
};
use bugatti::expand;
use bugatti::output;
use bugatti::provider::AgentSession;
use bugatti::report::{self, ReportInput};
use bugatti::run::{self, ArtifactDir, EffectiveConfigSummary};
Expand Down Expand Up @@ -72,9 +73,14 @@ fn main() {

let cli = Cli::parse();

let c = output::stdout_colors();
println!(
"\x1b[1mbugatti\x1b[0m \x1b[38;5;243mv{}\x1b[0m",
env!("CARGO_PKG_VERSION")
"{}bugatti{} {}v{}{}",
c.bold,
c.reset,
c.dim,
env!("CARGO_PKG_VERSION"),
c.reset
);
println!();

Expand Down Expand Up @@ -384,9 +390,10 @@ fn run_test_with_artifacts(
);

// Print per-test run info
let dim = "\x1b[38;5;243m";
let light = "\x1b[38;5;250m";
let reset = "\x1b[0m";
let c = output::stdout_colors();
let dim = c.dim;
let light = c.light;
let reset = c.reset;
if ctx.effective.provider.agent_args.is_empty() {
println!(" {dim}Provider:{reset} {}", ctx.effective.provider.name);
} else {
Expand Down
110 changes: 110 additions & 0 deletions src/output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::io::IsTerminal;
use std::sync::OnceLock;

#[derive(Clone, Copy, Debug)]
pub enum Stream {
Stdout,
Stderr,
}

/// Shared ANSI palette used by terminal output formatting.
#[derive(Debug)]
pub struct Colors {
pub enabled: bool,
pub bold: &'static str,
pub dim: &'static str,
pub light: &'static str,
pub tool: &'static str,
pub thinking: &'static str,
pub result: &'static str,
pub prompt: &'static str,
pub cmd: &'static str,
pub sep: &'static str,
pub reset: &'static str,
}

static COLORS: OnceLock<Colors> = OnceLock::new();
static STDERR_COLORS: OnceLock<Colors> = OnceLock::new();

fn detect_color_enabled(stream: Stream) -> bool {
if std::env::var_os("NO_COLOR").is_some() {
return false;
}

match stream {
Stream::Stdout => std::io::stdout().is_terminal(),
Stream::Stderr => std::io::stderr().is_terminal(),
}
}

fn build_colors(enabled: bool) -> Colors {
let code = |value| if enabled { value } else { "" };

Colors {
enabled,
bold: code("\x1b[1m"),
dim: code("\x1b[38;5;243m"),
light: code("\x1b[38;5;250m"),
tool: code("\x1b[38;5;111m"),
thinking: code("\x1b[38;5;183m"),
result: code("\x1b[38;5;151m"),
prompt: code("\x1b[38;5;223m"),
cmd: code("\x1b[38;5;152m"),
sep: code("\x1b[38;5;238m"),
reset: code("\x1b[0m"),
}
}

/// Returns a lazily initialized singleton color palette.
pub fn colors() -> &'static Colors {
stdout_colors()
}

/// Returns a lazily initialized stdout color palette.
pub fn stdout_colors() -> &'static Colors {
COLORS.get_or_init(|| build_colors(detect_color_enabled(Stream::Stdout)))
}

/// Returns a lazily initialized stderr color palette.
pub fn stderr_colors() -> &'static Colors {
STDERR_COLORS.get_or_init(|| build_colors(detect_color_enabled(Stream::Stderr)))
}

/// Returns whether ANSI color output should be enabled.
///
/// Color is disabled when:
/// - `NO_COLOR` is set to any value
/// - stdout is not a terminal (e.g. piped/redirected)
pub fn color_enabled() -> bool {
stdout_colors().enabled
}

/// Returns whether ANSI color output should be enabled for stderr.
pub fn color_enabled_stderr() -> bool {
stderr_colors().enabled
}

/// Returns `code` when color is enabled, otherwise an empty string.
pub fn ansi(code: &'static str) -> &'static str {
if stdout_colors().enabled {
code
} else {
""
}
}

/// Returns `code` when stderr color is enabled, otherwise an empty string.
pub fn ansi_stderr(code: &'static str) -> &'static str {
if stderr_colors().enabled {
code
} else {
""
}
}

pub mod prelude {
pub use super::{
ansi, ansi_stderr, color_enabled, color_enabled_stderr, colors, stderr_colors,
stdout_colors, Colors, Stream,
};
}