use crate::tokens::{estimate_tokens, prune_to_budget};
use std::io::Write;
fn split_lines_with_tokens(text: &str) -> (Vec<&str>, Vec<usize>, usize) {
let b = text.as_bytes();
if text.is_ascii() {
let guess = (b.len() / 128).clamp(16, 1 << 20);
let mut lines: Vec<&str> = Vec::with_capacity(guess);
let mut toks: Vec<usize> = Vec::with_capacity(guess);
let mut total = 0usize;
for l in text.lines() {
let t = l.len().div_ceil(4);
lines.push(l);
toks.push(t);
total += t;
}
return (lines, toks, total);
}
let guess = (b.len() / 128).clamp(16, 1 << 20);
let mut lines: Vec<&str> = Vec::with_capacity(guess);
let mut toks: Vec<usize> = Vec::with_capacity(guess);
let mut total = 0usize;
let mut line_start = 0usize;
let mut ascii_run = 0usize;
let mut line_tokens = 0usize;
let mut i = 0usize;
while i < b.len() {
let c = b[i];
if c == b'\n' {
let mut end = i;
if end > line_start && b[end - 1] == b'\r' {
end -= 1;
}
line_tokens += ascii_run.div_ceil(4);
total += line_tokens;
lines.push(&text[line_start..end]);
toks.push(line_tokens);
i += 1;
line_start = i;
ascii_run = 0;
line_tokens = 0;
continue;
}
if c < 0x80 {
if c == b'\r' && i + 1 < b.len() && b[i + 1] == b'\n' {
i += 1;
continue;
}
ascii_run += 1;
if ascii_run == 4 {
line_tokens += 1;
ascii_run = 0;
}
} else if c & 0xC0 != 0x80 {
if ascii_run > 0 {
line_tokens += ascii_run.div_ceil(4);
ascii_run = 0;
}
line_tokens += 1;
}
i += 1;
}
if line_start < b.len() {
line_tokens += ascii_run.div_ceil(4);
total += line_tokens;
lines.push(&text[line_start..]);
toks.push(line_tokens);
}
(lines, toks, total)
}
pub fn cmd_compact(file: &str, max_tokens: usize) -> anyhow::Result<()> {
let text = std::fs::read_to_string(file)?;
let (lines, toks, total) = split_lines_with_tokens(&text);
let stdout = std::io::stdout();
let mut out = std::io::BufWriter::with_capacity(256 * 1024, stdout.lock());
if total <= max_tokens {
out.write_all(text.as_bytes())?;
out.write_all(b"\n")?;
out.flush()?;
eprintln!("[rdsh] no compaction needed ({total} <= {max_tokens} tokens)");
return Ok(());
}
let first = lines.first().copied().unwrap_or("");
let mut kept: Vec<&str> = vec![];
let mut used = toks.first().copied().unwrap_or(0) + 200;
for (l, t) in lines.iter().zip(toks.iter()).rev() {
if used + *t > max_tokens {
break;
}
used += *t;
kept.push(l);
}
kept.reverse();
let dropped = lines.len().saturating_sub(kept.len());
let summary =
format!("...[rdsh compact: dropped ~{dropped} turn(s), {total}-><={max_tokens} tokens]...");
let mut body = String::new();
body.reserve(first.len() + summary.len() + kept.iter().map(|l| l.len() + 1).sum::<usize>() + 2);
body.push_str(first);
body.push('\n');
body.push_str(&summary);
body.push('\n');
for l in &kept {
if *l == first {
continue;
}
body.push_str(l);
body.push('\n');
}
let body = prune_to_budget(&body, max_tokens);
out.write_all(body.as_bytes())?;
out.write_all(b"\n")?;
out.flush()?;
eprintln!(
"[rdsh] compacted {total} -> ~{} tokens ({} lines kept of {})",
estimate_tokens(&body),
kept.len(),
lines.len()
);
Ok(())
}