oxi_agent/tools/
render_utils.rs1pub fn shorten_path(path: &str) -> String {
6 if let Some(home) = dirs::home_dir() {
7 let home_str = home.to_string_lossy();
8 if let Some(rest) = path.strip_prefix(home_str.as_ref()) {
9 return format!("~{}", rest);
10 }
11 }
12 path.to_string()
13}
14
15pub fn replace_tabs(text: &str) -> &str {
17 if !text.contains('\t') {
19 return text;
20 }
21 text
23}
24
25pub fn replace_tabs_owned(text: &str) -> String {
27 text.replace('\t', " ")
28}
29
30pub fn normalize_display_text(text: &str) -> String {
32 text.replace('\r', "")
33}
34
35pub fn sanitize_binary_output(text: &str) -> String {
38 let mut result = String::with_capacity(text.len());
39 for ch in text.chars() {
40 if ch == '\n' || ch == '\t' || ch.is_ascii_graphic() || ch == ' ' {
41 result.push(ch);
42 } else if ch == '\r' {
43 } else {
45 result.push('�');
46 }
47 }
48 result
49}
50
51pub fn get_text_output(content: &str) -> String {
53 let sanitized = sanitize_binary_output(content);
54 normalize_display_text(&sanitized)
55}
56
57pub fn truncate_output_preview(text: &str, max_lines: usize, max_bytes: usize) -> String {
59 let lines: Vec<&str> = text.lines().collect();
60
61 if lines.len() <= max_lines && text.len() <= max_bytes {
62 return text.to_string();
63 }
64
65 let selected: Vec<&str> = lines.into_iter().take(max_lines).collect();
66 let mut result = selected.join("\n");
67
68 if result.len() > max_bytes {
69 result.truncate(max_bytes);
70 while !result.is_char_boundary(result.len()) {
72 result.pop();
73 }
74 }
75
76 result
77}
78
79pub fn invalid_arg_text() -> &'static str {
81 "[invalid arg]"
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn test_shorten_path_with_home() {
90 let home = dirs::home_dir().unwrap();
91 let home_str = home.to_string_lossy().to_string();
92 let path = format!("{}/foo/bar.txt", home_str);
93 assert_eq!(shorten_path(&path), "~/foo/bar.txt");
94 }
95
96 #[test]
97 fn test_shorten_path_no_home() {
98 assert_eq!(shorten_path("/tmp/foo.txt"), "/tmp/foo.txt");
99 }
100
101 #[test]
102 fn test_replace_tabs_no_tabs() {
103 assert_eq!(replace_tabs("hello world"), "hello world");
104 }
105
106 #[test]
107 fn test_replace_tabs_owned_with_tabs() {
108 assert_eq!(replace_tabs_owned("hello\tworld"), "hello world");
109 }
110
111 #[test]
112 fn test_normalize_display_text() {
113 assert_eq!(normalize_display_text("hello\r\nworld"), "hello\nworld");
114 assert_eq!(normalize_display_text("no-cr"), "no-cr");
115 }
116
117 #[test]
118 fn test_sanitize_binary_output() {
119 assert_eq!(sanitize_binary_output("hello\nworld"), "hello\nworld");
120 assert_eq!(sanitize_binary_output("tab\there"), "tab\there");
121
122 let with_control = "hello\x01world";
124 let result = sanitize_binary_output(with_control);
125 assert!(result.contains('�'));
126 }
127
128 #[test]
129 fn test_get_text_output() {
130 assert_eq!(get_text_output("hello\r\nworld"), "hello\nworld");
131 }
132
133 #[test]
134 fn test_truncate_output_preview_within_limits() {
135 let text = "line1\nline2\nline3";
136 assert_eq!(truncate_output_preview(text, 10, 1000), text);
137 }
138
139 #[test]
140 fn test_truncate_output_preview_by_lines() {
141 let text = "line1\nline2\nline3\nline4\nline5";
142 let result = truncate_output_preview(text, 2, 1000);
143 assert!(result.contains("line1"));
144 assert!(result.contains("line2"));
145 assert!(!result.contains("line5"));
146 }
147
148 #[test]
149 fn test_invalid_arg_text() {
150 assert_eq!(invalid_arg_text(), "[invalid arg]");
151 }
152}