tui lib for nicer cli

This commit is contained in:
Dhanji Prasanna
2025-10-01 11:19:34 +10:00
parent 5f642061de
commit 1621d081ec
4 changed files with 384 additions and 72 deletions

281
Cargo.lock generated
View File

@@ -436,7 +436,7 @@ dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width",
"unicode-width 0.2.0",
"windows-sys 0.59.0",
]
@@ -475,6 +475,24 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convert_case"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "coolor"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3"
dependencies = [
"crossterm",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -510,6 +528,115 @@ dependencies = [
"libc",
]
[[package]]
name = "crokey"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51360853ebbeb3df20c76c82aecf43d387a62860f1a59ba65ab51f00eea85aad"
dependencies = [
"crokey-proc_macros",
"crossterm",
"once_cell",
"serde",
"strict",
]
[[package]]
name = "crokey-proc_macros"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf1a727caeb5ee5e0a0826a97f205a9cf84ee964b0b48239fef5214a00ae439"
dependencies = [
"crossterm",
"proc-macro2",
"quote",
"strict",
"syn",
]
[[package]]
name = "crossbeam"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
"bitflags 2.9.4",
"crossterm_winapi",
"derive_more 2.0.1",
"document-features",
"mio",
"parking_lot",
"rustix 1.0.8",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "crunchy"
version = "0.2.4"
@@ -539,6 +666,27 @@ dependencies = [
"syn",
]
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"convert_case 0.7.1",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -611,6 +759,15 @@ dependencies = [
"const-random",
]
[[package]]
name = "document-features"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
dependencies = [
"litrs",
]
[[package]]
name = "either"
version = "1.15.0"
@@ -818,6 +975,7 @@ dependencies = [
"anyhow",
"chrono",
"clap",
"crossterm",
"dirs 5.0.1",
"g3-config",
"g3-core",
@@ -825,6 +983,7 @@ dependencies = [
"rustyline",
"serde",
"serde_json",
"termimad",
"tokio",
"tokio-util",
"tracing",
@@ -1310,7 +1469,7 @@ dependencies = [
"console",
"number_prefix",
"portable-atomic",
"unicode-width",
"unicode-width 0.2.0",
"web-time",
]
@@ -1405,6 +1564,29 @@ dependencies = [
"serde",
]
[[package]]
name = "lazy-regex"
version = "3.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126"
dependencies = [
"lazy-regex-proc_macros",
"once_cell",
"regex",
]
[[package]]
name = "lazy-regex-proc_macros"
version = "3.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -1470,13 +1652,19 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "litrs"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
[[package]]
name = "llama_cpp"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f126770a2ed5e0e4596119479dc56f56b99037246bf0e36c544f7581a9458fd"
dependencies = [
"derive_more",
"derive_more 0.99.20",
"futures",
"llama_cpp_sys",
"num_cpus",
@@ -1540,6 +1728,15 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimad"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2"
dependencies = [
"once_cell",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -1562,6 +1759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
@@ -2167,7 +2365,7 @@ dependencies = [
"nix",
"radix_trie",
"unicode-segmentation",
"unicode-width",
"unicode-width 0.2.0",
"utf8parse",
"windows-sys 0.60.2",
]
@@ -2340,6 +2538,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.6"
@@ -2387,6 +2606,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strict"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006"
[[package]]
name = "strsim"
version = "0.11.1"
@@ -2461,6 +2686,22 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "termimad"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ff5ca043d65d4ea43b65cdb4e3aba119657d0d12caf44f93212ec3168a8e20"
dependencies = [
"coolor",
"crokey",
"crossbeam",
"lazy-regex",
"minimad",
"serde",
"thiserror 2.0.16",
"unicode-width 0.1.14",
]
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -2757,9 +2998,15 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.2.1"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "url"
@@ -2981,6 +3228,22 @@ dependencies = [
"rustix 0.38.44",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
@@ -2990,6 +3253,12 @@ dependencies = [
"windows-sys 0.61.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.1"

View File

@@ -19,3 +19,5 @@ dirs = "5.0"
tokio-util = "0.7"
indicatif = "0.17"
chrono = { version = "0.4", features = ["serde"] }
crossterm = "0.29.0"
termimad = "0.34.0"

View File

@@ -8,6 +8,9 @@ use std::path::PathBuf;
use tokio_util::sync::CancellationToken;
use tracing::{error, info};
mod tui;
use tui::SimpleOutput;
#[derive(Parser)]
#[command(name = "g3")]
#[command(about = "A modular, composable AI coding agent")]
@@ -122,14 +125,16 @@ pub async fn run() -> Result<()> {
} else if let Some(task) = cli.task {
// Single-shot mode
info!("Executing task: {}", task);
let output = SimpleOutput::new();
let result = agent
.execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true)
.await?;
println!("{}", result);
output.print_markdown(&result);
} else {
let output = SimpleOutput::new();
// Interactive mode (default)
info!("Starting interactive mode");
println!("📁 Workspace: {}", project.workspace().display());
output.print(&format!("📁 Workspace: {}", project.workspace().display()));
run_interactive(agent, cli.show_prompt, cli.show_code).await?;
}
@@ -137,29 +142,30 @@ pub async fn run() -> Result<()> {
}
async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -> Result<()> {
let output = SimpleOutput::new();
println!();
println!("🤖 G3 AI Coding Agent - Interactive Mode");
println!(
output.print("");
output.print("🤖 G3 AI Coding Agent - Interactive Mode");
output.print(
"I solve problems by writing and executing code. Tell me what you need to accomplish!"
);
println!();
output.print("");
// Display provider and model information
match agent.get_provider_info() {
Ok((provider, model)) => {
println!("🔧 Provider: {} | Model: {}", provider, model);
output.print(&format!("🔧 Provider: {} | Model: {}", provider, model));
}
Err(e) => {
error!("Failed to get provider info: {}", e);
}
}
println!();
println!("Type 'exit' or 'quit' to exit, use Up/Down arrows for command history");
println!("For multiline input: use \\ at the end of a line to continue");
println!("Submit multiline with Enter (without backslash)");
println!();
output.print("");
output.print("Type 'exit' or 'quit' to exit, use Up/Down arrows for command history");
output.print("For multiline input: use \\ at the end of a line to continue");
output.print("Submit multiline with Enter (without backslash)");
output.print("");
// Initialize rustyline editor with history
let mut rl = DefaultEditor::new()?;
@@ -180,7 +186,7 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -
loop {
// Display context window progress bar before each prompt
display_context_progress(&agent);
display_context_progress(&agent, &output);
// Adjust prompt based on whether we're in multi-line mode
let prompt = if in_multiline { "... > " } else { "g3> " };
@@ -220,7 +226,7 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -
}
// Process the multiline input
execute_task(&mut agent, &input, show_prompt, show_code).await;
execute_task(&mut agent, &input, show_prompt, show_code, &output).await;
} else {
// Single line input
let input = line.trim().to_string();
@@ -237,23 +243,23 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -
rl.add_history_entry(&input)?;
// Process the single line input
execute_task(&mut agent, &input, show_prompt, show_code).await;
execute_task(&mut agent, &input, show_prompt, show_code, &output).await;
}
}
Err(ReadlineError::Interrupted) => {
// Ctrl-C pressed
if in_multiline {
// Cancel multiline input
println!("Multi-line input cancelled");
output.print("Multi-line input cancelled");
multiline_buffer.clear();
in_multiline = false;
} else {
println!("CTRL-C");
output.print("CTRL-C");
}
continue;
}
Err(ReadlineError::Eof) => {
println!("CTRL-D");
output.print("CTRL-D");
break;
}
Err(err) => {
@@ -268,14 +274,14 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -
let _ = rl.save_history(history_path);
}
println!("👋 Goodbye!");
output.print("👋 Goodbye!");
Ok(())
}
async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_code: bool) {
async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_code: bool, output: &SimpleOutput) {
// Show thinking indicator immediately
print!("🤔 Thinking...");
std::io::stdout().flush().unwrap();
output.print("🤔 Thinking...");
// Note: flush is handled internally by println
// Create cancellation token for this request
let cancellation_token = CancellationToken::new();
@@ -290,16 +296,16 @@ async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_co
}
_ = tokio::signal::ctrl_c() => {
cancel_token_clone.cancel();
println!("\n⚠️ Operation cancelled by user (Ctrl+C)");
output.print("\n⚠️ Operation cancelled by user (Ctrl+C)");
return;
}
};
match execution_result {
Ok(response) => println!("{}", response),
Ok(response) => output.print_markdown(&response),
Err(e) => {
if e.to_string().contains("cancelled") {
println!("⚠️ Operation cancelled by user");
output.print("⚠️ Operation cancelled by user");
} else {
error!("Error: {}", e);
}
@@ -307,24 +313,9 @@ async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_co
}
}
fn display_context_progress(agent: &Agent) {
fn display_context_progress(agent: &Agent, output: &SimpleOutput) {
let context = agent.get_context_window();
let percentage = context.percentage_used();
// Create a simple visual progress bar using the requested characters (10 dots max)
let bar_width = 10;
let filled_width = ((percentage / 100.0) * bar_width as f32) as usize;
let empty_width = bar_width - filled_width;
let filled_chars = "".repeat(filled_width);
let empty_chars = "".repeat(empty_width);
let progress_bar = format!("{}{}", filled_chars, empty_chars);
// Print context info with visual progress bar
println!(
"Context: {} {:.1}% | {}/{} tokens",
progress_bar, percentage, context.used_tokens, context.total_tokens
);
output.print_context(context.used_tokens, context.total_tokens, context.percentage_used());
}
/// Set up the workspace directory for autonomous mode
@@ -342,10 +333,11 @@ fn setup_workspace_directory() -> Result<PathBuf> {
// Create the directory if it doesn't exist
if !workspace_dir.exists() {
std::fs::create_dir_all(&workspace_dir)?;
println!(
let output = SimpleOutput::new();
output.print(&format!(
"📁 Created workspace directory: {}",
workspace_dir.display()
);
));
}
Ok(workspace_dir)
@@ -359,14 +351,16 @@ async fn run_autonomous(
show_code: bool,
max_turns: usize,
) -> Result<()> {
println!("🤖 G3 AI Coding Agent - Autonomous Mode");
println!("📁 Using workspace: {}", project.workspace().display());
let output = SimpleOutput::new();
output.print("🤖 G3 AI Coding Agent - Autonomous Mode");
output.print(&format!("📁 Using workspace: {}", project.workspace().display()));
// Check if requirements exist
if !project.has_requirements() {
println!("❌ Error: requirements.md not found in workspace directory");
println!(" Please create a requirements.md file with your project requirements at:");
println!(" {}/requirements.md", project.workspace().display());
output.print("❌ Error: requirements.md not found in workspace directory");
output.print(" Please create a requirements.md file with your project requirements at:");
output.print(&format!(" {}/requirements.md", project.workspace().display()));
return Ok(());
}
@@ -374,20 +368,20 @@ async fn run_autonomous(
let requirements = match project.read_requirements()? {
Some(content) => content,
None => {
println!("❌ Error: Could not read requirements.md");
output.print("❌ Error: Could not read requirements.md");
return Ok(());
}
};
println!("📋 Requirements loaded from requirements.md");
println!("🔄 Starting coach-player feedback loop...");
output.print("📋 Requirements loaded from requirements.md");
output.print("🔄 Starting coach-player feedback loop...");
let mut turn = 1;
let mut coach_feedback = String::new();
let mut implementation_approved = false;
loop {
println!("\n=== TURN {}/{} - PLAYER MODE ===", turn, max_turns);
output.print(&format!("\n=== TURN {}/{} - PLAYER MODE ===", turn, max_turns));
// Player mode: implement requirements (with coach feedback if available)
let player_prompt = if coach_feedback.is_empty() {
@@ -402,13 +396,13 @@ async fn run_autonomous(
)
};
println!("🎯 Starting player implementation...");
output.print("🎯 Starting player implementation...");
let player_result = agent
.execute_task_with_timing(&player_prompt, None, false, show_prompt, show_code, true)
.await;
if let Err(e) = player_result {
println!("❌ Player implementation failed: {}", e);
output.print(&format!("❌ Player implementation failed: {}", e));
}
// Create a new agent instance for coach mode to ensure fresh context
@@ -418,7 +412,7 @@ async fn run_autonomous(
// Ensure coach agent is also in the workspace directory
project.enter_workspace()?;
println!("\n=== TURN {}/{} - COACH MODE ===", turn, max_turns);
output.print(&format!("\n=== TURN {}/{} - COACH MODE ===", turn, max_turns));
// Coach mode: critique the implementation
let coach_prompt = format!(
@@ -442,26 +436,26 @@ Keep your response concise and focused on actionable items.",
requirements
);
println!("🎓 Starting coach review...");
output.print("🎓 Starting coach review...");
let coach_result = coach_agent
.execute_task_with_timing(&coach_prompt, None, false, show_prompt, show_code, true)
.await?;
println!("🎓 Coach review completed");
println!("Coach feedback: {}", coach_result);
output.print("🎓 Coach review completed");
output.print(&format!("Coach feedback: {}", coach_result));
// Check if coach approved the implementation
if coach_result.contains("IMPLEMENTATION_APPROVED") {
println!("\n=== SESSION COMPLETED - IMPLEMENTATION APPROVED ===");
println!("✅ Coach approved the implementation!");
output.print("\n=== SESSION COMPLETED - IMPLEMENTATION APPROVED ===");
output.print("✅ Coach approved the implementation!");
implementation_approved = true;
break;
}
// Check if we've reached max turns
if turn >= max_turns {
println!("\n=== SESSION COMPLETED - MAX TURNS REACHED ===");
println!("⏰ Maximum turns ({}) reached", max_turns);
output.print("\n=== SESSION COMPLETED - MAX TURNS REACHED ===");
output.print(&format!("⏰ Maximum turns ({}) reached", max_turns));
break;
}
@@ -469,13 +463,13 @@ Keep your response concise and focused on actionable items.",
coach_feedback = coach_result;
turn += 1;
println!("🔄 Coach provided feedback for next iteration");
output.print("🔄 Coach provided feedback for next iteration");
}
if implementation_approved {
println!("\n🎉 Autonomous mode completed successfully");
output.print("\n🎉 Autonomous mode completed successfully");
} else {
println!("\n🔄 Autonomous mode completed (max iterations)");
output.print("\n🔄 Autonomous mode completed (max iterations)");
}
Ok(())

47
crates/g3-cli/src/tui.rs Normal file
View File

@@ -0,0 +1,47 @@
use crossterm::style::Color;
use termimad::MadSkin;
/// Simple output handler with markdown support
pub struct SimpleOutput {
mad_skin: MadSkin,
}
impl SimpleOutput {
pub fn new() -> Self {
let mut mad_skin = MadSkin::default();
// Configure termimad skin for better markdown rendering
mad_skin.set_headers_fg(Color::Cyan);
mad_skin.bold.set_fg(Color::Yellow);
mad_skin.italic.set_fg(Color::Magenta);
mad_skin.code_block.set_bg(Color::Rgb { r: 40, g: 40, b: 40 });
Self { mad_skin }
}
pub fn print(&self, text: &str) {
println!("{}", text);
}
pub fn print_markdown(&self, markdown: &str) {
self.mad_skin.print_text(markdown);
}
pub fn print_status(&self, status: &str) {
println!("📊 {}", status);
}
pub fn print_context(&self, used: u32, total: u32, percentage: f32) {
let bar_width = 10;
let filled_width = ((percentage / 100.0) * bar_width as f32) as usize;
let empty_width = bar_width - filled_width;
let filled_chars = "".repeat(filled_width);
let empty_chars = "".repeat(empty_width);
let progress_bar = format!("{}{}", filled_chars, empty_chars);
println!(
"Context: {} {:.1}% | {}/{} tokens",
progress_bar, percentage, used, total
);
}
}