tui_logger/
lib.rs

1//! # Logger with smart widget for the `tui` and `ratatui` crate
2//!
3//! [![dependency status](https://deps.rs/repo/github/gin66/tui-logger/status.svg?service=github&nocache=0_9_1)](https://deps.rs/repo/github/gin66/tui-logger)
4//! ![Build examples](https://github.com/gin66/tui-logger/workflows/Build%20examples/badge.svg?service=github)
5//!
6//!
7//! ## Demo of the widget
8//!
9//! ![Demo](https://github.com/gin66/tui-logger/blob/master/doc/demo_v0.14.4.gif?raw=true)
10//!
11//! ## Documentation
12//!
13//! [Documentation](https://docs.rs/tui-logger/latest/tui_logger/)
14//!
15//! ## Important note for `tui`
16//!
17//! The `tui` crate has been archived and `ratatui` has taken over.
18//! In order to avoid supporting compatibility for an inactive crate,
19//! the v0.9.x releases are the last to support `tui`. In case future bug fixes
20//! are needed, the branch `tui_legacy` has been created to track changes to 0.9.x releases.
21//!
22//! Starting with v0.10 `tui-logger` is `ratatui` only.
23//!
24//! ## Features
25//!
26//! - [X] Logger implementation for the `log` crate
27//! - [X] Logger enable/disable detection via hash table (avoid string compare)
28//! - [X] Hot logger code only copies enabled log messages with timestamp into a circular buffer
29//! - [X] Widgets/move_message() retrieve captured log messages from hot circular buffer
30//! - [X] Lost message detection due to circular buffer
31//! - [X] Log filtering performed on log record target
32//! - [X] Simple Widgets to view logs and configure debuglevel per target
33//! - [X] Logging of enabled logs to file
34//! - [X] Scrollback in log history
35//! - [x] Title of target and log pane can be configured
36//! - [X] `slog` support, providing a Drain to integrate into your `slog` infrastructure
37//! - [X] `tracing` support
38//! - [X] Support to use custom formatter for log events
39//! - [ ] Allow configuration of target dependent loglevel specifically for file logging
40//! - [ ] Avoid duplicating of target, module and filename in every log record
41//! - [ ] Simultaneous modification of all targets' display/hot logging loglevel by key command
42//!
43//! ## Smart Widget
44//!
45//! Smart widget consists of two widgets. Left is the target selector widget and
46//! on the right side the logging messages view scrolling up. The target selector widget
47//! can be hidden/shown during runtime via key command.
48//! The key command to be provided to the TuiLoggerWidget via transition() function.
49//!
50//! The target selector widget looks like this:
51//!
52//! ![widget](https://github.com/gin66/tui-logger/blob/master/doc/example.png?raw=true)
53//!
54//! It controls:
55//!
56//! - Capturing of log messages by the logger
57//! - Selection of levels for display in the logging message view
58//!
59//! The two columns have the following meaning:
60//!
61//! - Code EWIDT: E stands for Error, W for Warn, Info, Debug and Trace.
62//!   + Inverted characters (EWIDT) are enabled log levels in the view
63//!   + Normal characters show enabled capturing of a log level per target
64//!   + If any of EWIDT are not shown, then the respective log level is not captured
65//! - Target of the log events can be defined in the log e.g. `warn!(target: "demo", "Log message");`
66//!
67//! ## Smart Widget Key Commands
68//! ```ignore
69//! |  KEY     | ACTION
70//! |----------|-----------------------------------------------------------|
71//! | h        | Toggles target selector widget hidden/visible
72//! | f        | Toggle focus on the selected target only
73//! | UP       | Select previous target in target selector widget
74//! | DOWN     | Select next target in target selector widget
75//! | LEFT     | Reduce SHOWN (!) log messages by one level
76//! | RIGHT    | Increase SHOWN (!) log messages by one level
77//! | -        | Reduce CAPTURED (!) log messages by one level
78//! | +        | Increase CAPTURED (!) log messages by one level
79//! | PAGEUP   | Enter Page Mode and scroll approx. half page up in log history.
80//! | PAGEDOWN | Only in page mode: scroll 10 events down in log history.
81//! | ESCAPE   | Exit page mode and go back to scrolling mode
82//! | SPACE    | Toggles hiding of targets, which have logfilter set to off
83//! ```
84//!
85//! The mapping of key to action has to be done in the application. The respective TuiWidgetEvent
86//! has to be provided to TuiWidgetState::transition().
87//!
88//! Remark to the page mode: The timestamp of the event at event history's bottom line is used as
89//! reference. This means, changing the filters in the EWIDT/focus from the target selector window
90//! should work as expected without jumps in the history. The page next/forward advances as
91//! per visibility of the events.
92//!
93//! ## Basic usage to initialize logger-system:
94//! ```rust
95//! #[macro_use]
96//! extern crate log;
97//! //use tui_logger;
98//!
99//! fn main() {
100//!     // Early initialization of the logger
101//!
102//!     // Set max_log_level to Trace
103//!     tui_logger::init_logger(log::LevelFilter::Trace).unwrap();
104//!
105//!     // Set default level for unknown targets to Trace
106//!     tui_logger::set_default_level(log::LevelFilter::Trace);
107//!
108//!     // code....
109//! }
110//! ```
111//!
112//! For use of the widget please check examples/demo.rs
113//!
114//! ## Demo
115//!
116//! Run demo using termion:
117//!
118//! ```ignore
119//! cargo run --example demo --features termion
120//! ```
121//!
122//! Run demo with crossterm:
123//!
124//! ```ignore
125//! cargo run --example demo --features crossterm
126//! ```
127//!
128//! Run demo using termion and simple custom formatter in bottom right log widget:
129//!
130//! ```ignore
131//! cargo run --example demo --features termion,formatter
132//! ```
133//!
134//! ## `slog` support
135//!
136//! `tui-logger` provides a [`TuiSlogDrain`] which implements `slog::Drain` and will route all records
137//! it receives to the `tui-logger` widget.
138//!
139//! Enabled by feature "slog-support"
140//!
141//! ## `tracing-subscriber` support
142//!
143//! `tui-logger` provides a [`TuiTracingSubscriberLayer`] which implements
144//! `tracing_subscriber::Layer` and will collect all events
145//! it receives to the `tui-logger` widget
146//!
147//! Enabled by feature "tracing-support"
148//!
149//! ## Custom filtering
150//! ```rust
151//! #[macro_use]
152//! extern crate log;
153//! //use tui_logger;
154//! use env_logger;
155//!
156//! fn main() {
157//!     // Early initialization of the logger
158//!     let drain = tui_logger::Drain::new();
159//!     // instead of tui_logger::init_logger, we use `env_logger`
160//!     env_logger::Builder::default()
161//!         .format(move |buf, record|
162//!             // patch the env-logger entry through our drain to the tui-logger
163//!             Ok(drain.log(record))
164//!         ).init(); // make this the global logger
165//!     // code....
166//! }
167//! ```
168//!
169//! ## Custom formatting
170//!
171//! For experts only ! Configure along the lines:
172//! ```ignore
173//! use tui_logger::LogFormatter;
174//!
175//! let formatter = MyLogFormatter();
176//!
177//! TuiLoggerWidget::default()
178//! .block(Block::bordered().title("Filtered TuiLoggerWidget"))
179//! .formatter(formatter)
180//! .state(&filter_state)
181//! .render(left, buf);
182//! ```
183//! The example demo can be invoked to use a custom formatter as example for the bottom right widget.
184//!
185// Enable docsrs doc_cfg - to display non-default feature documentation.
186#![cfg_attr(docsrs, feature(doc_cfg))]
187#[macro_use]
188extern crate lazy_static;
189
190use std::collections::hash_map::Iter;
191use std::collections::hash_map::Keys;
192use std::collections::HashMap;
193use std::io::Write;
194use std::mem;
195use std::sync::Arc;
196use std::thread;
197
198use chrono::{DateTime, Local};
199use log::{Level, Log, Metadata, Record, SetLoggerError};
200use parking_lot::Mutex;
201use ratatui::{
202    buffer::Buffer,
203    layout::Rect,
204    style::{Modifier, Style},
205    widgets::{Block, Widget},
206};
207use widget::inner::TuiLoggerInner;
208use widget::inner::TuiWidgetInnerState;
209
210mod circular;
211#[cfg(feature = "slog-support")]
212#[cfg_attr(docsrs, doc(cfg(feature = "slog-support")))]
213mod slog;
214#[cfg(feature = "tracing-support")]
215#[cfg_attr(docsrs, doc(cfg(feature = "tracing-support")))]
216mod tracing_subscriber;
217
218pub use crate::circular::CircularBuffer;
219#[cfg(feature = "slog-support")]
220#[cfg_attr(docsrs, doc(cfg(feature = "slog-support")))]
221pub use crate::slog::TuiSlogDrain;
222#[cfg(feature = "tracing-support")]
223#[cfg_attr(docsrs, doc(cfg(feature = "tracing-support")))]
224pub use crate::tracing_subscriber::TuiTracingSubscriberLayer;
225#[doc(no_inline)]
226pub use log::LevelFilter;
227
228pub mod widget;
229pub use widget::inner::TuiWidgetState;
230pub use widget::logformatter::LogFormatter;
231pub use widget::smart::TuiLoggerSmartWidget;
232pub use widget::standard::TuiLoggerWidget;
233
234pub mod file;
235pub use file::TuiLoggerFile;
236
237pub struct ExtLogRecord {
238    pub timestamp: DateTime<Local>,
239    pub level: Level,
240    pub target: String,
241    pub file: String,
242    pub line: u32,
243    pub msg: String,
244}
245
246fn advance_levelfilter(levelfilter: LevelFilter) -> (Option<LevelFilter>, Option<LevelFilter>) {
247    match levelfilter {
248        LevelFilter::Trace => (None, Some(LevelFilter::Debug)),
249        LevelFilter::Debug => (Some(LevelFilter::Trace), Some(LevelFilter::Info)),
250        LevelFilter::Info => (Some(LevelFilter::Debug), Some(LevelFilter::Warn)),
251        LevelFilter::Warn => (Some(LevelFilter::Info), Some(LevelFilter::Error)),
252        LevelFilter::Error => (Some(LevelFilter::Warn), Some(LevelFilter::Off)),
253        LevelFilter::Off => (Some(LevelFilter::Error), None),
254    }
255}
256
257/// LevelConfig stores the relation target->LevelFilter in a hash table.
258///
259/// The table supports copying from the logger system LevelConfig to
260/// a widget's LevelConfig. In order to detect changes, the generation
261/// of the hash table is compared with any previous copied table.
262/// On every change the generation is incremented.
263#[derive(Default)]
264pub struct LevelConfig {
265    config: HashMap<String, LevelFilter>,
266    generation: u64,
267    origin_generation: u64,
268    default_display_level: Option<LevelFilter>,
269}
270impl LevelConfig {
271    /// Create an empty LevelConfig.
272    pub fn new() -> LevelConfig {
273        LevelConfig {
274            config: HashMap::new(),
275            generation: 0,
276            origin_generation: 0,
277            default_display_level: None,
278        }
279    }
280    /// Set for a given target the LevelFilter in the table and update the generation.
281    pub fn set(&mut self, target: &str, level: LevelFilter) {
282        if let Some(lev) = self.config.get_mut(target) {
283            if *lev != level {
284                *lev = level;
285                self.generation += 1;
286            }
287            return;
288        }
289        self.config.insert(target.to_string(), level);
290        self.generation += 1;
291    }
292    /// Set default display level filter for new targets - independent from recording
293    pub fn set_default_display_level(&mut self, level: LevelFilter) {
294        self.default_display_level = Some(level);
295    }
296    /// Retrieve an iter for all the targets stored in the hash table.
297    pub fn keys(&self) -> Keys<String, LevelFilter> {
298        self.config.keys()
299    }
300    /// Get the levelfilter for a given target.
301    pub fn get(&self, target: &str) -> Option<LevelFilter> {
302        self.config.get(target).cloned()
303    }
304    /// Retrieve an iterator through all entries of the table.
305    pub fn iter(&self) -> Iter<String, LevelFilter> {
306        self.config.iter()
307    }
308    /// Merge an origin LevelConfig into this one.
309    ///
310    /// The origin table defines the maximum levelfilter.
311    /// If this table has a higher levelfilter, then it will be reduced.
312    /// Unknown targets will be copied to this table.
313    fn merge(&mut self, origin: &LevelConfig) {
314        if self.origin_generation != origin.generation {
315            for (target, origin_levelfilter) in origin.iter() {
316                if let Some(levelfilter) = self.get(target) {
317                    if levelfilter <= *origin_levelfilter {
318                        continue;
319                    }
320                }
321                let levelfilter = self
322                    .default_display_level
323                    .map(|lvl| {
324                        if lvl > *origin_levelfilter {
325                            *origin_levelfilter
326                        } else {
327                            lvl
328                        }
329                    })
330                    .unwrap_or(*origin_levelfilter);
331                self.set(target, levelfilter);
332            }
333            self.generation = origin.generation;
334        }
335    }
336}
337
338/// These are the sub-structs for the static TUI_LOGGER struct.
339struct HotSelect {
340    hashtable: HashMap<u64, LevelFilter>,
341    default: LevelFilter,
342}
343struct HotLog {
344    events: CircularBuffer<ExtLogRecord>,
345    mover_thread: Option<thread::JoinHandle<()>>,
346}
347
348struct TuiLogger {
349    hot_select: Mutex<HotSelect>,
350    hot_log: Mutex<HotLog>,
351    inner: Mutex<TuiLoggerInner>,
352}
353impl TuiLogger {
354    pub fn move_events(&self) {
355        // If there are no new events, then just return
356        if self.hot_log.lock().events.total_elements() == 0 {
357            return;
358        }
359        // Exchange new event buffer with the hot buffer
360        let mut received_events = {
361            let hot_depth = self.inner.lock().hot_depth;
362            let new_circular = CircularBuffer::new(hot_depth);
363            let mut hl = self.hot_log.lock();
364            mem::replace(&mut hl.events, new_circular)
365        };
366        let mut tli = self.inner.lock();
367        let total = received_events.total_elements();
368        let elements = received_events.len();
369        tli.total_events += total;
370        let mut consumed = received_events.take();
371        let mut reversed = Vec::with_capacity(consumed.len() + 1);
372        while let Some(log_entry) = consumed.pop() {
373            reversed.push(log_entry);
374        }
375        if total > elements {
376            // Too many events received, so some have been lost
377            let new_log_entry = ExtLogRecord {
378                timestamp: reversed[reversed.len() - 1].timestamp,
379                level: Level::Warn,
380                target: "TuiLogger".to_string(),
381                file: "?".to_string(),
382                line: 0,
383                msg: format!(
384                    "There have been {} events lost, {} recorded out of {}",
385                    total - elements,
386                    elements,
387                    total
388                ),
389            };
390            reversed.push(new_log_entry);
391        }
392        let default_level = tli.default;
393        while let Some(log_entry) = reversed.pop() {
394            if tli.targets.get(&log_entry.target).is_none() {
395                tli.targets.set(&log_entry.target, default_level);
396            }
397            if let Some(ref mut file_options) = tli.dump {
398                let mut output = String::new();
399                let (lev_long, lev_abbr, with_loc) = match log_entry.level {
400                    log::Level::Error => ("ERROR", "E", true),
401                    log::Level::Warn => ("WARN ", "W", true),
402                    log::Level::Info => ("INFO ", "I", false),
403                    log::Level::Debug => ("DEBUG", "D", true),
404                    log::Level::Trace => ("TRACE", "T", true),
405                };
406                if let Some(fmt) = file_options.timestamp_fmt.as_ref() {
407                    output.push_str(&format!("{}", log_entry.timestamp.format(fmt)));
408                    output.push(file_options.format_separator);
409                }
410                match file_options.format_output_level {
411                    None => {}
412                    Some(TuiLoggerLevelOutput::Abbreviated) => {
413                        output.push_str(lev_abbr);
414                        output.push(file_options.format_separator);
415                    }
416                    Some(TuiLoggerLevelOutput::Long) => {
417                        output.push_str(lev_long);
418                        output.push(file_options.format_separator);
419                    }
420                }
421                if file_options.format_output_target {
422                    output.push_str(&log_entry.target);
423                    output.push(file_options.format_separator);
424                }
425                if with_loc {
426                    if file_options.format_output_file {
427                        output.push_str(&log_entry.file);
428                        output.push(file_options.format_separator);
429                    }
430                    if file_options.format_output_line {
431                        output.push_str(&format!("{}", log_entry.line));
432                        output.push(file_options.format_separator);
433                    }
434                }
435                output.push_str(&log_entry.msg);
436                if let Err(_e) = writeln!(file_options.dump, "{}", output) {
437                    // TODO: What to do in case of write error ?
438                }
439            }
440            tli.events.push(log_entry);
441        }
442    }
443}
444lazy_static! {
445    static ref TUI_LOGGER: TuiLogger = {
446        let hs = HotSelect {
447            hashtable: HashMap::with_capacity(1000),
448            default: LevelFilter::Info,
449        };
450        let hl = HotLog {
451            events: CircularBuffer::new(1000),
452            mover_thread: None,
453        };
454        let tli = TuiLoggerInner {
455            hot_depth: 1000,
456            events: CircularBuffer::new(10000),
457            total_events: 0,
458            dump: None,
459            default: LevelFilter::Info,
460            targets: LevelConfig::new(),
461        };
462        TuiLogger {
463            hot_select: Mutex::new(hs),
464            hot_log: Mutex::new(hl),
465            inner: Mutex::new(tli),
466        }
467    };
468}
469
470// Lots of boilerplate code, so that init_logger can return two error types...
471#[derive(Debug)]
472pub enum TuiLoggerError {
473    SetLoggerError(SetLoggerError),
474    ThreadError(std::io::Error),
475}
476impl std::error::Error for TuiLoggerError {
477    fn description(&self) -> &str {
478        match self {
479            TuiLoggerError::SetLoggerError(_) => "SetLoggerError",
480            TuiLoggerError::ThreadError(_) => "ThreadError",
481        }
482    }
483    fn cause(&self) -> Option<&dyn std::error::Error> {
484        match self {
485            TuiLoggerError::SetLoggerError(_) => None,
486            TuiLoggerError::ThreadError(err) => Some(err),
487        }
488    }
489}
490impl std::fmt::Display for TuiLoggerError {
491    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492        match self {
493            TuiLoggerError::SetLoggerError(err) => write!(f, "SetLoggerError({})", err),
494            TuiLoggerError::ThreadError(err) => write!(f, "ThreadError({})", err),
495        }
496    }
497}
498
499/// Init the logger.
500pub fn init_logger(max_level: LevelFilter) -> Result<(), TuiLoggerError> {
501    let join_handle = thread::Builder::new()
502        .name("tui-logger::move_events".into())
503        .spawn(|| {
504            let duration = std::time::Duration::from_millis(10);
505            loop {
506                thread::park_timeout(duration);
507                TUI_LOGGER.move_events();
508            }
509        })
510        .map_err(|err| TuiLoggerError::ThreadError(err))?;
511    TUI_LOGGER.hot_log.lock().mover_thread = Some(join_handle);
512    if cfg!(feature = "tracing-support") {
513        set_default_level(max_level);
514        Ok(())
515    } else {
516        log::set_max_level(max_level);
517        log::set_logger(&*TUI_LOGGER).map_err(|err| TuiLoggerError::SetLoggerError(err))
518    }
519}
520
521#[cfg(feature = "slog-support")]
522#[cfg_attr(docsrs, doc(cfg(feature = "slog-support")))]
523pub fn slog_drain() -> TuiSlogDrain {
524    TuiSlogDrain
525}
526
527#[cfg(feature = "tracing-support")]
528#[cfg_attr(docsrs, doc(cfg(feature = "tracing-support")))]
529pub fn tracing_subscriber_layer() -> TuiTracingSubscriberLayer {
530    TuiTracingSubscriberLayer
531}
532
533/// Set the depth of the hot buffer in order to avoid message loss.
534/// This is effective only after a call to move_events()
535pub fn set_hot_buffer_depth(depth: usize) {
536    TUI_LOGGER.inner.lock().hot_depth = depth;
537}
538
539/// Set the depth of the circular buffer in order to avoid message loss.
540/// This will delete all existing messages in the circular buffer.
541pub fn set_buffer_depth(depth: usize) {
542    TUI_LOGGER.inner.lock().events = CircularBuffer::new(depth);
543}
544
545/// Define filename and log formmating options for file dumping.
546pub fn set_log_file(file_options: TuiLoggerFile) {
547    TUI_LOGGER.inner.lock().dump = Some(file_options);
548}
549
550/// Set default levelfilter for unknown targets of the logger
551pub fn set_default_level(levelfilter: LevelFilter) {
552    TUI_LOGGER.hot_select.lock().default = levelfilter;
553    TUI_LOGGER.inner.lock().default = levelfilter;
554}
555
556/// Set levelfilter for a specific target in the logger
557pub fn set_level_for_target(target: &str, levelfilter: LevelFilter) {
558    let h = fxhash::hash64(&target);
559    TUI_LOGGER.inner.lock().targets.set(target, levelfilter);
560    let mut hs = TUI_LOGGER.hot_select.lock();
561    hs.hashtable.insert(h, levelfilter);
562}
563
564impl TuiLogger {
565    fn raw_log(&self, record: &Record) {
566        let log_entry = ExtLogRecord {
567            timestamp: chrono::Local::now(),
568            level: record.level(),
569            target: record.target().to_string(),
570            file: record.file().unwrap_or("?").to_string(),
571            line: record.line().unwrap_or(0),
572            msg: format!("{}", record.args()),
573        };
574        let mut events_lock = self.hot_log.lock();
575        events_lock.events.push(log_entry);
576        let need_signal =
577            (events_lock.events.total_elements() % (events_lock.events.capacity() / 2)) == 0;
578        if need_signal {
579            events_lock
580                .mover_thread
581                .as_ref()
582                .map(|jh| thread::Thread::unpark(jh.thread()));
583        }
584    }
585}
586
587impl Log for TuiLogger {
588    fn enabled(&self, metadata: &Metadata) -> bool {
589        let h = fxhash::hash64(metadata.target());
590        let hs = self.hot_select.lock();
591        if let Some(&levelfilter) = hs.hashtable.get(&h) {
592            metadata.level() <= levelfilter
593        } else {
594            metadata.level() <= hs.default
595        }
596    }
597
598    fn log(&self, record: &Record) {
599        if self.enabled(record.metadata()) {
600            self.raw_log(record)
601        }
602    }
603
604    fn flush(&self) {}
605}
606
607/// A simple `Drain` to log any event directly.
608#[derive(Default)]
609pub struct Drain;
610
611impl Drain {
612    /// Create a new Drain
613    pub fn new() -> Self {
614        Drain
615    }
616    /// Log the given record to the main tui-logger
617    pub fn log(&self, record: &Record) {
618        TUI_LOGGER.raw_log(record)
619    }
620}
621
622#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
623pub enum TuiWidgetEvent {
624    SpaceKey,
625    UpKey,
626    DownKey,
627    LeftKey,
628    RightKey,
629    PlusKey,
630    MinusKey,
631    HideKey,
632    FocusKey,
633    PrevPageKey,
634    NextPageKey,
635    EscapeKey,
636}
637
638/// This is the definition for the TuiLoggerTargetWidget,
639/// which allows configuration of the logger system and selection of log messages.
640pub struct TuiLoggerTargetWidget<'b> {
641    block: Option<Block<'b>>,
642    /// Base style of the widget
643    style: Style,
644    style_show: Style,
645    style_hide: Style,
646    style_off: Option<Style>,
647    highlight_style: Style,
648    state: Arc<Mutex<TuiWidgetInnerState>>,
649    targets: Vec<String>,
650}
651impl<'b> Default for TuiLoggerTargetWidget<'b> {
652    fn default() -> TuiLoggerTargetWidget<'b> {
653        //TUI_LOGGER.move_events();
654        TuiLoggerTargetWidget {
655            block: None,
656            style: Default::default(),
657            style_off: None,
658            style_hide: Style::default(),
659            style_show: Style::default().add_modifier(Modifier::REVERSED),
660            highlight_style: Style::default().add_modifier(Modifier::REVERSED),
661            state: Arc::new(Mutex::new(TuiWidgetInnerState::new())),
662            targets: vec![],
663        }
664    }
665}
666impl<'b> TuiLoggerTargetWidget<'b> {
667    pub fn block(mut self, block: Block<'b>) -> TuiLoggerTargetWidget<'b> {
668        self.block = Some(block);
669        self
670    }
671    fn opt_style(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
672        if let Some(s) = style {
673            self.style = s;
674        }
675        self
676    }
677    fn opt_style_off(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
678        if style.is_some() {
679            self.style_off = style;
680        }
681        self
682    }
683    fn opt_style_hide(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
684        if let Some(s) = style {
685            self.style_hide = s;
686        }
687        self
688    }
689    fn opt_style_show(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
690        if let Some(s) = style {
691            self.style_show = s;
692        }
693        self
694    }
695    fn opt_highlight_style(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
696        if let Some(s) = style {
697            self.highlight_style = s;
698        }
699        self
700    }
701    pub fn style(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
702        self.style = style;
703        self
704    }
705    pub fn style_off(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
706        self.style_off = Some(style);
707        self
708    }
709    pub fn style_hide(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
710        self.style_hide = style;
711        self
712    }
713    pub fn style_show(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
714        self.style_show = style;
715        self
716    }
717    pub fn highlight_style(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
718        self.highlight_style = style;
719        self
720    }
721    fn inner_state(mut self, state: Arc<Mutex<TuiWidgetInnerState>>) -> TuiLoggerTargetWidget<'b> {
722        self.state = state;
723        self
724    }
725    pub fn state(mut self, state: &TuiWidgetState) -> TuiLoggerTargetWidget<'b> {
726        self.state = state.inner.clone();
727        self
728    }
729}
730impl<'b> Widget for TuiLoggerTargetWidget<'b> {
731    fn render(mut self, area: Rect, buf: &mut Buffer) {
732        buf.set_style(area, self.style);
733        let list_area = match self.block.take() {
734            Some(b) => {
735                let inner_area = b.inner(area);
736                b.render(area, buf);
737                inner_area
738            }
739            None => area,
740        };
741        if list_area.width < 8 || list_area.height < 1 {
742            return;
743        }
744
745        let la_left = list_area.left();
746        let la_top = list_area.top();
747        let la_width = list_area.width as usize;
748
749        {
750            let inner = &TUI_LOGGER.inner.lock();
751            let hot_targets = &inner.targets;
752            let mut state = self.state.lock();
753            let hide_off = state.hide_off;
754            let offset = state.offset;
755            let focus_selected = state.focus_selected;
756            {
757                let targets = &mut state.config;
758                targets.merge(hot_targets);
759                self.targets.clear();
760                for (t, levelfilter) in targets.iter() {
761                    if hide_off && levelfilter == &LevelFilter::Off {
762                        continue;
763                    }
764                    self.targets.push(t.clone());
765                }
766                self.targets.sort();
767            }
768            state.nr_items = self.targets.len();
769            if state.selected >= state.nr_items {
770                state.selected = state.nr_items.max(1) - 1;
771            }
772            if state.selected < state.nr_items {
773                state.opt_selected_target = Some(self.targets[state.selected].clone());
774                let t = &self.targets[state.selected];
775                let (more, less) = if let Some(levelfilter) = state.config.get(t) {
776                    advance_levelfilter(levelfilter)
777                } else {
778                    (None, None)
779                };
780                state.opt_selected_visibility_less = less;
781                state.opt_selected_visibility_more = more;
782                let (more, less) = if let Some(levelfilter) = hot_targets.get(t) {
783                    advance_levelfilter(levelfilter)
784                } else {
785                    (None, None)
786                };
787                state.opt_selected_recording_less = less;
788                state.opt_selected_recording_more = more;
789            }
790            let list_height = (list_area.height as usize).min(self.targets.len());
791            let offset = if list_height > self.targets.len() {
792                0
793            } else if state.selected < state.nr_items {
794                let sel = state.selected;
795                if sel >= offset + list_height {
796                    // selected is below visible list range => make it the bottom
797                    sel - list_height + 1
798                } else if sel.min(offset) + list_height > self.targets.len() {
799                    self.targets.len() - list_height
800                } else {
801                    sel.min(offset)
802                }
803            } else {
804                0
805            };
806            state.offset = offset;
807
808            let targets = &(&state.config);
809            let default_level = inner.default;
810            for i in 0..list_height {
811                let t = &self.targets[i + offset];
812                // Comment in relation to issue #69:
813                // Widgets maintain their own list of level filters per target.
814                // These lists are not forwarded to the TUI_LOGGER, but kept widget private.
815                // Example: This widget's private list contains a target named "not_yet",
816                // and the application hasn't logged an entry with target "not_yet".
817                // If displaying the target list, then "not_yet" will be only present in target,
818                // but not in hot_targets. In issue #69 the problem has been, that
819                // `hot_targets.get(t).unwrap()` has caused a panic. Which is to be expected.
820                // The remedy is to use unwrap_or with default_level.
821                let hot_level_filter = hot_targets.get(t).unwrap_or(default_level);
822                let level_filter = targets.get(t).unwrap_or(default_level);
823                for (j, sym, lev) in &[
824                    (0, "E", Level::Error),
825                    (1, "W", Level::Warn),
826                    (2, "I", Level::Info),
827                    (3, "D", Level::Debug),
828                    (4, "T", Level::Trace),
829                ] {
830                    if let Some(cell) = buf.cell_mut((la_left + j, la_top + i as u16)) {
831                        let cell_style = if hot_level_filter >= *lev {
832                            if level_filter >= *lev {
833                                if !focus_selected || i + offset == state.selected {
834                                    self.style_show
835                                } else {
836                                    self.style_hide
837                                }
838                            } else {
839                                self.style_hide
840                            }
841                        } else if let Some(style_off) = self.style_off {
842                            style_off
843                        } else {
844                            cell.set_symbol(" ");
845                            continue;
846                        };
847                        cell.set_style(cell_style);
848                        cell.set_symbol(sym);
849                    }
850                }
851                buf.set_stringn(la_left + 5, la_top + i as u16, ":", la_width, self.style);
852                buf.set_stringn(
853                    la_left + 6,
854                    la_top + i as u16,
855                    t,
856                    la_width,
857                    if i + offset == state.selected {
858                        self.highlight_style
859                    } else {
860                        self.style
861                    },
862                );
863            }
864        }
865    }
866}
867
868/// The TuiLoggerWidget shows the logging messages in an endless scrolling view.
869/// It is controlled by a TuiWidgetState for selected events.
870#[derive(Debug, Clone, Copy, PartialEq, Hash)]
871pub enum TuiLoggerLevelOutput {
872    Abbreviated,
873    Long,
874}