tui_logger/widget/
target.rs

1use std::sync::Arc;
2
3use parking_lot::Mutex;
4use ratatui::{
5    buffer::Buffer,
6    layout::Rect,
7    style::{Modifier, Style},
8    widgets::{Block, Widget},
9};
10
11use crate::logger::TUI_LOGGER;
12use crate::widget::inner::TuiWidgetInnerState;
13use crate::TuiWidgetState;
14use log::Level;
15use log::LevelFilter;
16
17fn advance_levelfilter(levelfilter: LevelFilter) -> (Option<LevelFilter>, Option<LevelFilter>) {
18    match levelfilter {
19        LevelFilter::Trace => (None, Some(LevelFilter::Debug)),
20        LevelFilter::Debug => (Some(LevelFilter::Trace), Some(LevelFilter::Info)),
21        LevelFilter::Info => (Some(LevelFilter::Debug), Some(LevelFilter::Warn)),
22        LevelFilter::Warn => (Some(LevelFilter::Info), Some(LevelFilter::Error)),
23        LevelFilter::Error => (Some(LevelFilter::Warn), Some(LevelFilter::Off)),
24        LevelFilter::Off => (Some(LevelFilter::Error), None),
25    }
26}
27
28/// This is the definition for the TuiLoggerTargetWidget,
29/// which allows configuration of the logger system and selection of log messages.
30pub struct TuiLoggerTargetWidget<'b> {
31    block: Option<Block<'b>>,
32    /// Base style of the widget
33    style: Style,
34    style_show: Style,
35    style_hide: Style,
36    style_off: Option<Style>,
37    highlight_style: Style,
38    state: Arc<Mutex<TuiWidgetInnerState>>,
39    targets: Vec<String>,
40}
41impl<'b> Default for TuiLoggerTargetWidget<'b> {
42    fn default() -> TuiLoggerTargetWidget<'b> {
43        TuiLoggerTargetWidget {
44            block: None,
45            style: Default::default(),
46            style_off: None,
47            style_hide: Style::default(),
48            style_show: Style::default().add_modifier(Modifier::REVERSED),
49            highlight_style: Style::default().add_modifier(Modifier::REVERSED),
50            state: Arc::new(Mutex::new(TuiWidgetInnerState::new())),
51            targets: vec![],
52        }
53    }
54}
55impl<'b> TuiLoggerTargetWidget<'b> {
56    pub fn block(mut self, block: Block<'b>) -> TuiLoggerTargetWidget<'b> {
57        self.block = Some(block);
58        self
59    }
60    pub fn opt_style(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
61        if let Some(s) = style {
62            self.style = s;
63        }
64        self
65    }
66    pub fn opt_style_off(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
67        if style.is_some() {
68            self.style_off = style;
69        }
70        self
71    }
72    pub fn opt_style_hide(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
73        if let Some(s) = style {
74            self.style_hide = s;
75        }
76        self
77    }
78    pub fn opt_style_show(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
79        if let Some(s) = style {
80            self.style_show = s;
81        }
82        self
83    }
84    pub fn opt_highlight_style(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
85        if let Some(s) = style {
86            self.highlight_style = s;
87        }
88        self
89    }
90    pub fn style(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
91        self.style = style;
92        self
93    }
94    pub fn style_off(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
95        self.style_off = Some(style);
96        self
97    }
98    pub fn style_hide(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
99        self.style_hide = style;
100        self
101    }
102    pub fn style_show(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
103        self.style_show = style;
104        self
105    }
106    pub fn highlight_style(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
107        self.highlight_style = style;
108        self
109    }
110    pub(crate) fn inner_state(
111        mut self,
112        state: Arc<Mutex<TuiWidgetInnerState>>,
113    ) -> TuiLoggerTargetWidget<'b> {
114        self.state = state;
115        self
116    }
117    pub fn state(mut self, state: &TuiWidgetState) -> TuiLoggerTargetWidget<'b> {
118        self.state = state.clone_state();
119        self
120    }
121}
122impl<'b> Widget for TuiLoggerTargetWidget<'b> {
123    fn render(mut self, area: Rect, buf: &mut Buffer) {
124        buf.set_style(area, self.style);
125        let list_area = match self.block.take() {
126            Some(b) => {
127                let inner_area = b.inner(area);
128                b.render(area, buf);
129                inner_area
130            }
131            None => area,
132        };
133        if list_area.width < 8 || list_area.height < 1 {
134            return;
135        }
136
137        let la_left = list_area.left();
138        let la_top = list_area.top();
139        let la_width = list_area.width as usize;
140
141        {
142            let inner = &TUI_LOGGER.inner.lock();
143            let hot_targets = &inner.targets;
144            let mut state = self.state.lock();
145            let hide_off = state.hide_off;
146            let offset = state.offset;
147            let focus_selected = state.focus_selected;
148            {
149                let targets = &mut state.config;
150                targets.merge(hot_targets);
151                self.targets.clear();
152                for (t, levelfilter) in targets.iter() {
153                    if hide_off && levelfilter == &LevelFilter::Off {
154                        continue;
155                    }
156                    self.targets.push(t.clone());
157                }
158                self.targets.sort();
159            }
160            state.nr_items = self.targets.len();
161            if state.selected >= state.nr_items {
162                state.selected = state.nr_items.max(1) - 1;
163            }
164            if state.selected < state.nr_items {
165                state.opt_selected_target = Some(self.targets[state.selected].clone());
166                let t = &self.targets[state.selected];
167                let (more, less) = if let Some(levelfilter) = state.config.get(t) {
168                    advance_levelfilter(levelfilter)
169                } else {
170                    (None, None)
171                };
172                state.opt_selected_visibility_less = less;
173                state.opt_selected_visibility_more = more;
174                let (more, less) = if let Some(levelfilter) = hot_targets.get(t) {
175                    advance_levelfilter(levelfilter)
176                } else {
177                    (None, None)
178                };
179                state.opt_selected_recording_less = less;
180                state.opt_selected_recording_more = more;
181            }
182            let list_height = (list_area.height as usize).min(self.targets.len());
183            let offset = if list_height > self.targets.len() {
184                0
185            } else if state.selected < state.nr_items {
186                let sel = state.selected;
187                if sel >= offset + list_height {
188                    // selected is below visible list range => make it the bottom
189                    sel - list_height + 1
190                } else if sel.min(offset) + list_height > self.targets.len() {
191                    self.targets.len() - list_height
192                } else {
193                    sel.min(offset)
194                }
195            } else {
196                0
197            };
198            state.offset = offset;
199
200            let targets = &(&state.config);
201            let default_level = inner.default;
202            for i in 0..list_height {
203                let t = &self.targets[i + offset];
204                // Comment in relation to issue #69:
205                // Widgets maintain their own list of level filters per target.
206                // These lists are not forwarded to the TUI_LOGGER, but kept widget private.
207                // Example: This widget's private list contains a target named "not_yet",
208                // and the application hasn't logged an entry with target "not_yet".
209                // If displaying the target list, then "not_yet" will be only present in target,
210                // but not in hot_targets. In issue #69 the problem has been, that
211                // `hot_targets.get(t).unwrap()` has caused a panic. Which is to be expected.
212                // The remedy is to use unwrap_or with default_level.
213                let hot_level_filter = hot_targets.get(t).unwrap_or(default_level);
214                let level_filter = targets.get(t).unwrap_or(default_level);
215                for (j, sym, lev) in &[
216                    (0, "E", Level::Error),
217                    (1, "W", Level::Warn),
218                    (2, "I", Level::Info),
219                    (3, "D", Level::Debug),
220                    (4, "T", Level::Trace),
221                ] {
222                    if let Some(cell) = buf.cell_mut((la_left + j, la_top + i as u16)) {
223                        let cell_style = if hot_level_filter >= *lev {
224                            if level_filter >= *lev {
225                                if !focus_selected || i + offset == state.selected {
226                                    self.style_show
227                                } else {
228                                    self.style_hide
229                                }
230                            } else {
231                                self.style_hide
232                            }
233                        } else if let Some(style_off) = self.style_off {
234                            style_off
235                        } else {
236                            cell.set_symbol(" ");
237                            continue;
238                        };
239                        cell.set_style(cell_style);
240                        cell.set_symbol(sym);
241                    }
242                }
243                buf.set_stringn(la_left + 5, la_top + i as u16, ":", la_width, self.style);
244                buf.set_stringn(
245                    la_left + 6,
246                    la_top + i as u16,
247                    t,
248                    la_width,
249                    if i + offset == state.selected {
250                        self.highlight_style
251                    } else {
252                        self.style
253                    },
254                );
255            }
256        }
257    }
258}