pub fn truncate_str(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes {
return s;
}
let mut end = max_bytes;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
pub fn looks_like_file_path(s: &str) -> bool {
if !s.starts_with('/') {
return false;
}
if s[1..].contains('/') {
return true;
}
let first_word = s.split_whitespace().next().unwrap_or(s);
if let Some(ext) = std::path::Path::new(first_word).extension()
&& !ext.is_empty()
{
return true;
}
false
}
pub fn tilde_home(s: &str) -> String {
let home = match dirs::home_dir() {
Some(h) => h,
None => return s.to_string(),
};
let home_str = home.to_string_lossy();
if home_str.is_empty() {
return s.to_string();
}
s.replace(home_str.as_ref(), "~")
}
pub fn truncate_middle(s: &str, max_bytes: usize) -> String {
if s.len() <= max_bytes {
return s.to_string();
}
const ELLIPSIS: &str = "…"; if max_bytes <= ELLIPSIS.len() + 2 {
return truncate_str(s, max_bytes).to_string();
}
let budget = max_bytes - ELLIPSIS.len();
let tail_bytes = budget.div_ceil(2);
let head_bytes = budget - tail_bytes;
let mut head_end = head_bytes;
while head_end > 0 && !s.is_char_boundary(head_end) {
head_end -= 1;
}
let mut tail_start = s.len() - tail_bytes;
while tail_start < s.len() && !s.is_char_boundary(tail_start) {
tail_start += 1;
}
if tail_start <= head_end {
return truncate_str(s, max_bytes).to_string();
}
format!("{}{}{}", &s[..head_end], ELLIPSIS, &s[tail_start..])
}
fn format_token_count(tokens: u32) -> String {
let tokens = tokens as f64;
if tokens >= 1_000_000.0 {
format!("{:.1}M", tokens / 1_000_000.0)
} else if tokens >= 1_000.0 {
format!("{:.0}K", tokens / 1_000.0)
} else if tokens > 0.0 {
format!("{}", tokens as u32)
} else {
"0".to_string()
}
}
pub fn format_ctx_footer(used: u32, max: u32, tps: Option<f64>) -> String {
let pct = if max > 0 {
(used as f64 / max as f64) * 100.0
} else {
0.0
};
let base = format!(
"ctx: {}/{} {:.0}%",
format_token_count(used),
format_token_count(max),
pct
);
if let Some(rate) = tps {
format!("{} | {:.0} tok/s", base, rate)
} else {
base
}
}
pub fn md_table(headers: &[&str], rows: &[Vec<String>]) -> String {
if rows.is_empty() {
return String::new();
}
let clean = |s: &str| s.replace(['|', '\n'], " ");
let mut out = String::new();
out.push_str("| ");
out.push_str(
&headers
.iter()
.map(|h| clean(h))
.collect::<Vec<_>>()
.join(" | "),
);
out.push_str(" |\n|");
for _ in headers {
out.push_str(" --- |");
}
out.push('\n');
for row in rows {
out.push_str("| ");
out.push_str(&row.iter().map(|c| clean(c)).collect::<Vec<_>>().join(" | "));
out.push_str(" |\n");
}
out
}
pub fn strip_ctx_footer(text: &str) -> String {
text.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.starts_with("ctx:") || !trimmed.contains('/') || !trimmed.contains('%')
})
.collect::<Vec<_>>()
.join("\n")
.trim_end_matches('\n')
.to_string()
}
pub fn thinking_excerpt(thinking: &str) -> Option<String> {
thinking_excerpt_capped(thinking, THINKING_STATUS_CHARS)
}
pub const THINKING_STATUS_CHARS: usize = 80;
pub fn thinking_excerpt_capped(thinking: &str, max_chars: usize) -> Option<String> {
let trimmed = thinking.trim();
if trimmed.len() < 20 {
return None;
}
let mut sentences: Vec<&str> = trimmed
.split(['.', '?', '!', '\n'])
.map(str::trim)
.filter(|s| s.len() >= 12)
.collect();
let last = sentences.pop()?;
let cleaned = last
.strip_prefix("I am ")
.or_else(|| last.strip_prefix("I'm "))
.or_else(|| last.strip_prefix("I will "))
.or_else(|| last.strip_prefix("Let me "))
.or_else(|| last.strip_prefix("Let us "))
.unwrap_or(last)
.trim();
if cleaned.is_empty() {
return None;
}
let mut chars = cleaned.chars();
let first = chars.next()?;
let rest: String = chars.collect();
let pretty = format!("{}{}", first.to_uppercase(), rest);
let capped: String = pretty.chars().take(max_chars).collect();
Some(if pretty.chars().count() > max_chars {
format!("{capped}…")
} else {
capped
})
}
pub const THINKING_EXCERPT_CHARS: usize = 300;