• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jzombie / term-wm / 20761962976

06 Jan 2026 09:01PM UTC coverage: 33.582%. First build
20761962976

push

github

web-flow
Enable Coveralls (#2)

845 of 854 new or added lines in 23 files covered. (98.95%)

2072 of 6170 relevant lines covered (33.58%)

0.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.81
/src/components/debug_log.rs
1
use std::collections::VecDeque;
2
use std::io::{self, Write};
3
use std::sync::{Arc, Mutex, OnceLock};
4

5
use crossterm::event::{Event, KeyCode, MouseEventKind};
6
use ratatui::style::{Color, Style};
7
use ratatui::text::{Line, Text};
8
use ratatui::widgets::Paragraph;
9
use ratatui::{Frame, layout::Rect};
10

11
use crate::components::Component;
12

13
const DEFAULT_MAX_LINES: usize = 2000;
14
static GLOBAL_LOG: OnceLock<DebugLogHandle> = OnceLock::new();
15
static PANIC_HOOK_INSTALLED: OnceLock<()> = OnceLock::new();
16

17
pub fn set_global_debug_log(handle: DebugLogHandle) -> bool {
×
18
    GLOBAL_LOG.set(handle).is_ok()
×
19
}
×
20

21
pub fn global_debug_log() -> Option<DebugLogHandle> {
×
22
    GLOBAL_LOG.get().cloned()
×
23
}
×
24

25
pub fn log_line(line: impl Into<String>) {
×
26
    if let Some(handle) = GLOBAL_LOG.get() {
×
27
        handle.push(line);
×
28
    }
×
29
}
×
30

31
pub fn install_panic_hook() {
×
32
    if PANIC_HOOK_INSTALLED.get().is_some() {
×
33
        return;
×
34
    }
×
35
    let _ = PANIC_HOOK_INSTALLED.set(());
×
36
    let prev = std::panic::take_hook();
×
37
    std::panic::set_hook(Box::new(move |info| {
×
38
        if let Some(handle) = GLOBAL_LOG.get() {
×
39
            handle.push("".to_string());
×
40
            handle.push("=== PANIC ===".to_string());
×
41
            if let Some(location) = info.location() {
×
42
                handle.push(format!(
×
43
                    "{}:{}:{}",
×
44
                    location.file(),
×
45
                    location.line(),
×
46
                    location.column()
×
47
                ));
×
48
            }
×
49
            if let Some(msg) = info.payload().downcast_ref::<&str>() {
×
50
                handle.push(format!("message: {msg}"));
×
51
            } else if let Some(msg) = info.payload().downcast_ref::<String>() {
×
52
                handle.push(format!("message: {msg}"));
×
53
            } else {
×
54
                handle.push("message: <non-string panic>".to_string());
×
55
            }
×
56
            let backtrace = std::backtrace::Backtrace::force_capture();
×
57
            for line in backtrace.to_string().lines() {
×
58
                handle.push(line.to_string());
×
59
            }
×
60
            handle.push("============".to_string());
×
61
        }
×
62
        prev(info);
×
63
    }));
×
64
}
×
65

66
#[derive(Debug)]
67
struct DebugLogBuffer {
68
    lines: VecDeque<String>,
69
    max_lines: usize,
70
}
71

72
impl DebugLogBuffer {
73
    fn new(max_lines: usize) -> Self {
3✔
74
        Self {
3✔
75
            lines: VecDeque::new(),
3✔
76
            max_lines: max_lines.max(1),
3✔
77
        }
3✔
78
    }
3✔
79

80
    fn push_line(&mut self, line: String) {
7✔
81
        self.lines.push_back(line);
7✔
82
        while self.lines.len() > self.max_lines {
8✔
83
            self.lines.pop_front();
1✔
84
        }
1✔
85
    }
7✔
86
}
87

88
#[derive(Clone, Debug)]
89
pub struct DebugLogHandle {
90
    inner: Arc<Mutex<DebugLogBuffer>>,
91
}
92

93
impl DebugLogHandle {
94
    pub fn push(&self, line: impl Into<String>) {
7✔
95
        if let Ok(mut buffer) = self.inner.lock() {
7✔
96
            buffer.push_line(line.into());
7✔
97
        }
7✔
98
    }
7✔
99

100
    pub fn writer(&self) -> DebugLogWriter {
1✔
101
        DebugLogWriter::new(self.clone())
1✔
102
    }
1✔
103
}
104

105
#[derive(Debug)]
106
pub struct DebugLogWriter {
107
    handle: DebugLogHandle,
108
    pending: Vec<u8>,
109
}
110

111
impl DebugLogWriter {
112
    pub fn new(handle: DebugLogHandle) -> Self {
1✔
113
        Self {
1✔
114
            handle,
1✔
115
            pending: Vec::new(),
1✔
116
        }
1✔
117
    }
1✔
118

119
    fn flush_pending(&mut self, force: bool) {
2✔
120
        if self.pending.is_empty() {
2✔
121
            return;
×
122
        }
2✔
123
        if force {
2✔
124
            let text = String::from_utf8_lossy(&self.pending).to_string();
1✔
125
            self.pending.clear();
1✔
126
            for line in text.split('\n') {
1✔
127
                if !line.is_empty() || force {
1✔
128
                    self.handle.push(line.to_string());
1✔
129
                }
1✔
130
            }
131
            return;
1✔
132
        }
1✔
133
        let Some(pos) = self.pending.iter().rposition(|b| *b == b'\n') else {
8✔
134
            return;
×
135
        };
136
        let drained: Vec<u8> = self.pending.drain(..=pos).collect();
1✔
137
        let text = String::from_utf8_lossy(&drained).to_string();
1✔
138
        for line in text.split('\n') {
3✔
139
            if !line.is_empty() {
3✔
140
                self.handle.push(line.to_string());
2✔
141
            }
2✔
142
        }
143
    }
2✔
144
}
145

146
impl Write for DebugLogWriter {
147
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1✔
148
        self.pending.extend_from_slice(buf);
1✔
149
        self.flush_pending(false);
1✔
150
        Ok(buf.len())
1✔
151
    }
1✔
152

153
    fn flush(&mut self) -> io::Result<()> {
1✔
154
        self.flush_pending(true);
1✔
155
        Ok(())
1✔
156
    }
1✔
157
}
158

