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 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()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_ctx_footer() {
assert_eq!(
strip_ctx_footer("Hello\n\nctx: 84K/200K 42% | 406 tok/s"),
"Hello"
);
assert_eq!(strip_ctx_footer("ctx: 8K/200K 4%"), "");
assert_eq!(
strip_ctx_footer("Some text\nctx: 1M/2M 50% | 123 tok/s\nMore text"),
"Some text\nMore text"
);
assert_eq!(
strip_ctx_footer("Note: something here"),
"Note: something here"
);
let multi = "line1\nline2\nline3";
assert_eq!(strip_ctx_footer(multi), multi);
}
#[test]
fn test_truncate_str_ascii() {
assert_eq!(truncate_str("hello world", 5), "hello");
assert_eq!(truncate_str("hello", 10), "hello");
assert_eq!(truncate_str("hello", 5), "hello");
}
#[test]
fn test_truncate_str_multibyte_boundary() {
let s = "abc█def";
assert_eq!(truncate_str(s, 3), "abc"); assert_eq!(truncate_str(s, 4), "abc"); assert_eq!(truncate_str(s, 5), "abc"); assert_eq!(truncate_str(s, 6), "abc█"); }
#[test]
fn test_truncate_str_emoji() {
let s = "hi🦀bye";
assert_eq!(truncate_str(s, 2), "hi");
assert_eq!(truncate_str(s, 3), "hi"); assert_eq!(truncate_str(s, 5), "hi"); assert_eq!(truncate_str(s, 6), "hi🦀");
}
#[test]
fn test_truncate_str_zero() {
assert_eq!(truncate_str("hello", 0), "");
assert_eq!(truncate_str("🦀", 0), "");
}
#[test]
fn test_truncate_str_empty() {
assert_eq!(truncate_str("", 5), "");
assert_eq!(truncate_str("", 0), "");
}
#[test]
fn test_truncate_str_all_multibyte() {
let s = "███"; assert_eq!(truncate_str(s, 1), ""); assert_eq!(truncate_str(s, 3), "█");
assert_eq!(truncate_str(s, 7), "██"); assert_eq!(truncate_str(s, 9), "███");
}
#[test]
fn test_truncate_middle_preserves_tail() {
let s = "/Users/alice/srv/myapp/lib/presentation/pages/some_really_long_widget_name.dart";
let out = truncate_middle(s, 40);
assert!(
out.ends_with("some_really_long_widget_name.dart")
|| out.ends_with("_name.dart")
|| out.ends_with(".dart"),
"expected filename tail preserved, got: {}",
out
);
assert!(out.contains('…'), "expected ellipsis marker in middle");
assert!(out.len() <= 40, "expected <= 40 bytes, got {}", out.len());
}
#[test]
fn test_truncate_middle_short_string_unchanged() {
assert_eq!(truncate_middle("short.rs", 80), "short.rs");
}
#[test]
fn test_truncate_middle_tiny_budget_falls_back() {
let out = truncate_middle("hello world", 4);
assert!(out.len() <= 4);
}
#[test]
fn test_tilde_home_collapses_prefix() {
if let Some(h) = dirs::home_dir() {
let home_str = h.to_string_lossy();
let full = format!("{}/srv/project/file.rs", home_str);
assert_eq!(tilde_home(&full), "~/srv/project/file.rs");
assert_eq!(tilde_home("/tmp/foo"), "/tmp/foo");
let cmd = format!("cp {}/a {}/b", home_str, home_str);
assert_eq!(tilde_home(&cmd), "cp ~/a ~/b");
}
}
#[test]
fn test_looks_like_file_path_absolute_with_segments() {
assert!(looks_like_file_path("/Users/alice/Downloads/report.pdf"));
assert!(looks_like_file_path("/tmp/foo.txt"));
assert!(looks_like_file_path("/etc/hosts"));
}
#[test]
fn test_looks_like_file_path_with_extension_and_text() {
assert!(looks_like_file_path("/report.pdf check this"));
assert!(looks_like_file_path("/data.csv analyze"));
}
#[test]
fn test_looks_like_file_path_slash_commands_are_not_paths() {
assert!(!looks_like_file_path("/help"));
assert!(!looks_like_file_path("/models"));
assert!(!looks_like_file_path("/deploy staging"));
assert!(!looks_like_file_path("/credits"));
}
#[test]
fn test_looks_like_file_path_no_slash_prefix() {
assert!(!looks_like_file_path("hello world"));
assert!(!looks_like_file_path("report.pdf"));
}
#[test]
fn test_format_ctx_footer_k_values() {
assert_eq!(format_ctx_footer(8000, 200000, None), "ctx: 8K/200K 4%");
}
#[test]
fn test_format_ctx_footer_small_values() {
assert_eq!(format_ctx_footer(500, 200000, None), "ctx: 500/200K 0%");
}
#[test]
fn test_format_ctx_footer_m_values() {
assert_eq!(
format_ctx_footer(1200000, 2000000, None),
"ctx: 1.2M/2.0M 60%"
);
}
#[test]
fn test_format_ctx_footer_zero_used() {
assert_eq!(format_ctx_footer(0, 200000, None), "ctx: 0/200K 0%");
}
#[test]
fn test_format_ctx_footer_zero_max() {
assert_eq!(format_ctx_footer(5000, 0, None), "ctx: 5K/0 0%");
}
#[test]
fn test_format_ctx_footer_full() {
assert_eq!(
format_ctx_footer(200000, 200000, None),
"ctx: 200K/200K 100%"
);
}
#[test]
fn test_format_ctx_footer_with_tps() {
assert_eq!(
format_ctx_footer(8000, 200000, Some(45.7)),
"ctx: 8K/200K 4% | 46 tok/s"
);
assert_eq!(
format_ctx_footer(500, 200000, Some(123.4)),
"ctx: 500/200K 0% | 123 tok/s"
);
}
}