cursive_extras/views/
log_view.rs

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