159
#[derive(Debug)]
160
pub struct DebugLogComponent {
161
    handle: DebugLogHandle,
162
    scroll_from_bottom: usize,
163
}
164

165
impl DebugLogComponent {
166
    pub fn new(max_lines: usize) -> (Self, DebugLogHandle) {
3✔
167
        let handle = DebugLogHandle {
3✔
168
            inner: Arc::new(Mutex::new(DebugLogBuffer::new(max_lines))),
3✔
169
        };
3✔
170
        (
3✔
171
            Self {
3✔
172
                handle: handle.clone(),
3✔
173
                scroll_from_bottom: 0,
3✔
174
            },
3✔
175
            handle,
3✔
176
        )
3✔
177
    }
3✔
178

179
    pub fn new_default() -> (Self, DebugLogHandle) {
×
180
        Self::new(DEFAULT_MAX_LINES)
×
181
    }
×
182

183
    fn clamp_scroll(&mut self, line_count: usize, area: Rect) -> usize {
×
184
        let max_scroll = line_count.saturating_sub(area.height as usize);
×
185
        if self.scroll_from_bottom > max_scroll {
×
186
            self.scroll_from_bottom = max_scroll;
×
187
        }
×
188
        max_scroll
×
189
    }
×
190
}
191

192
impl Component for DebugLogComponent {
193
    fn render(&mut self, frame: &mut Frame, area: Rect, focused: bool) {
×
194
        if area.width == 0 || area.height == 0 {
×
195
            return;
×
196
        }
×
197
        let buffer = frame.buffer_mut();
×
198
        let bounds = area.intersection(buffer.area);
×
199
        if bounds.width == 0 || bounds.height == 0 {
×
200
            return;
×
201
        }
×
202
        for y in bounds.y..bounds.y.saturating_add(bounds.height) {
×
203
            for x in bounds.x..bounds.x.saturating_add(bounds.width) {
×
204
                if let Some(cell) = buffer.cell_mut((x, y)) {
×
205
                    cell.reset();
×
206
                    cell.set_symbol(" ");
×
207
                }
×
208
            }
209
        }
210
        let lines = if let Ok(buffer) = self.handle.inner.lock() {
×
211
            buffer.lines.iter().cloned().collect::<Vec<_>>()
×
212
        } else {
213
            Vec::new()
×
214
        };
215
        let max_scroll = self.clamp_scroll(lines.len(), area);
×
216
        let scroll_top = max_scroll.saturating_sub(self.scroll_from_bottom);
×
217
        let text = Text::from(lines.into_iter().map(Line::from).collect::<Vec<_>>());
×
218
        let mut paragraph = Paragraph::new(text).scroll((scroll_top as u16, 0));
×
219
        if focused {
×
220
            paragraph = paragraph.style(Style::default().fg(Color::Yellow));
×
221
        }
×
222
        frame.render_widget(paragraph, area);
×
223
    }
×
224

225
    fn handle_event(&mut self, event: &Event) -> bool {
2✔
226
        match event {
2✔
227
            Event::Key(key) => match key.code {
1✔
228
                KeyCode::PageUp => {
229
                    self.scroll_from_bottom = self.scroll_from_bottom.saturating_add(5);
1✔
230
                    true
1✔
231
                }
232
                KeyCode::PageDown => {
233
                    self.scroll_from_bottom = self.scroll_from_bottom.saturating_sub(5);
×
234
                    true
×
235
                }
236
                KeyCode::Home => {
237
                    self.scroll_from_bottom = usize::MAX;
×
238
                    true
×
239
                }
240
                KeyCode::End => {
241
                    self.scroll_from_bottom = 0;
×
242
                    true
×
243
                }
244
                _ => false,
×
245
            },
246
            Event::Mouse(mouse) => match mouse.kind {
1✔
247
                MouseEventKind::ScrollUp => {
248
                    self.scroll_from_bottom = self.scroll_from_bottom.saturating_add(2);
×
249
                    true
×
250
                }
251
                MouseEventKind::ScrollDown => {
252
                    self.scroll_from_bottom = self.scroll_from_bottom.saturating_sub(2);
1✔
253
                    true
1✔
254
                }
255
                _ => false,
×
256
            },
257
            _ => false,
×
258
        }
259
    }
2✔
260
}
261

