tui_logger/widget/
standard.rs

1use crate::widget::logformatter::LogFormatter;
2use crate::widget::standard_formatter::LogStandardFormatter;
3use parking_lot::Mutex;
4use std::sync::Arc;
5
6use ratatui::{
7    buffer::Buffer,
8    layout::Rect,
9    style::Style,
10    widgets::{Block, Widget},
11};
12
13use crate::{CircularBuffer, TuiLoggerLevelOutput, TuiWidgetState, TUI_LOGGER};
14
15use super::inner::TuiWidgetInnerState;
16
17pub struct TuiLoggerWidget<'b> {
18    block: Option<Block<'b>>,
19    logformatter: Option<Box<dyn LogFormatter>>,
20    /// Base style of the widget
21    style: Style,
22    /// Level based style
23    style_error: Option<Style>,
24    style_warn: Option<Style>,
25    style_debug: Option<Style>,
26    style_trace: Option<Style>,
27    style_info: Option<Style>,
28    format_separator: char,
29    format_timestamp: Option<String>,
30    format_output_level: Option<TuiLoggerLevelOutput>,
31    format_output_target: bool,
32    format_output_file: bool,
33    format_output_line: bool,
34    state: Arc<Mutex<TuiWidgetInnerState>>,
35}
36impl<'b> Default for TuiLoggerWidget<'b> {
37    fn default() -> TuiLoggerWidget<'b> {
38        //TUI_LOGGER.move_events();
39        TuiLoggerWidget {
40            block: None,
41            logformatter: None,
42            style: Default::default(),
43            style_error: None,
44            style_warn: None,
45            style_debug: None,
46            style_trace: None,
47            style_info: None,
48            format_separator: ':',
49            format_timestamp: Some("%H:%M:%S".to_string()),
50            format_output_level: Some(TuiLoggerLevelOutput::Long),
51            format_output_target: true,
52            format_output_file: true,
53            format_output_line: true,
54            state: Arc::new(Mutex::new(TuiWidgetInnerState::new())),
55        }
56    }
57}
58impl<'b> TuiLoggerWidget<'b> {
59    pub fn block(mut self, block: Block<'b>) -> Self {
60        self.block = Some(block);
61        self
62    }
63    pub fn opt_formatter(mut self, formatter: Option<Box<dyn LogFormatter>>) -> Self {
64        self.logformatter = formatter;
65        self
66    }
67    pub fn formatter(mut self, formatter: Box<dyn LogFormatter>) -> Self {
68        self.logformatter = Some(formatter);
69        self
70    }
71    pub fn opt_style(mut self, style: Option<Style>) -> Self {
72        if let Some(s) = style {
73            self.style = s;
74        }
75        self
76    }
77    pub fn opt_style_error(mut self, style: Option<Style>) -> Self {
78        if style.is_some() {
79            self.style_error = style;
80        }
81        self
82    }
83    pub fn opt_style_warn(mut self, style: Option<Style>) -> Self {
84        if style.is_some() {
85            self.style_warn = style;
86        }
87        self
88    }
89    pub fn opt_style_info(mut self, style: Option<Style>) -> Self {
90        if style.is_some() {
91            self.style_info = style;
92        }
93        self
94    }
95    pub fn opt_style_trace(mut self, style: Option<Style>) -> Self {
96        if style.is_some() {
97            self.style_trace = style;
98        }
99        self
100    }
101    pub fn opt_style_debug(mut self, style: Option<Style>) -> Self {
102        if style.is_some() {
103            self.style_debug = style;
104        }
105        self
106    }
107    pub fn style(mut self, style: Style) -> Self {
108        self.style = style;
109        self
110    }
111    pub fn style_error(mut self, style: Style) -> Self {
112        self.style_error = Some(style);
113        self
114    }
115    pub fn style_warn(mut self, style: Style) -> Self {
116        self.style_warn = Some(style);
117        self
118    }
119    pub fn style_info(mut self, style: Style) -> Self {
120        self.style_info = Some(style);
121        self
122    }
123    pub fn style_trace(mut self, style: Style) -> Self {
124        self.style_trace = Some(style);
125        self
126    }
127    pub fn style_debug(mut self, style: Style) -> Self {
128        self.style_debug = Some(style);
129        self
130    }
131    pub fn opt_output_separator(mut self, opt_sep: Option<char>) -> Self {
132        if let Some(ch) = opt_sep {
133            self.format_separator = ch;
134        }
135        self
136    }
137    /// Separator character between field.
138    /// Default is ':'
139    pub fn output_separator(mut self, sep: char) -> Self {
140        self.format_separator = sep;
141        self
142    }
143    pub fn opt_output_timestamp(mut self, opt_fmt: Option<Option<String>>) -> Self {
144        if let Some(fmt) = opt_fmt {
145            self.format_timestamp = fmt;
146        }
147        self
148    }
149    /// The format string can be defined as described in
150    /// <https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html>
151    ///
152    /// If called with None, timestamp is not included in output.
153    ///
154    /// Default is %H:%M:%S
155    pub fn output_timestamp(mut self, fmt: Option<String>) -> Self {
156        self.format_timestamp = fmt;
157        self
158    }
159    pub fn opt_output_level(mut self, opt_fmt: Option<Option<TuiLoggerLevelOutput>>) -> Self {
160        if let Some(fmt) = opt_fmt {
161            self.format_output_level = fmt;
162        }
163        self
164    }
165    /// Possible values are
166    /// - TuiLoggerLevelOutput::Long        => DEBUG/TRACE/...
167    /// - TuiLoggerLevelOutput::Abbreviated => D/T/...
168    ///
169    /// If called with None, level is not included in output.
170    ///
171    /// Default is Long
172    pub fn output_level(mut self, level: Option<TuiLoggerLevelOutput>) -> Self {
173        self.format_output_level = level;
174        self
175    }
176    pub fn opt_output_target(mut self, opt_enabled: Option<bool>) -> Self {
177        if let Some(enabled) = opt_enabled {
178            self.format_output_target = enabled;
179        }
180        self
181    }
182    /// Enables output of target field of event
183    ///
184    /// Default is true
185    pub fn output_target(mut self, enabled: bool) -> Self {
186        self.format_output_target = enabled;
187        self
188    }
189    pub fn opt_output_file(mut self, opt_enabled: Option<bool>) -> Self {
190        if let Some(enabled) = opt_enabled {
191            self.format_output_file = enabled;
192        }
193        self
194    }
195    /// Enables output of file field of event
196    ///
197    /// Default is true
198    pub fn output_file(mut self, enabled: bool) -> Self {
199        self.format_output_file = enabled;
200        self
201    }
202    pub fn opt_output_line(mut self, opt_enabled: Option<bool>) -> Self {
203        if let Some(enabled) = opt_enabled {
204            self.format_output_line = enabled;
205        }
206        self
207    }
208    /// Enables output of line field of event
209    ///
210    /// Default is true
211    pub fn output_line(mut self, enabled: bool) -> Self {
212        self.format_output_line = enabled;
213        self
214    }
215    pub fn inner_state(mut self, state: Arc<Mutex<TuiWidgetInnerState>>) -> Self {
216        self.state = state;
217        self
218    }
219    pub fn state(mut self, state: &TuiWidgetState) -> Self {
220        self.state = state.inner.clone();
221        self
222    }
223}
224impl<'b> Widget for TuiLoggerWidget<'b> {
225    fn render(mut self, area: Rect, buf: &mut Buffer) {
226        let formatter = match self.logformatter.take() {
227            Some(fmt) => fmt,
228            None => {
229                let fmt = LogStandardFormatter {
230                    style: self.style,
231                    style_error: self.style_error,
232                    style_warn: self.style_warn,
233                    style_debug: self.style_debug,
234                    style_trace: self.style_trace,
235                    style_info: self.style_info,
236                    format_separator: self.format_separator,
237                    format_timestamp: self.format_timestamp,
238                    format_output_level: self.format_output_level,
239                    format_output_target: self.format_output_target,
240                    format_output_file: self.format_output_file,
241                    format_output_line: self.format_output_line,
242                };
243                Box::new(fmt)
244            }
245        };
246
247        buf.set_style(area, self.style);
248        let list_area = match self.block.take() {
249            Some(b) => {
250                let inner_area = b.inner(area);
251                b.render(area, buf);
252                inner_area
253            }
254            None => area,
255        };
256        if list_area.width < formatter.min_width() || list_area.height < 1 {
257            return;
258        }
259
260        let mut state = self.state.lock();
261        let la_height = list_area.height as usize;
262        let la_left = list_area.left();
263        let la_top = list_area.top();
264        let la_width = list_area.width as usize;
265        //let mut lines: Vec<Line> = vec![];
266        let mut lines = CircularBuffer::new(la_height);
267        {
268            state.opt_timestamp_next_page = None;
269            let opt_timestamp_bottom = state.opt_timestamp_bottom;
270            let mut opt_timestamp_prev_page = None;
271            let mut tui_lock = TUI_LOGGER.inner.lock();
272            let mut circular = CircularBuffer::new(10); // MAGIC constant
273            for evt in tui_lock.events.rev_iter() {
274                if let Some(level) = state.config.get(&evt.target) {
275                    if level < evt.level {
276                        continue;
277                    }
278                } else if let Some(level) = state.config.default_display_level {
279                    if level < evt.level {
280                        continue;
281                    }
282                }
283                if state.focus_selected {
284                    if let Some(target) = state.opt_selected_target.as_ref() {
285                        if target != &evt.target {
286                            continue;
287                        }
288                    }
289                }
290                // Here all filters have been applied,
291                // So check, if user is paging through history
292                if let Some(timestamp) = opt_timestamp_bottom.as_ref() {
293                    if *timestamp < evt.timestamp {
294                        circular.push(evt.timestamp);
295                        continue;
296                    }
297                }
298                if !circular.is_empty() {
299                    state.opt_timestamp_next_page = circular.take().first().cloned();
300                }
301                let mut evt_lines = formatter.format(la_width, evt);
302                while let Some(line) = evt_lines.pop() {
303                    lines.push(line);
304                }
305                if lines.len() >= la_height {
306                    break;
307                }
308                if opt_timestamp_prev_page.is_none() && lines.len() >= la_height / 2 {
309                    opt_timestamp_prev_page = Some(evt.timestamp);
310                }
311            }
312            state.opt_timestamp_prev_page = opt_timestamp_prev_page.or(state.opt_timestamp_bottom);
313        }
314
315        // This apparently ensures, that the log starts at top
316        let offset: u16 = if state.opt_timestamp_bottom.is_none() {
317            0
318        } else {
319            let lines_cnt = lines.len();
320            std::cmp::max(0, la_height - lines_cnt) as u16
321        };
322
323        for (i, line) in lines.iter().rev().take(la_height).enumerate() {
324            line.render(
325                Rect {
326                    x: la_left,
327                    y: la_top + i as u16 + offset,
328                    width: list_area.width,
329                    height: 1,
330                },
331                buf,
332            )
333        }
334    }
335}