1use crate::compress::Compressor;
2
3const DEFAULT_THRESHOLD_BYTES: usize = 5 * 1024;
4const DEFAULT_KEEP_HEAD_BYTES: usize = 2 * 1024;
5const DEFAULT_KEEP_TAIL_BYTES: usize = 2 * 1024;
6
7pub fn strip_ansi(input: &str) -> String {
8 let bytes = input.as_bytes();
9 let mut output = String::with_capacity(input.len());
10 let mut index = 0;
11 let mut last_kept = 0;
12
13 while index < bytes.len() {
14 if bytes[index] != 0x1b {
15 index += 1;
16 continue;
17 }
18
19 let Some(next) = bytes.get(index + 1).copied() else {
20 break;
21 };
22
23 let end = if next == b'[' {
24 let mut cursor = index + 2;
25 while cursor < bytes.len() {
26 if (0x40..=0x7e).contains(&bytes[cursor]) {
27 cursor += 1;
28 break;
29 }
30 cursor += 1;
31 }
32 cursor
33 } else if (0x40..=0x5f).contains(&next) {
34 index + 2
35 } else {
36 index += 1;
37 continue;
38 };
39
40 output.push_str(&input[last_kept..index]);
41 index = end.min(bytes.len());
42 last_kept = index;
43 }
44
45 output.push_str(&input[last_kept..]);
46 output
47}
48
49pub fn dedup_consecutive(input: &str) -> String {
50 let had_trailing_newline = input.ends_with('\n');
51 let mut output = String::with_capacity(input.len());
52 let mut lines = input.lines();
53
54 let Some(mut current) = lines.next() else {
55 return String::new();
56 };
57 let mut count = 1usize;
58
59 for line in lines {
60 if line == current {
61 count += 1;
62 } else {
63 push_dedup_run(&mut output, current, count);
64 current = line;
65 count = 1;
66 }
67 }
68 push_dedup_run(&mut output, current, count);
69
70 if !had_trailing_newline {
71 output.pop();
72 }
73
74 output
75}
76
77fn push_dedup_run(output: &mut String, line: &str, count: usize) {
78 output.push_str(line);
79 output.push('\n');
80 if count >= 4 {
81 output.push_str("... (");
82 output.push_str(&(count - 1).to_string());
83 output.push_str(" more)\n");
84 } else {
85 for _ in 1..count {
86 output.push_str(line);
87 output.push('\n');
88 }
89 }
90}
91
92pub fn middle_truncate(
93 input: &str,
94 threshold_bytes: usize,
95 keep_head: usize,
96 keep_tail: usize,
97) -> String {
98 if input.len() <= threshold_bytes {
99 return input.to_string();
100 }
101
102 let head_end = floor_char_boundary(input, keep_head.min(input.len()));
103 let tail_start = ceil_char_boundary(input, input.len().saturating_sub(keep_tail));
104
105 if head_end >= tail_start {
106 return input.to_string();
107 }
108
109 let truncated_bytes = tail_start - head_end;
110 let mut output = String::with_capacity(head_end + keep_tail + 64);
111 output.push_str(&input[..head_end]);
112 if !output.ends_with('\n') {
113 output.push('\n');
114 }
115 output.push_str("...<truncated ");
116 output.push_str(&truncated_bytes.to_string());
117 output.push_str(" bytes>...\n");
118 output.push_str(&input[tail_start..]);
119 output
120}
121
122fn floor_char_boundary(input: &str, mut index: usize) -> usize {
123 while index > 0 && !input.is_char_boundary(index) {
124 index -= 1;
125 }
126 index
127}
128
129fn ceil_char_boundary(input: &str, mut index: usize) -> usize {
130 while index < input.len() && !input.is_char_boundary(index) {
131 index += 1;
132 }
133 index
134}
135
136pub struct GenericCompressor;
137
138impl GenericCompressor {
139 pub fn compress_output(output: &str) -> String {
140 let stripped = strip_ansi(output);
141 let deduped = dedup_consecutive(&stripped);
142 middle_truncate(
143 &deduped,
144 DEFAULT_THRESHOLD_BYTES,
145 DEFAULT_KEEP_HEAD_BYTES,
146 DEFAULT_KEEP_TAIL_BYTES,
147 )
148 }
149}
150
151impl Compressor for GenericCompressor {
152 fn matches(&self, _command: &str) -> bool {
153 true
154 }
155
156 fn compress(&self, _command: &str, output: &str) -> String {
157 Self::compress_output(output)
158 }
159}