1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
use ratatui::{
    prelude::Line,
    style::{Color, Modifier, Style},
    text::{Span, Text},
};
use std::{
    io::{self, Write},
    sync::{Arc, Mutex},
};
use tracing::warn;

#[derive(Debug, Default, Clone)]
pub struct TuiLog<'a> {
    pub inner: Arc<Mutex<Text<'a>>>,
}

impl<'a> TuiLog<'a> {
    pub fn resize(&self, h: usize) {
        let mut inner = self.inner.lock().unwrap();

        let len = inner.lines.len().saturating_sub(h);
        inner.lines.drain(..len);
    }
}

impl<'a> Write for TuiLog<'a> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        // TODO: this processing can probably occur in the consumer of the log lines
        // (and instead of having a TuiLog::resize the consumer can take
        // ownership of the lines and manage them itself).

        // Not super confident this is the ideal parser but it works for now and doesn't
        // depend on an old version of nom. Alternatives to consider may include
        // `vte`, `anstyle-parse`, `vt100`, or others.
        use cansi::v3::categorise_text;

        let line =
            core::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

        let mut spans = Vec::new();
        let mut lines = Vec::new();

        for out in categorise_text(line) {
            let mut style = Style::default();
            // NOTE: There are other values returned from cansi that we don't bother to use
            // for now including background color, italics, blinking, etc.
            style.fg = match out.fg {
                Some(cansi::Color::Black) => Some(Color::Black),
                Some(cansi::Color::Red) => Some(Color::Red),
                Some(cansi::Color::Green) => Some(Color::Green),
                Some(cansi::Color::Yellow) => Some(Color::Yellow),
                Some(cansi::Color::Blue) => Some(Color::Blue),
                Some(cansi::Color::Magenta) => Some(Color::Magenta),
                Some(cansi::Color::Cyan) => Some(Color::Cyan),
                Some(cansi::Color::White) => Some(Color::White),
                // "Bright" versions currently not handled
                Some(c) => {
                    warn!("Unknown color {:#?}", c);
                    style.fg
                },
                None => style.fg,
            };
            match out.intensity {
                Some(cansi::Intensity::Normal) | None => {},
                Some(cansi::Intensity::Bold) => style.add_modifier = Modifier::BOLD,
                Some(cansi::Intensity::Faint) => style.add_modifier = Modifier::DIM,
            }

            // search for newlines
            for t in out.text.split_inclusive('\n') {
                if !t.is_empty() {
                    spans.push(Span::styled(t.to_owned(), style));
                }
                if t.ends_with('\n') {
                    lines.push(Line::from(core::mem::take(&mut spans)));
                }
            }
        }
        if !spans.is_empty() {
            lines.push(Line::from(spans));
        }

        self.inner.lock().unwrap().lines.append(&mut lines);

        Ok(buf.len())
    }

    // We can potentially use this to reduce locking frequency?
    fn flush(&mut self) -> io::Result<()> { Ok(()) }
}