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