Skip to main content

farhand_core/
text.rs

1//! Text shaping for tool results: bounded output and numbered line windows.
2
3/// Keep the head and tail of `bytes` within `max`, marking the cut. Output
4/// is lossy UTF-8; the model sees text, never raw bytes.
5pub fn bounded(bytes: &[u8], max: usize) -> (String, bool) {
6    if bytes.len() <= max {
7        return (String::from_utf8_lossy(bytes).into_owned(), false);
8    }
9    let head_len = max * 2 / 3;
10    let tail_len = max - head_len;
11    let head = String::from_utf8_lossy(&bytes[..head_len]);
12    let tail = String::from_utf8_lossy(&bytes[bytes.len() - tail_len..]);
13    let omitted = bytes.len() - head_len - tail_len;
14    (
15        format!(
16            "{head}\n\n[... {omitted} bytes omitted; output limit is {max} bytes ...]\n\n{tail}"
17        ),
18        true,
19    )
20}
21
22/// A window of numbered lines, `cat -n` style, starting at 1-based `offset`
23/// and at most `limit` lines long. Returns the text and whether more lines
24/// follow the window.
25pub fn numbered_window(text: &str, offset: usize, limit: usize) -> (String, usize, bool) {
26    let start = offset.max(1) - 1;
27    let total = text.lines().count();
28    let mut out = String::new();
29    let mut shown = 0;
30    for (i, line) in text.lines().enumerate().skip(start).take(limit) {
31        out.push_str(&format!("{:>6}\t{}\n", i + 1, line));
32        shown += 1;
33    }
34    let more = start + shown < total;
35    (out, total, more)
36}
37
38/// Escape a string as a single-quoted POSIX shell word.
39pub fn shell_quote(s: &str) -> String {
40    if !s.is_empty()
41        && s.bytes()
42            .all(|b| b.is_ascii_alphanumeric() || b"-_./=:@%+,".contains(&b))
43    {
44        return s.to_string();
45    }
46    format!("'{}'", s.replace('\'', "'\\''"))
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn bounded_keeps_head_and_tail() {
55        let data: Vec<u8> = (0..1000).map(|i| b'a' + (i % 26) as u8).collect();
56        let (s, cut) = bounded(&data, 300);
57        assert!(cut);
58        assert!(s.starts_with("abcdefghij"));
59        assert!(s.contains("omitted"));
60        assert!(s.ends_with(std::str::from_utf8(&data[900..]).unwrap()));
61        let (s, cut) = bounded(b"short", 300);
62        assert!(!cut);
63        assert_eq!(s, "short");
64    }
65
66    #[test]
67    fn window_numbers_from_offset() {
68        let (s, total, more) = numbered_window("a\nb\nc\nd", 2, 2);
69        assert_eq!(s, "     2\tb\n     3\tc\n");
70        assert_eq!(total, 4);
71        assert!(more);
72        let (_, _, more) = numbered_window("a\nb", 1, 10);
73        assert!(!more);
74    }
75
76    #[test]
77    fn quoting() {
78        assert_eq!(shell_quote("abc/def.txt"), "abc/def.txt");
79        assert_eq!(shell_quote("it's here"), "'it'\\''s here'");
80        assert_eq!(shell_quote(""), "''");
81    }
82}