use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
pub fn capitalize_first(input: &str) -> String {
let mut chars = input.chars();
match chars.next() {
Some(first) => {
let mut capitalized = first.to_uppercase().collect::<String>();
capitalized.push_str(chars.as_str());
capitalized
}
None => String::new(),
}
}
pub fn format_and_truncate_tool_result(text: &str, max_lines: usize, line_width: usize) -> String {
let max_graphemes = (max_lines * line_width).saturating_sub(max_lines);
if let Some(formatted_json) = format_json_compact(text) {
truncate_text(&formatted_json, max_graphemes)
} else {
truncate_text(text, max_graphemes)
}
}
pub fn format_json_compact(text: &str) -> Option<String> {
let json = serde_json::from_str::<serde_json::Value>(text).ok()?;
let json_pretty = serde_json::to_string_pretty(&json).unwrap_or_else(|_| json.to_string());
let mut result = String::new();
let mut chars = json_pretty.chars().peekable();
let mut in_string = false;
let mut escape_next = false;
while let Some(ch) = chars.next() {
match ch {
'"' if !escape_next => {
in_string = !in_string;
result.push(ch);
}
'\\' if in_string => {
escape_next = !escape_next;
result.push(ch);
}
'\n' | '\r' if !in_string => {
}
' ' | '\t' if !in_string => {
if let (Some(&next_ch), Some(last_ch)) = (chars.peek(), result.chars().last()) {
if (last_ch == ':' || last_ch == ',') && !matches!(next_ch, '}' | ']') {
result.push(' ');
}
}
}
_ => {
if escape_next && in_string {
escape_next = false;
}
result.push(ch);
}
}
}
Some(result)
}
pub fn truncate_text(text: &str, max_graphemes: usize) -> String {
let mut graphemes = text.grapheme_indices(true);
if let Some((byte_index, _)) = graphemes.nth(max_graphemes) {
if max_graphemes >= 3 {
let mut truncate_graphemes = text.grapheme_indices(true);
if let Some((truncate_byte_index, _)) = truncate_graphemes.nth(max_graphemes - 3) {
let truncated = &text[..truncate_byte_index];
format!("{truncated}...")
} else {
text.to_string()
}
} else {
let truncated = &text[..byte_index];
truncated.to_string()
}
} else {
text.to_string()
}
}
pub fn center_truncate_path(path: &str, max_width: usize) -> String {
if max_width == 0 {
return String::new();
}
if UnicodeWidthStr::width(path) <= max_width {
return path.to_string();
}
let sep = std::path::MAIN_SEPARATOR;
let has_leading_sep = path.starts_with(sep);
let has_trailing_sep = path.ends_with(sep);
let mut raw_segments: Vec<&str> = path.split(sep).collect();
if has_leading_sep && !raw_segments.is_empty() && raw_segments[0].is_empty() {
raw_segments.remove(0);
}
if has_trailing_sep
&& !raw_segments.is_empty()
&& raw_segments.last().is_some_and(|last| last.is_empty())
{
raw_segments.pop();
}
if raw_segments.is_empty() {
if has_leading_sep {
let root = sep.to_string();
if UnicodeWidthStr::width(root.as_str()) <= max_width {
return root;
}
}
return "…".to_string();
}
struct Segment<'a> {
original: &'a str,
text: String,
truncatable: bool,
is_suffix: bool,
}
let assemble = |leading: bool, segments: &[Segment<'_>]| -> String {
let mut result = String::new();
if leading {
result.push(sep);
}
for segment in segments {
if !result.is_empty() && !result.ends_with(sep) {
result.push(sep);
}
result.push_str(segment.text.as_str());
}
result
};
let front_truncate = |original: &str, allowed_width: usize| -> String {
if allowed_width == 0 {
return String::new();
}
if UnicodeWidthStr::width(original) <= allowed_width {
return original.to_string();
}
if allowed_width == 1 {
return "…".to_string();
}
let mut kept: Vec<char> = Vec::new();
let mut used_width = 1; for ch in original.chars().rev() {
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
if used_width + ch_width > allowed_width {
break;
}
used_width += ch_width;
kept.push(ch);
}
kept.reverse();
let mut truncated = String::from("…");
for ch in kept {
truncated.push(ch);
}
truncated
};
let mut combos: Vec<(usize, usize)> = Vec::new();
let segment_count = raw_segments.len();
for left in 1..=segment_count {
let min_right = if left == segment_count { 0 } else { 1 };
for right in min_right..=(segment_count - left) {
combos.push((left, right));
}
}
let desired_suffix = if segment_count > 1 {
std::cmp::min(2, segment_count - 1)
} else {
0
};
let mut prioritized: Vec<(usize, usize)> = Vec::new();
let mut fallback: Vec<(usize, usize)> = Vec::new();
for combo in combos {
if combo.1 >= desired_suffix {
prioritized.push(combo);
} else {
fallback.push(combo);
}
}
let sort_combos = |items: &mut Vec<(usize, usize)>| {
items.sort_by(|(left_a, right_a), (left_b, right_b)| {
left_b
.cmp(left_a)
.then_with(|| right_b.cmp(right_a))
.then_with(|| (left_b + right_b).cmp(&(left_a + right_a)))
});
};
sort_combos(&mut prioritized);
sort_combos(&mut fallback);
let fit_segments =
|segments: &mut Vec<Segment<'_>>, allow_front_truncate: bool| -> Option<String> {
loop {
let candidate = assemble(has_leading_sep, segments);
let width = UnicodeWidthStr::width(candidate.as_str());
if width <= max_width {
return Some(candidate);
}
if !allow_front_truncate {
return None;
}
let mut indices: Vec<usize> = Vec::new();
for (idx, seg) in segments.iter().enumerate().rev() {
if seg.truncatable && seg.is_suffix {
indices.push(idx);
}
}
for (idx, seg) in segments.iter().enumerate().rev() {
if seg.truncatable && !seg.is_suffix {
indices.push(idx);
}
}
if indices.is_empty() {
return None;
}
let mut changed = false;
for idx in indices {
let original_width = UnicodeWidthStr::width(segments[idx].original);
if original_width <= max_width && segment_count > 2 {
continue;
}
let seg_width = UnicodeWidthStr::width(segments[idx].text.as_str());
let other_width = width.saturating_sub(seg_width);
let allowed_width = max_width.saturating_sub(other_width).max(1);
let new_text = front_truncate(segments[idx].original, allowed_width);
if new_text != segments[idx].text {
segments[idx].text = new_text;
changed = true;
break;
}
}
if !changed {
return None;
}
}
};
for (left_count, right_count) in prioritized.into_iter().chain(fallback) {
let mut segments: Vec<Segment<'_>> = raw_segments[..left_count]
.iter()
.map(|seg| Segment {
original: seg,
text: (*seg).to_string(),
truncatable: true,
is_suffix: false,
})
.collect();
let need_ellipsis = left_count + right_count < segment_count;
if need_ellipsis {
segments.push(Segment {
original: "…",
text: "…".to_string(),
truncatable: false,
is_suffix: false,
});
}
if right_count > 0 {
segments.extend(
raw_segments[segment_count - right_count..]
.iter()
.map(|seg| Segment {
original: seg,
text: (*seg).to_string(),
truncatable: true,
is_suffix: true,
}),
);
}
let allow_front_truncate = need_ellipsis || segment_count <= 2;
if let Some(candidate) = fit_segments(&mut segments, allow_front_truncate) {
return candidate;
}
}
front_truncate(path, max_width)
}
pub fn proper_join<T: AsRef<str>>(items: &[T]) -> String {
match items.len() {
0 => String::new(),
1 => items[0].as_ref().to_string(),
2 => format!("{} and {}", items[0].as_ref(), items[1].as_ref()),
_ => {
let last = items[items.len() - 1].as_ref();
let mut result = String::new();
for (i, item) in items.iter().take(items.len() - 1).enumerate() {
if i > 0 {
result.push_str(", ");
}
result.push_str(item.as_ref());
}
format!("{result} and {last}")
}
}
}