cursive_extras/views/
log_view.rs

1use std::{
2    fs,
3    path::PathBuf,
4    sync::LazyLock
5};
6use cursive_core::{
7    View,
8    Printer,
9    Vec2,
10    utils::{
11        markup::StyledString,
12        lines::spans::{LinesIterator, Row}
13    },
14    views::ScrollView,
15    view::{ScrollStrategy, Scrollable},
16    theme::{
17        BaseColor,
18        Color,
19        Style,
20        ColorStyle
21    }
22};
23use rust_utils::logging::Log;
24use regex::Regex;
25use unicode_width::UnicodeWidthStr;
26use crate::SpannedStrExt;
27
28static INFO_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[.*INFO\]").unwrap());
29static DBG_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[.*DEBUG\]").unwrap());
30static WARN_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[.*WARN\]").unwrap());
31static ERROR_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[.*ERROR\]").unwrap());
32static FATAL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[.*FATAL\]").unwrap());
33
34/// A special view that follows a specified log file
35/// similar to how `tail -f <log file name>` tracks a log file
36///
37/// Auto-refresh in cursive must be set by calling `Cursive::set_fps()`
38/// or `Cursive::set_autorefresh()` before using this view or it won't work
39#[derive(Clone)]
40pub struct LogView {
41    path: PathBuf,
42    content: LogContent
43}
44
45impl LogView {
46    /// Create a new `LogView` with the specified log file path
47    pub fn new<P: Into<PathBuf>>(path: P) -> LogView {
48        let path = path.into();
49        let raw_log = fs::read_to_string(&path).unwrap_or_default();
50        let content = LogContent::new(raw_log);
51
52        LogView {
53            path,
54            content
55        }
56    }
57
58    /// Wrap the `LogView` in a `ScrollView` that jumps to the bottom when the log updates
59    pub fn scroll_to_bottom(self) -> ScrollView<Self> {
60        self.scrollable().scroll_strategy(ScrollStrategy::StickToBottom)
61    }
62}
63
64impl View for LogView {
65    fn draw(&self, printer: &Printer) {
66        for y in 0..printer.size.y {
67            printer.print_hline((0, y), printer.size.x, " ");
68        }
69        self.content.draw(printer);
70    }
71
72    fn required_size(&mut self, bound: Vec2) -> Vec2 {
73        self.content.fit_to_width(bound.x);
74        (bound.x, self.content.num_lines()).into()
75    }
76
77    fn layout(&mut self, size: Vec2) {
78        let raw_log = fs::read_to_string(&self.path).unwrap_or_default();
79        self.content.set_content(raw_log);
80        self.content.fit_to_width(size.x);
81    }
82}
83
84impl From<&Log> for LogView {
85    /// Create a `LogView` from a `rust-utils` `Log` object
86    ///
87    /// This will follow the main log if it is enabled or the latest dated
88    /// log file otherwise
89    fn from(log: &Log) -> Self {
90        let path = if let Some(main_log_path) = log.main_log_path() {
91            main_log_path
92        }
93        else {
94            log.log_path()
95        };
96
97        Self::new(path)
98    }
99}
100
101#[derive(Clone)]
102struct LogContent {
103    content: StyledString,
104    rows: Vec<Row>
105}
106
107impl LogContent {
108    fn new(content: String) -> Self {
109        let content = colorize_log(&content);
110        LogContent {
111            content,
112            rows: Vec::new()
113        }
114    }
115
116    // sets the content
117    // since this might be expensive, the content
118    // is only set when the new content is different from the current content
119    fn set_content(&mut self, new_content: String) {
120        if new_content.as_str() != self.content.source() {
121            self.content = colorize_log(&new_content);
122        }
123    }
124
125    // resize the log content to fit in the specified width
126    fn fit_to_width(&mut self, width: usize) {
127        if width == 0 { return; }
128        self.rows = LinesIterator::new(self.content.as_spanned_str(), width).collect();
129    }
130
131    fn num_lines(&self) -> usize {
132        self.rows.len()
133    }
134
135    fn draw(&self, printer: &Printer) {
136        for (y, row) in self.rows.iter().enumerate() {
137            let mut x = 0;
138            for span in row.resolve(self.content.as_spanned_str()) {
139                printer.with_style(*span.attr, |printer| {
140                    printer.print((x, y), span.content);
141                    x += span.content.width();
142                });
143            }
144        }
145    }
146}
147
148fn colorize_log(log: &str) -> StyledString {
149    let mut styled_log = StyledString::new();
150
151    let mut line_color = Style::from(Color::Light(BaseColor::White));
152    for log_line in log.lines() {
153        if INFO_RE.is_match(log_line) {
154            line_color = Style::from(Color::Dark(BaseColor::Green));
155        }
156        else if DBG_RE.is_match(log_line) {
157            line_color = Style::from(Color::Dark(BaseColor::Cyan));
158        }
159        else if WARN_RE.is_match(log_line) {
160            line_color = Style::from(Color::Light(BaseColor::Yellow));
161        }
162        else if ERROR_RE.is_match(log_line) {
163            line_color = Style::from(Color::Light(BaseColor::Red));
164        }
165        else if FATAL_RE.is_match(log_line) {
166            line_color = Style::from(ColorStyle::new(Color::Light(BaseColor::Red), Color::Dark(BaseColor::Black)));
167        }
168
169        styled_log.append_styled(log_line, line_color);
170        styled_log.append('\n');
171    }
172
173    styled_log
174}