nntp_proxy/tui/
log_capture.rs1use std::collections::VecDeque;
4use std::io::{self, Write};
5use std::mem;
6use std::sync::{Arc, Mutex};
7
8const MAX_LOG_LINES: usize = 1000;
10
11#[derive(Clone)]
13pub struct LogBuffer {
14 lines: Arc<Mutex<VecDeque<String>>>,
15}
16
17impl LogBuffer {
18 #[must_use]
20 pub fn new() -> Self {
21 Self {
22 lines: Arc::new(Mutex::new(VecDeque::with_capacity(MAX_LOG_LINES))),
23 }
24 }
25
26 pub fn push(&self, line: String) {
28 if let Ok(mut lines) = self.lines.lock() {
29 if lines.len() >= MAX_LOG_LINES {
30 lines.pop_front();
31 }
32 lines.push_back(line);
33 }
34 }
35
36 #[must_use]
38 pub fn recent_lines(&self, count: usize) -> Vec<String> {
39 self.lines.lock().map_or_else(
40 |_| Vec::new(),
41 |lines| lines.iter().rev().take(count).rev().cloned().collect(),
42 )
43 }
44
45 #[must_use]
47 pub fn all_lines(&self) -> Vec<String> {
48 self.lines
49 .lock()
50 .map_or_else(|_| Vec::new(), |lines| lines.iter().cloned().collect())
51 }
52
53 pub fn with_recent_lines<F, R>(&self, count: usize, f: F) -> Option<R>
58 where
59 F: FnOnce(&VecDeque<String>, usize) -> R,
60 {
61 self.lines.lock().ok().map(|lines| {
62 let skip = lines.len().saturating_sub(count);
63 f(&lines, skip)
64 })
65 }
66}
67
68impl Default for LogBuffer {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74pub struct LogWriter {
76 buffer: LogBuffer,
77 line_buffer: String,
78}
79
80impl LogWriter {
81 #[must_use]
83 pub fn new(buffer: LogBuffer) -> Self {
84 Self {
85 buffer,
86 line_buffer: String::with_capacity(256),
87 }
88 }
89}
90
91impl Write for LogWriter {
92 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
93 let s =
94 std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
95
96 for c in s.chars() {
97 if c == '\n' {
98 if !self.line_buffer.is_empty() {
100 self.buffer.push(mem::take(&mut self.line_buffer));
102 }
103 } else {
104 self.line_buffer.push(c);
105 }
106 }
107
108 Ok(buf.len())
109 }
110
111 fn flush(&mut self) -> io::Result<()> {
112 if !self.line_buffer.is_empty() {
114 self.buffer.push(mem::take(&mut self.line_buffer));
116 }
117 Ok(())
118 }
119}
120
121pub struct LogMakeWriter {
123 buffer: LogBuffer,
124}
125
126impl LogMakeWriter {
127 #[must_use]
129 pub const fn new(buffer: LogBuffer) -> Self {
130 Self { buffer }
131 }
132}
133
134impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for LogMakeWriter {
135 type Writer = LogWriter;
136
137 fn make_writer(&'a self) -> Self::Writer {
138 LogWriter::new(self.buffer.clone())
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use std::io::Write;
146
147 #[test]
148 fn test_log_buffer_push_and_retrieve() {
149 let buffer = LogBuffer::new();
150 buffer.push("Line 1".to_string());
151 buffer.push("Line 2".to_string());
152 buffer.push("Line 3".to_string());
153
154 let lines = buffer.all_lines();
155 assert_eq!(lines.len(), 3);
156 assert_eq!(lines[0], "Line 1");
157 assert_eq!(lines[2], "Line 3");
158 }
159
160 #[test]
161 fn test_log_buffer_capacity_limit() {
162 let buffer = LogBuffer::new();
163
164 for i in 0..1500 {
166 buffer.push(format!("Line {i}"));
167 }
168
169 let lines = buffer.all_lines();
170 assert_eq!(lines.len(), MAX_LOG_LINES);
171 assert_eq!(lines[0], "Line 500");
173 }
174
175 #[test]
176 fn test_log_buffer_recent_lines() {
177 let buffer = LogBuffer::new();
178 for i in 0..10 {
179 buffer.push(format!("Line {i}"));
180 }
181
182 let recent = buffer.recent_lines(3);
183 assert_eq!(recent.len(), 3);
184 assert_eq!(recent[0], "Line 7");
185 assert_eq!(recent[1], "Line 8");
186 assert_eq!(recent[2], "Line 9");
187 }
188
189 #[test]
190 fn test_log_writer_splits_lines() {
191 let buffer = LogBuffer::new();
192 let mut writer = LogWriter::new(buffer.clone());
193
194 writer.write_all(b"Line 1\nLine 2\nLine 3\n").unwrap();
195 writer.flush().unwrap();
196
197 let lines = buffer.all_lines();
198 assert_eq!(lines.len(), 3);
199 assert_eq!(lines[0], "Line 1");
200 assert_eq!(lines[1], "Line 2");
201 assert_eq!(lines[2], "Line 3");
202 }
203
204 #[test]
205 fn test_log_writer_partial_lines() {
206 let buffer = LogBuffer::new();
207 let mut writer = LogWriter::new(buffer.clone());
208
209 writer.write_all(b"Partial ").unwrap();
210 writer.write_all(b"line\n").unwrap();
211 writer.flush().unwrap();
212
213 let lines = buffer.all_lines();
214 assert_eq!(lines.len(), 1);
215 assert_eq!(lines[0], "Partial line");
216 }
217
218 #[test]
219 fn test_log_writer_flush_incomplete() {
220 let buffer = LogBuffer::new();
221 let mut writer = LogWriter::new(buffer.clone());
222
223 writer.write_all(b"No newline").unwrap();
224 writer.flush().unwrap();
225
226 let lines = buffer.all_lines();
227 assert_eq!(lines.len(), 1);
228 assert_eq!(lines[0], "No newline");
229 }
230}