use serde_json::Value;
pub mod diff;
pub mod render;
pub mod trim;
pub const RESET: &str = "\x1b[0m";
pub const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m";
pub const BLACK: &str = "\x1b[30m";
pub const RED: &str = "\x1b[31m";
pub const GREEN: &str = "\x1b[32m";
pub const YELLOW: &str = "\x1b[33m";
pub const BLUE: &str = "\x1b[34m";
pub const MAGENTA: &str = "\x1b[35m";
pub const CYAN: &str = "\x1b[36m";
pub const WHITE: &str = "\x1b[37m";
pub const LIGHT_GREY: &str = "\x1b[90m";
pub const LIGHT_RED: &str = "\x1b[91m";
pub const LIGHT_GREEN: &str = "\x1b[92m";
pub const LIGHT_YELLOW: &str = "\x1b[93m";
pub const LIGHT_BLUE: &str = "\x1b[94m";
pub const LIGHT_MAGENTA: &str = "\x1b[95m";
pub const LIGHT_CYAN: &str = "\x1b[96m";
pub const BG_GREEN: &str = "\x1b[48;5;22m";
pub const BG_RED: &str = "\x1b[48;5;52m";
fn truncate(s: &str, max: usize) -> String {
if max == 0 {
return String::new();
}
let keep = max.saturating_sub(1);
truncate_with_ellipsis(s, keep, "…")
}
pub(crate) fn truncate_chars(input: &str, max_chars: usize) -> String {
truncate_with_ellipsis(input, max_chars, "...")
}
fn truncate_with_ellipsis(input: &str, max: usize, ellipsis: &str) -> String {
if max == 0 {
return String::new();
}
if input.len() <= max && input.is_ascii() {
return input.to_string();
}
let mut cut: Option<usize> = None;
for (char_count, (i, ch)) in input.char_indices().enumerate() {
if char_count == max {
cut = Some(i);
break;
}
cut = Some(i + ch.len_utf8());
}
match cut {
Some(end) if end < input.len() => {
let mut out = String::with_capacity(end + ellipsis.len());
out.push_str(&input[..end]);
out.push_str(ellipsis);
out
}
_ => input.to_string(),
}
}
pub(super) fn normalize_tool_name(name: &str) -> String {
name.trim()
.to_ascii_lowercase()
.replace(['-', '.', ' '], "_")
}
pub fn lookup_args<'a>(_name: &str, args: Option<&'a Value>) -> &'a Value {
static EMPTY: Value = Value::Null;
args.unwrap_or(&EMPTY)
}
pub use diff::{
compute_diff, render_split_diff, render_unified_diff, DiffFormatter, DiffHunk, DiffLine,
DiffOp, DiffResult,
};
pub use render::{
render_tree, render_tool_output, DiagnosticsFormatter, FileSummaryFormatter, GitStatusFormatter,
ImpactFormatter, PhaseFormatter, ProjectMapFormatter, SearchFormatter, SymbolLookupFormatter,
};
pub use trim::trim_llm_payload;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_with_ellipsis_contracts() {
assert_eq!(truncate_with_ellipsis("hello", 10, "..."), "hello");
assert_eq!(truncate_with_ellipsis("hello", 5, "..."), "hello");
assert_eq!(truncate_with_ellipsis("helloworld", 5, "..."), "hello...");
assert_eq!(truncate_with_ellipsis("hello", 0, "..."), "");
assert_eq!(truncate_with_ellipsis("héllo", 3, "…"), "hél…");
assert_eq!(truncate("hello world", 6), "hello…");
assert_eq!(truncate_chars("hello world", 5), "hello...");
}
#[test]
fn test_truncate_matches_previous_contract() {
assert_eq!(truncate("short", 60), "short");
assert_eq!(truncate(&"a".repeat(100), 5), "aaaa…");
assert_eq!(truncate_chars("foo", 10), "foo");
assert_eq!(truncate_chars(&"a".repeat(300), 240).len(), 243);
assert!(truncate_chars(&"a".repeat(300), 240).ends_with("..."));
}
}