cursive_extras/views/
log_view.rs1use 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#[derive(Clone)]
40pub struct LogView {
41 path: PathBuf,
42 content: LogContent
43}
44
45impl LogView {
46 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 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 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 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 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}