262
#[cfg(test)]
263
mod tests {
264
    use super::*;
265
    use crossterm::event::{Event, KeyCode, MouseEvent, MouseEventKind};
266
    use std::io::Write;
267

268
    #[test]
269
    fn debug_log_handle_and_buffer_limits() {
1✔
270
        let (_comp, handle) = DebugLogComponent::new(3);
1✔
271
        handle.push("one");
1✔
272
        handle.push("two");
1✔
273
        handle.push("three");
1✔
274
        handle.push("four");
1✔
275
        // internal buffer should be capped at 3
276
        if let Ok(buf) = handle.inner.lock() {
1✔
277
            assert_eq!(buf.lines.len(), 3);
1✔
278
            assert_eq!(buf.lines.front().unwrap().as_str(), "two");
1✔
279
        } else {
NEW
280
            panic!("lock failed");
×
281
        }
282
    }
1✔
283

284
    #[test]
285
    fn debug_log_writer_flushes_lines() {
1✔
286
        let (_comp, handle) = DebugLogComponent::new(10);
1✔
287
        let mut writer = handle.writer();
1✔
288
        let _ = writer.write(b"first line\nsecond line\npartial");
1✔
289
        // flush should push pending partial when forced
290
        writer.flush().unwrap();
1✔
291
        if let Ok(buf) = handle.inner.lock() {
1✔
292
            assert!(buf.lines.iter().any(|s| s == "first line"));
1✔
293
            assert!(buf.lines.iter().any(|s| s == "second line"));
2✔
294
            assert!(buf.lines.iter().any(|s| s == "partial"));
3✔
NEW
295
        }
×
296
    }
1✔
297

298
    #[test]
299
    fn debug_log_component_handle_event_scrolls() {
1✔
300
        let (mut comp, _handle) = DebugLogComponent::new(10);
1✔
301
        assert_eq!(comp.scroll_from_bottom, 0);
1✔
302
        comp.handle_event(&Event::Key(crossterm::event::KeyEvent::new(
1✔
303
            KeyCode::PageUp,
1✔
304
            crossterm::event::KeyModifiers::NONE,
1✔
305
        )));
1✔
306
        assert!(comp.scroll_from_bottom >= 5);
1✔
307
        let before = comp.scroll_from_bottom;
1✔
308
        comp.handle_event(&Event::Mouse(MouseEvent {
1✔
309
            kind: MouseEventKind::ScrollDown,
1✔
310
            column: 0,
1✔
311
            row: 0,
1✔
312
            modifiers: crossterm::event::KeyModifiers::NONE,
1✔
313
        }));
1✔
314
        // scroll_from_bottom should have decreased or stayed at zero
315
        assert!(comp.scroll_from_bottom <= before);
1✔
316
    }
1✔
317
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc