use std::env;
use std::path::Path;
use std::time::Instant;
use crate::cli::Args;
use crate::commands::format_token_count;
use ctx::error::Result;
use ctx::output::{generate_context, stream_context};
use ctx::tokens;
use ctx::walker::{self, discover_files, FileEntry, WalkerConfig};
pub fn run_context(args: Args) -> Result<()> {
let start = Instant::now();
let root = env::current_dir()?;
let config = WalkerConfig {
use_gitignore: !args.no_gitignore,
use_default_ignores: !args.no_default_ignores,
custom_ignores: args.ignore_patterns,
include_patterns: args.patterns,
};
let entries = discover_files(&root, &config)?;
if entries.is_empty() {
eprintln!("No files found matching the specified patterns.");
return Ok(());
}
let encoding: tokens::Encoding = std::str::FromStr::from_str(&args.encoding)
.ok()
.ok_or_else(|| {
format!(
"Invalid encoding '{}'. Valid options: cl100k_base, o200k_base, p50k_base",
args.encoding
)
})?;
if args.count_only {
return run_count_only(&root, &entries, encoding, args.stats, start);
}
let entries = if let Some(max_tokens) = args.max_tokens {
filter_files_by_tokens(&root, &entries, max_tokens, encoding)?
} else {
entries
};
if entries.is_empty() {
eprintln!("No files fit within the token budget.");
return Ok(());
}
let result = if args.no_stream {
let result = generate_context(
&root,
&entries,
args.format.to_lib(),
!args.no_tree,
args.show_sizes,
)?;
println!("{}", result.content);
result
} else {
stream_context(
&root,
&entries,
args.format.to_lib(),
!args.no_tree,
args.show_sizes,
)?
};
if args.stats {
let elapsed = start.elapsed();
let approx_tokens = result.output_bytes.div_ceil(4);
eprintln!(
"Generated context: {} files, {}, ~{} tokens in {:.2?}",
result.file_count,
walker::format_size(result.total_size),
format_token_count(approx_tokens),
elapsed
);
}
Ok(())
}
pub fn run_count_only(
root: &Path,
entries: &[FileEntry],
encoding: tokens::Encoding,
show_stats: bool,
start: Instant,
) -> Result<()> {
let mut total_tokens = 0usize;
let mut total_chars = 0usize;
let mut file_count = 0usize;
let mut skipped_count = 0usize;
for entry in entries {
let path = root.join(&entry.relative_path);
match std::fs::read(&path) {
Ok(bytes) => {
let content = String::from_utf8_lossy(&bytes);
let token_count = tokens::count_tokens_with_encoding(&content, encoding)?;
total_tokens += token_count;
total_chars += content.chars().count(); file_count += 1;
}
Err(e) => {
eprintln!(
"Warning: could not read {}: {}",
entry.relative_path.display(),
e
);
skipped_count += 1;
}
}
}
println!("Files: {}", file_count);
if skipped_count > 0 {
println!("Skipped (unreadable): {}", skipped_count);
}
println!("Characters (UTF-8): {}", total_chars);
println!("Tokens ({}): {}", encoding.as_str(), total_tokens);
if show_stats {
let elapsed = start.elapsed();
eprintln!("Counted in {:.2?}", elapsed);
}
Ok(())
}
pub fn filter_files_by_tokens(
root: &Path,
entries: &[FileEntry],
max_tokens: usize,
encoding: tokens::Encoding,
) -> Result<Vec<FileEntry>> {
let mut file_tokens: Vec<(usize, &FileEntry)> = Vec::new();
for entry in entries {
let path = root.join(&entry.relative_path);
if let Ok(bytes) = std::fs::read(&path) {
let content = String::from_utf8_lossy(&bytes);
let token_count = tokens::count_tokens_with_encoding(&content, encoding)?;
file_tokens.push((token_count, entry));
}
}
let mut selected = Vec::new();
let mut total = 0usize;
let mut omitted = 0usize;
for (tokens, entry) in file_tokens {
if total + tokens <= max_tokens {
total += tokens;
selected.push(entry.clone());
} else {
omitted += 1;
}
}
if omitted > 0 {
eprintln!(
"Token budget: {} files included ({} tokens), {} files omitted",
selected.len(),
total,
omitted
);
}
Ok(selected)
}