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    text::Line,
11    widgets::{Block, Widget},
12};
13
14use crate::widget::inner::LinePointer;
15use crate::{CircularBuffer, ExtLogRecord, TuiLoggerLevelOutput, TuiWidgetState, TUI_LOGGER};
16
17use super::inner::TuiWidgetInnerState;
18
19pub struct TuiLoggerWidget<'b> {
20    block: Option<Block<'b>>,
21    logformatter: Option<Box<dyn LogFormatter>>,
22    /// Base style of the widget
23    style: Style,
24    /// Level based style
25    style_error: Option<Style>,
26    style_warn: Option<Style>,
27    style_debug: Option<Style>,
28    style_trace: Option<Style>,
29    style_info: Option<Style>,
30    format_separator: char,
31    format_timestamp: Option<String>,
32    format_output_level: Option<TuiLoggerLevelOutput>,
33    format_output_target: bool,
34    format_output_file: bool,
35    format_output_line: bool,
36    state: Arc<Mutex<TuiWidgetInnerState>>,
37}
38impl<'b> Default for TuiLoggerWidget<'b> {
39    fn default() -> TuiLoggerWidget<'b> {
40        TuiLoggerWidget {
41            block: None,
42            logformatter: None,
43            style: Default::default(),
44            style_error: None,
45            style_warn: None,
46            style_debug: None,
47            style_trace: None,
48            style_info: None,
49            format_separator: ':',
50            format_timestamp: Some("%H:%M:%S".to_string()),
51            format_output_level: Some(TuiLoggerLevelOutput::Long),
52            format_output_target: true,
53            format_output_file: true,
54            format_output_line: true,
55            state: Arc::new(Mutex::new(TuiWidgetInnerState::new())),
56        }
57    }
58}
59impl<'b> TuiLoggerWidget<'b> {
60    pub fn block(mut self, block: Block<'b>) -> Self {
61        self.block = Some(block);
62        self
63    }
64    pub fn opt_formatter(mut self, formatter: Option<Box<dyn LogFormatter>>) -> Self {
65        self.logformatter = formatter;
66        self
67    }
68    pub fn formatter(mut self, formatter: Box<dyn LogFormatter>) -> Self {
69        self.logformatter = Some(formatter);
70        self
71    }
72    pub fn opt_style(mut self, style: Option<Style>) -> Self {
73        if let Some(s) = style {
74            self.style = s;
75        }
76        self
77    }
78    pub fn opt_style_error(mut self, style: Option<Style>) -> Self {
79        if style.is_some() {
80            self.style_error = style;
81        }
82        self
83    }
84    pub fn opt_style_warn(mut self, style: Option<Style>) -> Self {
85        if style.is_some() {
86            self.style_warn = style;
87        }
88        self
89    }
90    pub fn opt_style_info(mut self, style: Option<Style>) -> Self {
91        if style.is_some() {
92            self.style_info = style;
93        }
94        self
95    }
96    pub fn opt_style_trace(mut self, style: Option<Style>) -> Self {
97        if style.is_some() {
98            self.style_trace = style;
99        }
100        self
101    }
102    pub fn opt_style_debug(mut self, style: Option<Style>) -> Self {
103        if style.is_some() {
104            self.style_debug = style;
105        }
106        self
107    }
108    pub fn style(mut self, style: Style) -> Self {
109        self.style = style;
110        self
111    }
112    pub fn style_error(mut self, style: Style) -> Self {
113        self.style_error = Some(style);
114        self
115    }
116    pub fn style_warn(mut self, style: Style) -> Self {
117        self.style_warn = Some(style);
118        self
119    }
120    pub fn style_info(mut self, style: Style) -> Self {
121        self.style_info = Some(style);
122        self
123    }
124    pub fn style_trace(mut self, style: Style) -> Self {
125        self.style_trace = Some(style);
126        self
127    }
128    pub fn style_debug(mut self, style: Style) -> Self {
129        self.style_debug = Some(style);
130        self
131    }
132    pub fn opt_output_separator(mut self, opt_sep: Option<char>) -> Self {
133        if let Some(ch) = opt_sep {
134            self.format_separator = ch;
135        }
136        self
137    }
138    /// Separator character between field.
139    /// Default is ':'
140    pub fn output_separator(mut self, sep: char) -> Self {
141        self.format_separator = sep;
142        self
143    }
144    pub fn opt_output_timestamp(mut self, opt_fmt: Option<Option<String>>) -> Self {
145        if let Some(fmt) = opt_fmt {
146            self.format_timestamp = fmt;
147        }
148        self
149    }
150    /// The format string can be defined as described in
151    /// <https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html>
152    ///
153    /// If called with None, timestamp is not included in output.
154    ///
155    /// Default is %H:%M:%S
156    pub fn output_timestamp(mut self, fmt: Option<String>) -> Self {
157        self.format_timestamp = fmt;
158        self
159    }
160    pub fn opt_output_level(mut self, opt_fmt: Option<Option<TuiLoggerLevelOutput>>) -> Self {
161        if let Some(fmt) = opt_fmt {
162            self.format_output_level = fmt;
163        }
164        self
165    }
166    /// Possible values are
167    /// - TuiLoggerLevelOutput::Long        => DEBUG/TRACE/...
168    /// - TuiLoggerLevelOutput::Abbreviated => D/T/...
169    ///
170    /// If called with None, level is not included in output.
171    ///
172    /// Default is Long
173    pub fn output_level(mut self, level: Option<TuiLoggerLevelOutput>) -> Self {
174        self.format_output_level = level;
175        self
176    }
177    pub fn opt_output_target(mut self, opt_enabled: Option<bool>) -> Self {
178        if let Some(enabled) = opt_enabled {
179            self.format_output_target = enabled;
180        }
181        self
182    }
183    /// Enables output of target field of event
184    ///
185    /// Default is true
186    pub fn output_target(mut self, enabled: bool) -> Self {
187        self.format_output_target = enabled;
188        self
189    }
190    pub fn opt_output_file(mut self, opt_enabled: Option<bool>) -> Self {
191        if let Some(enabled) = opt_enabled {
192            self.format_output_file = enabled;
193        }
194        self
195    }
196    /// Enables output of file field of event
197    ///
198    /// Default is true
199    pub fn output_file(mut self, enabled: bool) -> Self {
200        self.format_output_file = enabled;
201        self
202    }
203    pub fn opt_output_line(mut self, opt_enabled: Option<bool>) -> Self {
204        if let Some(enabled) = opt_enabled {
205            self.format_output_line = enabled;
206        }
207        self
208    }
209    /// Enables output of line field of event
210    ///
211    /// Default is true
212    pub fn output_line(mut self, enabled: bool) -> Self {
213        self.format_output_line = enabled;
214        self
215    }
216    pub fn inner_state(mut self, state: Arc<Mutex<TuiWidgetInnerState>>) -> Self {
217        self.state = state;
218        self
219    }
220    pub fn state(mut self, state: &TuiWidgetState) -> Self {
221        self.state = state.clone_state();
222        self
223    }
224    fn next_event<'a>(
225        &self,
226        events: &'a CircularBuffer<ExtLogRecord>,
227        mut index: usize,
228        ignore_current: bool,
229        increment: bool,
230        state: &TuiWidgetInnerState,
231    ) -> Option<(Option<usize>, usize, &'a ExtLogRecord)> {
232        // The result is an optional next_index, the event index and the event
233        if ignore_current {
234            index = if increment {
235                index + 1
236            } else {
237                if index == 0 {
238                    return None;
239                }
240                index - 1
241            };
242        }
243        while let Some(evt) = events.element_at_index(index) {
244            let mut skip = false;
245            if let Some(level) = state
246                .config
247                .get(&evt.target())
248                .or(state.config.get_default_display_level())
249            {
250                if level < evt.level {
251                    skip = true;
252                }
253            }
254            if !skip && state.focus_selected {
255                if let Some(target) = state.opt_selected_target.as_ref() {
256                    if target != &evt.target() {
257                        skip = true;
258                    }
259                }
260            }
261            if skip {
262                index = if increment {
263                    index + 1
264                } else {
265                    if index == 0 {
266                        break;
267                    }
268                    index - 1
269                };
270            } else {
271                if increment {
272                    return Some((Some(index + 1), index, evt));
273                } else {
274                    if index == 0 {
275                        return Some((None, index, evt));
276                    }
277                    return Some((Some(index - 1), index, evt));
278                };
279            }
280        }
281        None
282    }
283}
284impl<'b> Widget for TuiLoggerWidget<'b> {
285    fn render(mut self, area: Rect, buf: &mut Buffer) {
286        let render_debug = false;
287
288        let formatter = match self.logformatter.take() {
289            Some(fmt) => fmt,
290            None => {
291                let fmt = LogStandardFormatter {
292                    style: self.style,
293                    style_error: self.style_error,
294                    style_warn: self.style_warn,
295                    style_debug: self.style_debug,
296                    style_trace: self.style_trace,
297                    style_info: self.style_info,
298                    format_separator: self.format_separator,
299                    format_timestamp: self.format_timestamp.clone(),
300                    format_output_level: self.format_output_level,
301                    format_output_target: self.format_output_target,
302                    format_output_file: self.format_output_file,
303                    format_output_line: self.format_output_line,
304                };
305                Box::new(fmt)
306            }
307        };
308
309        buf.set_style(area, self.style);
310        let list_area = match self.block.take() {
311            Some(b) => {
312                let inner_area = b.inner(area);
313                b.render(area, buf);
314                inner_area
315            }
316            None => area,
317        };
318        if list_area.width < formatter.min_width() || list_area.height < 1 {
319            return;
320        }
321
322        let mut state = self.state.lock();
323        let la_height = list_area.height as usize;
324        let la_left = list_area.left();
325        let la_top = list_area.top();
326        let la_width = list_area.width as usize;
327        let mut rev_lines: Vec<(LinePointer, Line)> = vec![];
328        let mut can_scroll_up = true;
329        let mut can_scroll_down = state.opt_line_pointer_center.is_some();
330        {
331            enum Pos {
332                Top,
333                Bottom,
334                Center(usize),
335            }
336            let tui_lock = TUI_LOGGER.inner.lock();
337            // If scrolling, the opt_line_pointer_center is set.
338            // Otherwise we are following the bottom of the events
339            let opt_pos_event_index = if let Some(lp) = state.opt_line_pointer_center {
340                tui_lock.events.first_index().map(|first_index| {
341                    if first_index <= lp.event_index {
342                        (Pos::Center(lp.subline), lp.event_index)
343                    } else {
344                        (Pos::Top, first_index)
345                    }
346                })
347            } else {
348                tui_lock
349                    .events
350                    .last_index()
351                    .map(|last_index| (Pos::Bottom, last_index))
352            };
353            if let Some((pos, event_index)) = opt_pos_event_index {
354                // There are events to be shown
355                let mut lines: Vec<(usize, Vec<Line>, usize)> = Vec::new();
356                let mut from_line: isize = 0;
357                let mut to_line = 0;
358                match pos {
359                    Pos::Center(subline) => {
360                        if render_debug {
361                            println!("CENTER {}", event_index);
362                        }
363                        if let Some((_, evt_index, evt)) =
364                            self.next_event(&tui_lock.events, event_index, false, true, &state)
365                        {
366                            let evt_lines = formatter.format(la_width, evt);
367                            from_line = (la_height / 2) as isize - subline as isize;
368                            to_line = la_height / 2 + (evt_lines.len() - 1) - subline;
369                            let n = evt_lines.len();
370                            lines.push((evt_index, evt_lines, n));
371                            if render_debug {
372                                println!("Center is {}", evt_index);
373                            }
374                        }
375                    }
376                    Pos::Top => {
377                        can_scroll_up = false;
378                        if render_debug {
379                            println!("TOP");
380                        }
381                        if let Some((_, evt_index, evt)) =
382                            self.next_event(&tui_lock.events, event_index, false, true, &state)
383                        {
384                            let evt_lines = formatter.format(la_width, evt);
385                            from_line = 0;
386                            to_line = evt_lines.len() - 1;
387                            let n = evt_lines.len();
388                            lines.push((evt_index, evt_lines, n));
389                            if render_debug {
390                                println!("Top is {}", evt_index);
391                            }
392                        }
393                    }
394                    Pos::Bottom => {
395                        if render_debug {
396                            println!("TOP");
397                        }
398                        if let Some((_, evt_index, evt)) =
399                            self.next_event(&tui_lock.events, event_index, false, false, &state)
400                        {
401                            let evt_lines = formatter.format(la_width, evt);
402                            to_line = la_height - 1;
403                            from_line = to_line as isize - (evt_lines.len() - 1) as isize;
404                            let n = evt_lines.len();
405                            lines.push((evt_index, evt_lines, n));
406                            if render_debug {
407                                println!("Bottom is {}", evt_index);
408                            }
409                        }
410                    }
411                }
412                if !lines.is_empty() {
413                    let mut cont = true;
414                    let mut at_top = false;
415                    let mut at_bottom = false;
416                    while cont {
417                        if render_debug {
418                            println!("from_line {}, to_line {}", from_line, to_line);
419                        }
420                        cont = false;
421                        if from_line > 0 {
422                            if let Some((_, evt_index, evt)) = self.next_event(
423                                &tui_lock.events,
424                                lines.first().as_ref().unwrap().0,
425                                true,
426                                false,
427                                &state,
428                            ) {
429                                let evt_lines = formatter.format(la_width, evt);
430                                from_line -= evt_lines.len() as isize;
431                                let n = evt_lines.len();
432                                lines.insert(0, (evt_index, evt_lines, n));
433                                cont = true;
434                            } else {
435                                // no more events, so adjust start
436                                at_top = true;
437                                if render_debug {
438                                    println!("no more events adjust start");
439                                }
440                                to_line = to_line - from_line as usize;
441                                from_line = 0;
442                                if render_debug {
443                                    println!("=> from_line {}, to_line {}", from_line, to_line);
444                                }
445                            }
446                        }
447                        if to_line < la_height - 1 {
448                            if let Some((_, evt_index, evt)) = self.next_event(
449                                &tui_lock.events,
450                                lines.last().as_ref().unwrap().0,
451                                true,
452                                true,
453                                &state,
454                            ) {
455                                let evt_lines = formatter.format(la_width, evt);
456                                to_line += evt_lines.len();
457                                let n = evt_lines.len();
458                                lines.push((evt_index, evt_lines, n));
459                                cont = true;
460                            } else {
461                                at_bottom = true;
462                                can_scroll_down = false;
463                                if render_debug {
464                                    println!("no more events at end");
465                                }
466                                // no more events
467                                if to_line != la_height - 1 {
468                                    cont = true;
469                                } else if !cont {
470                                    break;
471                                }
472                                // no more events, so adjust end
473                                from_line = from_line + (la_height - 1 - to_line) as isize;
474                                to_line = la_height - 1;
475                                if render_debug {
476                                    println!("=> from_line {}, to_line {}", from_line, to_line);
477                                }
478                            }
479                        }
480                        if at_top && at_bottom {
481                            break;
482                        }
483                    }
484                    if at_top {
485                        can_scroll_up = false;
486                    }
487                    if at_bottom {
488                        can_scroll_down = false;
489                    }
490                    if render_debug {
491                        println!("finished: from_line {}, to_line {}", from_line, to_line);
492                    }
493                    let mut curr: isize = to_line as isize;
494                    while let Some((evt_index, evt_lines, mut n)) = lines.pop() {
495                        for line in evt_lines.into_iter().rev() {
496                            n -= 1;
497                            if curr < 0 {
498                                break;
499                            }
500                            if curr < la_height as isize {
501                                let line_ptr = LinePointer {
502                                    event_index: evt_index,
503                                    subline: n,
504                                };
505                                rev_lines.push((line_ptr, line));
506                            }
507                            curr -= 1;
508                        }
509                    }
510                }
511            } else {
512                can_scroll_down = false;
513                can_scroll_up = false;
514            }
515        }
516
517        state.opt_line_pointer_next_page = if can_scroll_down {
518            rev_lines.first().map(|l| l.0)
519        } else {
520            None
521        };
522        state.opt_line_pointer_prev_page = if can_scroll_up {
523            rev_lines.last().map(|l| l.0)
524        } else {
525            None
526        };
527
528        if render_debug {
529            println!("Line pointers in buffer:");
530            for l in rev_lines.iter().rev() {
531                println!("event_index {}, subline {}", l.0.event_index, l.0.subline);
532            }
533            if state.opt_line_pointer_center.is_some() {
534                println!(
535                    "Linepointer center: {:?}",
536                    state.opt_line_pointer_center.unwrap()
537                );
538            }
539            if state.opt_line_pointer_next_page.is_some() {
540                println!(
541                    "Linepointer next: {:?}",
542                    state.opt_line_pointer_next_page.unwrap()
543                );
544            }
545            if state.opt_line_pointer_prev_page.is_some() {
546                println!(
547                    "Linepointer prev: {:?}",
548                    state.opt_line_pointer_prev_page.unwrap()
549                );
550            }
551        }
552
553        // This apparently ensures, that the log starts at top
554        let offset: u16 = if state.opt_line_pointer_center.is_none() {
555            0
556        } else {
557            let lines_cnt = rev_lines.len();
558            std::cmp::max(0, la_height - lines_cnt) as u16
559        };
560
561        for (i, line) in rev_lines.into_iter().rev().take(la_height).enumerate() {
562            line.1.render(
563                Rect {
564                    x: la_left,
565                    y: la_top + i as u16 + offset,
566                    width: list_area.width,
567                    height: 1,
568                },
569                buf,
570            )
571        }
572    }
573}