Merge remote-tracking branch 'origin/rcc/cli' into dev/rust

# Conflicts:
#	rust/crates/rusty-claude-cli/src/main.rs
This commit is contained in:
Yeachan-Heo
2026-04-01 01:10:40 +00:00
2 changed files with 418 additions and 433 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -15,12 +15,17 @@ use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ColorTheme { pub struct ColorTheme {
enabled: bool,
heading: Color, heading: Color,
emphasis: Color, emphasis: Color,
strong: Color, strong: Color,
inline_code: Color, inline_code: Color,
link: Color, link: Color,
quote: Color, quote: Color,
info: Color,
warning: Color,
success: Color,
error: Color,
spinner_active: Color, spinner_active: Color,
spinner_done: Color, spinner_done: Color,
spinner_failed: Color, spinner_failed: Color,
@@ -29,12 +34,17 @@ pub struct ColorTheme {
impl Default for ColorTheme { impl Default for ColorTheme {
fn default() -> Self { fn default() -> Self {
Self { Self {
heading: Color::Cyan, enabled: true,
emphasis: Color::Magenta, heading: Color::Blue,
emphasis: Color::Blue,
strong: Color::Yellow, strong: Color::Yellow,
inline_code: Color::Green, inline_code: Color::Green,
link: Color::Blue, link: Color::Blue,
quote: Color::DarkGrey, quote: Color::DarkGrey,
info: Color::Blue,
warning: Color::Yellow,
success: Color::Green,
error: Color::Red,
spinner_active: Color::Blue, spinner_active: Color::Blue,
spinner_done: Color::Green, spinner_done: Color::Green,
spinner_failed: Color::Red, spinner_failed: Color::Red,
@@ -42,6 +52,21 @@ impl Default for ColorTheme {
} }
} }
impl ColorTheme {
#[must_use]
pub fn without_color() -> Self {
Self {
enabled: false,
..Self::default()
}
}
#[must_use]
pub fn enabled(&self) -> bool {
self.enabled
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Spinner { pub struct Spinner {
frame_index: usize, frame_index: usize,
@@ -67,12 +92,19 @@ impl Spinner {
out, out,
SavePosition, SavePosition,
MoveToColumn(0), MoveToColumn(0),
Clear(ClearType::CurrentLine), Clear(ClearType::CurrentLine)
)?;
if theme.enabled() {
queue!(
out,
SetForegroundColor(theme.spinner_active), SetForegroundColor(theme.spinner_active),
Print(format!("{frame} {label}")), Print(format!("{frame} {label}")),
ResetColor, ResetColor,
RestorePosition RestorePosition
)?; )?;
} else {
queue!(out, Print(format!("{frame} {label}")), RestorePosition)?;
}
out.flush() out.flush()
} }
@@ -83,14 +115,17 @@ impl Spinner {
out: &mut impl Write, out: &mut impl Write,
) -> io::Result<()> { ) -> io::Result<()> {
self.frame_index = 0; self.frame_index = 0;
execute!(out, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
if theme.enabled() {
execute!( execute!(
out, out,
MoveToColumn(0),
Clear(ClearType::CurrentLine),
SetForegroundColor(theme.spinner_done), SetForegroundColor(theme.spinner_done),
Print(format!("{label}\n")), Print(format!("{label}\n")),
ResetColor ResetColor
)?; )?;
} else {
execute!(out, Print(format!("{label}\n")))?;
}
out.flush() out.flush()
} }
@@ -101,14 +136,17 @@ impl Spinner {
out: &mut impl Write, out: &mut impl Write,
) -> io::Result<()> { ) -> io::Result<()> {
self.frame_index = 0; self.frame_index = 0;
execute!(out, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
if theme.enabled() {
execute!( execute!(
out, out,
MoveToColumn(0),
Clear(ClearType::CurrentLine),
SetForegroundColor(theme.spinner_failed), SetForegroundColor(theme.spinner_failed),
Print(format!("{label}\n")), Print(format!("{label}\n")),
ResetColor ResetColor
)?; )?;
} else {
execute!(out, Print(format!("{label}\n")))?;
}
out.flush() out.flush()
} }
} }
@@ -123,6 +161,9 @@ struct RenderState {
impl RenderState { impl RenderState {
fn style_text(&self, text: &str, theme: &ColorTheme) -> String { fn style_text(&self, text: &str, theme: &ColorTheme) -> String {
if !theme.enabled() {
return text.to_string();
}
if self.strong > 0 { if self.strong > 0 {
format!("{}", text.bold().with(theme.strong)) format!("{}", text.bold().with(theme.strong))
} else if self.emphasis > 0 { } else if self.emphasis > 0 {
@@ -163,11 +204,70 @@ impl TerminalRenderer {
Self::default() Self::default()
} }
#[must_use]
pub fn with_color(enabled: bool) -> Self {
if enabled {
Self::new()
} else {
Self {
color_theme: ColorTheme::without_color(),
..Self::default()
}
}
}
#[must_use] #[must_use]
pub fn color_theme(&self) -> &ColorTheme { pub fn color_theme(&self) -> &ColorTheme {
&self.color_theme &self.color_theme
} }
fn paint(&self, text: impl AsRef<str>, color: Color) -> String {
let text = text.as_ref();
if self.color_theme.enabled() {
format!("{}", text.with(color))
} else {
text.to_string()
}
}
fn paint_bold(&self, text: impl AsRef<str>, color: Color) -> String {
let text = text.as_ref();
if self.color_theme.enabled() {
format!("{}", text.bold().with(color))
} else {
text.to_string()
}
}
fn paint_underlined(&self, text: impl AsRef<str>, color: Color) -> String {
let text = text.as_ref();
if self.color_theme.enabled() {
format!("{}", text.underlined().with(color))
} else {
text.to_string()
}
}
#[must_use]
pub fn info(&self, text: impl AsRef<str>) -> String {
self.paint(text, self.color_theme.info)
}
#[must_use]
pub fn warning(&self, text: impl AsRef<str>) -> String {
self.paint(text, self.color_theme.warning)
}
#[must_use]
pub fn success(&self, text: impl AsRef<str>) -> String {
self.paint(text, self.color_theme.success)
}
#[must_use]
pub fn error(&self, text: impl AsRef<str>) -> String {
self.paint(text, self.color_theme.error)
}
#[must_use] #[must_use]
pub fn render_markdown(&self, markdown: &str) -> String { pub fn render_markdown(&self, markdown: &str) -> String {
let mut output = String::new(); let mut output = String::new();
@@ -235,7 +335,7 @@ impl TerminalRenderer {
let _ = write!( let _ = write!(
output, output,
"{}", "{}",
format!("`{code}`").with(self.color_theme.inline_code) self.paint(format!("`{code}`"), self.color_theme.inline_code)
); );
} }
Event::Rule => output.push_str("---\n"), Event::Rule => output.push_str("---\n"),
@@ -252,16 +352,14 @@ impl TerminalRenderer {
let _ = write!( let _ = write!(
output, output,
"{}", "{}",
format!("[{dest_url}]") self.paint_underlined(format!("[{dest_url}]"), self.color_theme.link)
.underlined()
.with(self.color_theme.link)
); );
} }
Event::Start(Tag::Image { dest_url, .. }) => { Event::Start(Tag::Image { dest_url, .. }) => {
let _ = write!( let _ = write!(
output, output,
"{}", "{}",
format!("[image:{dest_url}]").with(self.color_theme.link) self.paint(format!("[image:{dest_url}]"), self.color_theme.link)
); );
} }
Event::Start( Event::Start(
@@ -294,12 +392,16 @@ impl TerminalRenderer {
3 => "### ", 3 => "### ",
_ => "#### ", _ => "#### ",
}; };
let _ = write!(output, "{}", prefix.bold().with(self.color_theme.heading)); let _ = write!(
output,
"{}",
self.paint_bold(prefix, self.color_theme.heading)
);
} }
fn start_quote(&self, state: &mut RenderState, output: &mut String) { fn start_quote(&self, state: &mut RenderState, output: &mut String) {
state.quote += 1; state.quote += 1;
let _ = write!(output, "{}", "".with(self.color_theme.quote)); let _ = write!(output, "{}", self.paint("", self.color_theme.quote));
} }
fn start_item(state: &RenderState, output: &mut String) { fn start_item(state: &RenderState, output: &mut String) {
@@ -312,7 +414,7 @@ impl TerminalRenderer {
let _ = writeln!( let _ = writeln!(
output, output,
"{}", "{}",
format!("╭─ {code_language}").with(self.color_theme.heading) self.paint(format!("╭─ {code_language}"), self.color_theme.heading)
); );
} }
} }
@@ -320,7 +422,7 @@ impl TerminalRenderer {
fn finish_code_block(&self, code_buffer: &str, code_language: &str, output: &mut String) { fn finish_code_block(&self, code_buffer: &str, code_language: &str, output: &mut String) {
output.push_str(&self.highlight_code(code_buffer, code_language)); output.push_str(&self.highlight_code(code_buffer, code_language));
if !code_language.is_empty() { if !code_language.is_empty() {
let _ = write!(output, "{}", "╰─".with(self.color_theme.heading)); let _ = write!(output, "{}", self.paint("╰─", self.color_theme.heading));
} }
output.push_str("\n\n"); output.push_str("\n\n");
} }
@@ -342,6 +444,10 @@ impl TerminalRenderer {
#[must_use] #[must_use]
pub fn highlight_code(&self, code: &str, language: &str) -> String { pub fn highlight_code(&self, code: &str, language: &str) -> String {
if !self.color_theme.enabled() {
return code.to_string();
}
let syntax = self let syntax = self
.syntax_set .syntax_set
.find_syntax_by_token(language) .find_syntax_by_token(language)
@@ -370,6 +476,16 @@ impl TerminalRenderer {
} }
writeln!(out) writeln!(out)
} }
#[must_use]
pub fn token_usage_summary(&self, input_tokens: u64, output_tokens: u64) -> String {
format!(
"{} {} input / {} output",
self.info("Token usage:"),
input_tokens,
output_tokens
)
}
} }
#[cfg(test)] #[cfg(test)]
@@ -437,4 +553,25 @@ mod tests {
let output = String::from_utf8_lossy(&out); let output = String::from_utf8_lossy(&out);
assert!(output.contains("Working")); assert!(output.contains("Working"));
} }
#[test]
fn renderer_can_disable_color_output() {
let terminal_renderer = TerminalRenderer::with_color(false);
let markdown_output = terminal_renderer.render_markdown(
"# Heading\n\nThis is **bold** and `code`.\n\n```rust\nfn hi() {}\n```",
);
assert!(!markdown_output.contains('\u{1b}'));
assert!(markdown_output.contains("Heading"));
assert!(markdown_output.contains("fn hi() {}"));
}
#[test]
fn token_usage_summary_uses_plain_text_without_color() {
let terminal_renderer = TerminalRenderer::with_color(false);
assert_eq!(
terminal_renderer.token_usage_summary(12, 34),
"Token usage: 12 input / 34 output"
);
}
} }