use crate::classify::DISPLAY_CAP;
fn floor_char_boundary(s: &str, idx: usize) -> usize {
if idx >= s.len() {
return s.len();
}
let mut i = idx;
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
i
}
fn ceil_char_boundary(s: &str, idx: usize) -> usize {
if idx >= s.len() {
return s.len();
}
let mut i = idx;
while i < s.len() && !s.is_char_boundary(i) {
i += 1;
}
i
}
pub fn bounded_truncate(output: &str) -> String {
bounded_truncate_indexed(output, true)
}
pub fn bounded_truncate_indexed(output: &str, indexed: bool) -> String {
if output.len() <= DISPLAY_CAP {
return output.to_string();
}
let head_budget = (DISPLAY_CAP as f64 * 0.6) as usize; let tail_budget = DISPLAY_CAP - head_budget;
let (head_end, tail_start) = cut_boundaries(output, head_budget, tail_budget);
let clamped_head = floor_char_boundary(output, head_budget).min(head_end);
let clamped_tail =
ceil_char_boundary(output, output.len().saturating_sub(tail_budget)).max(tail_start);
debug_assert!(
clamped_head + (output.len() - clamped_tail) <= DISPLAY_CAP,
"head+tail slices ({clamped_head} + {} bytes) must not exceed DISPLAY_CAP",
output.len() - clamped_tail
);
let truncated_bytes = output.len() - clamped_head - (output.len() - clamped_tail);
let marker = if indexed {
format!("... [{truncated_bytes} bytes truncated → use `oo recall` to query] ...\n")
} else {
format!("... [{truncated_bytes} bytes truncated] ...\n")
};
let mut result = String::with_capacity(DISPLAY_CAP + marker.len());
result.push_str(&output[..clamped_head]);
result.push_str(&marker);
result.push_str(&output[clamped_tail..]);
result
}
pub(crate) fn cut_boundaries(
output: &str,
head_budget: usize,
tail_budget: usize,
) -> (usize, usize) {
let raw_tail = output.len().saturating_sub(tail_budget);
let mut newline_count = 0usize;
let mut first_nl_at_or_after_head: Option<usize> = None;
let mut last_nl_before_raw_tail: Option<usize> = None;
for (i, b) in output.as_bytes().iter().enumerate() {
if *b != b'\n' {
continue;
}
newline_count += 1;
if i >= raw_tail && first_nl_at_or_after_head.is_some() {
break;
}
if i >= head_budget && first_nl_at_or_after_head.is_none() {
first_nl_at_or_after_head = Some(i);
}
if i < raw_tail {
last_nl_before_raw_tail = Some(i);
}
}
if newline_count < 2 {
let head = floor_char_boundary(output, head_budget);
let tail = ceil_char_boundary(output, raw_tail);
return (head, tail);
}
let head_end = match first_nl_at_or_after_head {
Some(pos) => pos + 1, None => floor_char_boundary(output, head_budget),
};
let tail_start = match last_nl_before_raw_tail {
Some(pos) => pos + 1, None => ceil_char_boundary(output, raw_tail),
};
if head_end >= tail_start {
return (head_end, output.len());
}
(head_end, tail_start)
}