veloren_server_cli/
tuilog.rs

1use ratatui::{
2    prelude::Line,
3    style::{Color, Modifier, Style},
4    text::{Span, Text},
5};
6use std::{
7    io::{self, Write},
8    sync::{Arc, Mutex},
9};
10use tracing::warn;
11
12#[derive(Debug, Default, Clone)]
13pub struct TuiLog<'a> {
14    pub inner: Arc<Mutex<Text<'a>>>,
15}
16
17impl TuiLog<'_> {
18    pub fn resize(&self, h: usize) {
19        let mut inner = self.inner.lock().unwrap();
20
21        let len = inner.lines.len().saturating_sub(h);
22        inner.lines.drain(..len);
23    }
24}
25
26impl Write for TuiLog<'_> {
27    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
28        // TODO: this processing can probably occur in the consumer of the log lines
29        // (and instead of having a TuiLog::resize the consumer can take
30        // ownership of the lines and manage them itself).
31
32        // Not super confident this is the ideal parser but it works for now and doesn't
33        // depend on an old version of nom. Alternatives to consider may include
34        // `vte`, `anstyle-parse`, `vt100`, or others.
35        use cansi::v3::categorise_text;
36
37        let line =
38            core::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
39
40        let mut spans = Vec::new();
41        let mut lines = Vec::new();
42
43        for out in categorise_text(line) {
44            let mut style = Style::default();
45            // NOTE: There are other values returned from cansi that we don't bother to use
46            // for now including background color, italics, blinking, etc.
47            style.fg = match out.fg {
48                Some(cansi::Color::Black) => Some(Color::Black),
49                Some(cansi::Color::Red) => Some(Color::Red),
50                Some(cansi::Color::Green) => Some(Color::Green),
51                Some(cansi::Color::Yellow) => Some(Color::Yellow),
52                Some(cansi::Color::Blue) => Some(Color::Blue),
53                Some(cansi::Color::Magenta) => Some(Color::Magenta),
54                Some(cansi::Color::Cyan) => Some(Color::Cyan),
55                Some(cansi::Color::White) => Some(Color::White),
56                // "Bright" versions currently not handled
57                Some(c) => {
58                    warn!("Unknown color {:#?}", c);
59                    style.fg
60                },
61                None => style.fg,
62            };
63            match out.intensity {
64                Some(cansi::Intensity::Normal) | None => {},
65                Some(cansi::Intensity::Bold) => style.add_modifier = Modifier::BOLD,
66                Some(cansi::Intensity::Faint) => style.add_modifier = Modifier::DIM,
67            }
68
69            // search for newlines
70            for t in out.text.split_inclusive('\n') {
71                if !t.is_empty() {
72                    spans.push(Span::styled(t.to_owned(), style));
73                }
74                if t.ends_with('\n') {
75                    lines.push(Line::from(core::mem::take(&mut spans)));
76                }
77            }
78        }
79        if !spans.is_empty() {
80            lines.push(Line::from(spans));
81        }
82
83        self.inner.lock().unwrap().lines.append(&mut lines);
84
85        Ok(buf.len())
86    }
87
88    // We can potentially use this to reduce locking frequency?
89    fn flush(&mut self) -> io::Result<()> { Ok(()) }
90}