cursive_flexi_logger_view/
lib.rs

1//! # A `FlexiLoggerView` for cursive
2//!
3//! This crate provides a new debug view for
4//! [gyscos/cursive](https://github.com/gyscos/cursive) using the
5//! [emabee/flexi_logger](https://github.com/emabee/flexi_logger) crate. This
6//! enables the `FlexiLoggerView` to respect the `RUST_LOG` environment variable
7//! as well as the `flexi_logger` configuration file. Have a look at the `demo`
8//! below to see how it looks.
9//!
10//! ## Using the `FlexiLoggerView`
11//!
12//! To create a `FlexiLoggerView` you first have to register the
13//! `cursive_flexi_logger` as a `LogTarget` in `flexi_logger`. After the
14//! `flexi_logger` has started, you may create a `FlexiLoggerView` instance and
15//! add it to cursive.
16//!
17//! ```rust
18//! use cursive::{Cursive, CursiveExt};
19//! use cursive_flexi_logger_view::FlexiLoggerView;
20//! use flexi_logger::Logger;
21//!
22//! fn main() {
23//!     // we need to initialize cursive first, as the cursive-flexi-logger
24//!     // needs a cursive callback sink to notify cursive about screen refreshs
25//!     // when a new log message arrives
26//!     let mut siv = Cursive::default();
27//!
28//!     Logger::try_with_env_or_str("trace")
29//!         .expect("Could not create Logger from environment :(")
30//!         .log_to_file_and_writer(
31//!            flexi_logger::FileSpec::default()
32//!                 .directory("logs")
33//!                 .suppress_timestamp(),
34//!             cursive_flexi_logger_view::cursive_flexi_logger(&siv)
35//!         )
36//!         .format(flexi_logger::colored_with_thread)
37//!         .start()
38//!         .expect("failed to initialize logger!");
39//!
40//!     siv.add_layer(FlexiLoggerView::scrollable()); // omit `scrollable` to remove scrollbars
41//!
42//!     log::info!("test log message");
43//!     // siv.run();
44//! }
45//! ```
46//!
47//! Look into the `FlexiLoggerView` documentation for a detailed explanation.
48//!
49//! ## Add toggleable flexi_logger debug console view
50//!
51//! This crate also provide utility functions, which is simplify usage of `FlexiLoggerView`, providing
52//! debug console view like [`Cursive::toggle_debug_console`](/cursive/latest/cursive/struct.Cursive.html#method.toggle_debug_console).
53//! There is 3 functions:
54//!
55//!  - `show_flexi_logger_debug_console`: show debug console view;
56//!  - `hide_flexi_logger_debug_console`: hide debug console view (if visible);
57//!  - `toggle_flexi_logger_debug_console`: show the debug console view, or hide it if it's already visible.
58//!
59//! ```rust
60//! use cursive::{Cursive, CursiveExt};
61//! use cursive_flexi_logger_view::{show_flexi_logger_debug_console, hide_flexi_logger_debug_console, toggle_flexi_logger_debug_console};
62//! use flexi_logger::Logger;
63//!
64//! fn main() {
65//!     // we need to initialize cursive first, as the cursive-flexi-logger
66//!     // needs a cursive callback sink to notify cursive about screen refreshs
67//!     // when a new log message arrives
68//!     let mut siv = Cursive::default();
69//!
70//!     Logger::try_with_env_or_str("trace")
71//!         .expect("Could not create Logger from environment :(")
72//!         .log_to_file_and_writer(
73//!            flexi_logger::FileSpec::default()
74//!                 .directory("logs")
75//!                 .suppress_timestamp(),
76//!             cursive_flexi_logger_view::cursive_flexi_logger(&siv)
77//!         )
78//!         .format(flexi_logger::colored_with_thread)
79//!         .start()
80//!         .expect("failed to initialize logger!");
81//!
82//!     siv.add_global_callback('~', toggle_flexi_logger_debug_console);  // Bind '~' key to show/hide debug console view
83//!     siv.add_global_callback('s', show_flexi_logger_debug_console);  // Bind 's' key to show debug console view
84//!     siv.add_global_callback('h', hide_flexi_logger_debug_console);  // Bind 'h' key to hide debug console view
85//!
86//!     log::info!("test log message");
87//!     // siv.run();
88//! }
89//! ```
90
91use arraydeque::{ArrayDeque, Wrapping};
92use cursive_core::theme::{BaseColor, Color};
93use cursive_core::utils::markup::StyledString;
94use cursive_core::view::{Nameable, ScrollStrategy, Scrollable, View};
95use cursive_core::views::{Dialog, ScrollView};
96use cursive_core::{CbSink, Cursive, Printer, Vec2};
97use flexi_logger::{writers::LogWriter, DeferredNow, Level, Record};
98use unicode_width::UnicodeWidthStr;
99
100use std::sync::{Arc, Mutex};
101use std::thread;
102
103type LogBuffer = ArrayDeque<[StyledString; 2048], Wrapping>;
104
105static FLEXI_LOGGER_DEBUG_VIEW_NAME: &str = "_flexi_debug_view";
106
107lazy_static::lazy_static! {
108    static ref LOGS: Arc<Mutex<LogBuffer>> = Arc::new(Mutex::new(LogBuffer::new()));
109}
110
111/// The `FlexiLoggerView` displays log messages from the `cursive_flexi_logger` log target.
112/// It is safe to create multiple instances of this struct.
113///
114/// # Create a plain `FlexiLoggerView`
115///
116/// ```rust
117/// use cursive::{Cursive, CursiveExt};
118/// use cursive_flexi_logger_view::FlexiLoggerView;
119/// use flexi_logger::Logger;
120///
121/// fn main() {
122///     // we need to initialize cursive first, as the cursive-flexi-logger
123///     // needs a cursive callback sink to notify cursive about screen refreshs
124///     // when a new log message arrives
125///     let mut siv = Cursive::default();
126///
127///     Logger::try_with_env_or_str("trace")
128///         .expect("Could not create Logger from environment :(")
129///         .log_to_file_and_writer(
130///            flexi_logger::FileSpec::default()
131///                 .directory("logs")
132///                 .suppress_timestamp(),
133///             cursive_flexi_logger_view::cursive_flexi_logger(&siv)
134///         )
135///         .format(flexi_logger::colored_with_thread)
136///         .start()
137///         .expect("failed to initialize logger!");
138///
139///     siv.add_layer(FlexiLoggerView::new()); // add a plain flexi-logger view
140///
141///     log::info!("test log message");
142///     // siv.run();
143/// }
144/// ```
145///
146/// # Create a scrollable `FlexiLoggerView`
147///
148/// ```rust
149/// use cursive::{Cursive, CursiveExt};
150/// use cursive_flexi_logger_view::FlexiLoggerView;
151/// use flexi_logger::Logger;
152///
153/// fn main() {
154///     // we need to initialize cursive first, as the cursive-flexi-logger
155///     // needs a cursive callback sink to notify cursive about screen refreshs
156///     // when a new log message arrives
157///     let mut siv = Cursive::default();
158///
159///     Logger::try_with_env_or_str("trace")
160///         .expect("Could not create Logger from environment :(")
161///         .log_to_file_and_writer(
162///            flexi_logger::FileSpec::default()
163///                 .directory("logs")
164///                 .suppress_timestamp(),
165///             cursive_flexi_logger_view::cursive_flexi_logger(&siv)
166///         )
167///         .format(flexi_logger::colored_with_thread)
168///         .start()
169///         .expect("failed to initialize logger!");
170///
171///     siv.add_layer(FlexiLoggerView::scrollable()); // add a scrollable flexi-logger view
172///
173///     log::info!("test log message");
174///     // siv.run();
175/// }
176/// ```
177pub struct FlexiLoggerView {
178    pub indent: bool,
179}
180
181pub trait Indentable {
182    fn no_indent(self) -> Self;
183    fn indent(self) -> Self;
184}
185
186impl FlexiLoggerView {
187    /// Create a new `FlexiLoggerView` which is wrapped in a `ScrollView`.
188    pub fn scrollable() -> ScrollView<Self> {
189        FlexiLoggerView { indent: true }
190            .scrollable()
191            .scroll_x(true)
192            .scroll_y(true)
193            .scroll_strategy(ScrollStrategy::StickToBottom)
194    }
195
196    /// Create a new `FlexiLoggerView`.
197    pub fn new() -> Self {
198        FlexiLoggerView { indent: true }
199    }
200}
201
202impl Indentable for ScrollView<FlexiLoggerView> {
203    /// Changes a `FlexiLoggerView`, which is contained in a `ScrollView`, to not indent messages
204    /// spanning multiple lines.
205    fn no_indent(mut self) -> Self {
206        self.get_inner_mut().indent = false;
207        self
208    }
209
210    /// Changes a `FlexiLoggerView`, which is contained in a `ScrollView`, to indent messages
211    /// spanning multiple lines.
212    fn indent(mut self) -> Self {
213        self.get_inner_mut().indent = true;
214        self
215    }
216}
217
218impl Indentable for FlexiLoggerView {
219    /// Changes a `FlexiLoggerView` to not indent messages spanning multiple lines.
220    fn no_indent(mut self) -> Self {
221        self.indent = false;
222        self
223    }
224
225    /// Changes a `FlexiLoggerView` to indent messages spanning multiple lines.
226    fn indent(mut self) -> Self {
227        self.indent = true;
228        self
229    }
230}
231
232impl View for FlexiLoggerView {
233    fn draw(&self, printer: &Printer<'_, '_>) {
234        let logs = LOGS.lock().unwrap();
235
236        // Only print the last logs, so skip what doesn't fit
237        let skipped = logs.len().saturating_sub(printer.size.y);
238
239        let mut y = 0;
240        for msg in logs.iter().skip(skipped) {
241            let mut x = 0;
242
243            // Assume the log message is the last styled span
244            let log_msg_index = msg.spans_raw().len() - 1;
245
246            for span in msg.spans().take(log_msg_index) {
247                printer.with_style(*span.attr, |printer| {
248                    printer.print((x, y), span.content);
249                });
250                x += span.width;
251            }
252
253            let log_msg = msg.spans().skip(log_msg_index).next().unwrap();
254            for part in log_msg.content.split('\n') {
255                printer.with_style(*log_msg.attr, |printer| {
256                    printer.print((x, y), part);
257                });
258                y += 1;
259                if !self.indent {
260                    x = 0;
261                }
262                // x is not modified ⇒ multiline messages look like this:
263                // DEBUG <src/main.rs:47> first line
264                //                        second line
265            }
266        }
267    }
268
269    fn required_size(&mut self, constraint: Vec2) -> Vec2 {
270        let logs = LOGS.lock().unwrap();
271
272        // The longest line sets the width
273        let w = logs
274            .iter()
275            .map(|msg| {
276                msg.spans()
277                    .map(|x|
278                    // if the log message contains more than one line,
279                    // only the longest line should be considered
280                    // (definitely not the total content.len())
281                    x.content.split('\n').map(|x| x.width()).max().unwrap())
282                    .sum::<usize>()
283            })
284            .max()
285            .unwrap_or(1);
286        let h = logs
287            .iter()
288            .map(|msg| {
289                msg.spans()
290                    .last()
291                    .map(|x| x.content.split('\n').count())
292                    .unwrap()
293            })
294            .sum::<usize>();
295        let w = std::cmp::max(w, constraint.x);
296        let h = std::cmp::max(h, constraint.y);
297
298        Vec2::new(w, h)
299    }
300}
301
302/// The `flexi_logger` `LogWriter` implementation for the `FlexiLoggerView`.
303///
304/// Use the `cursive_flexi_logger` function to create an instance of this struct.
305pub struct CursiveLogWriter {
306    sink: CbSink,
307}
308
309/// Creates a new `LogWriter` instance for the `FlexiLoggerView`. Use this to
310/// register a cursive log writer in `flexi_logger`.
311///
312/// Although, it is safe to create multiple cursive log writers, it may not be
313/// what you want. Each instance of a cursive log writer replicates the log
314/// messages in to `FlexiLoggerView`. When registering multiple cursive log
315/// writer instances, a single log messages will be duplicated by each log
316/// writer.
317///
318/// # Registering the cursive log writer in `flexi_logger`
319///
320/// ```rust
321/// use cursive::{Cursive, CursiveExt};
322/// use flexi_logger::Logger;
323///
324/// fn main() {
325///     // we need to initialize cursive first, as the cursive-flexi-logger
326///     // needs a cursive callback sink to notify cursive about screen refreshs
327///     // when a new log message arrives
328///     let mut siv = Cursive::default();
329///
330///     Logger::try_with_env_or_str("trace")
331///         .expect("Could not create Logger from environment :(")
332///         .log_to_file_and_writer(
333///            flexi_logger::FileSpec::default()
334///                 .directory("logs")
335///                 .suppress_timestamp(),
336///             cursive_flexi_logger_view::cursive_flexi_logger(&siv)
337///         )
338///         .format(flexi_logger::colored_with_thread)
339///         .start()
340///         .expect("failed to initialize logger!");
341/// }
342/// ```
343pub fn cursive_flexi_logger(siv: &Cursive) -> Box<CursiveLogWriter> {
344    Box::new(CursiveLogWriter {
345        sink: siv.cb_sink().clone(),
346    })
347}
348
349use time::{format_description::FormatItem, macros::format_description};
350
351const FORMAT: &[FormatItem<'static>] = format_description!("%T%.3f");
352
353impl LogWriter for CursiveLogWriter {
354    fn write(&self, now: &mut DeferredNow, record: &Record) -> std::io::Result<()> {
355        let color = Color::Dark(match record.level() {
356            Level::Trace => BaseColor::Green,
357            Level::Debug => BaseColor::Cyan,
358            Level::Info => BaseColor::Blue,
359            Level::Warn => BaseColor::Yellow,
360            Level::Error => BaseColor::Red,
361        });
362
363        let mut line = StyledString::new();
364        line.append_styled(format!("{}", now.format(FORMAT)), color);
365        line.append_plain(format!(
366            " [{}] ",
367            thread::current().name().unwrap_or("(unnamed)"),
368        ));
369        line.append_styled(format!("{}", record.level()), color);
370        line.append_plain(format!(
371            " <{}:{}> ",
372            record.file().unwrap_or("(unnamed)"),
373            record.line().unwrap_or(0),
374        ));
375        line.append_styled(format!("{}", &record.args()), color);
376
377        LOGS.lock().unwrap().push_back(line);
378        self.sink.send(Box::new(|_siv| {})).map_err(|_| {
379            std::io::Error::new(
380                std::io::ErrorKind::BrokenPipe,
381                "cursive callback sink is closed!",
382            )
383        })
384    }
385
386    fn flush(&self) -> std::io::Result<()> {
387        // we are not buffering
388        Ok(())
389    }
390
391    fn max_log_level(&self) -> log::LevelFilter {
392        log::LevelFilter::max()
393    }
394}
395
396/// Show the flexi_logger debug console.
397///
398/// This is analog to [`Cursive::show_debug_console`](/cursive/latest/cursive/struct.Cursive.html#method.show_debug_console).
399///
400/// # Add binding to show flexi_logger debug view
401///
402/// ```rust
403/// use cursive::{Cursive, CursiveExt};
404/// use cursive_flexi_logger_view::show_flexi_logger_debug_console;
405/// use flexi_logger::Logger;
406///
407/// fn main() {
408///     // we need to initialize cursive first, as the cursive-flexi-logger
409///     // needs a cursive callback sink to notify cursive about screen refreshs
410///     // when a new log message arrives
411///     let mut siv = Cursive::default();
412///
413///     Logger::try_with_env_or_str("trace")
414///         .expect("Could not create Logger from environment :(")
415///         .log_to_file_and_writer(
416///            flexi_logger::FileSpec::default()
417///                 .directory("logs")
418///                 .suppress_timestamp(),
419///             cursive_flexi_logger_view::cursive_flexi_logger(&siv)
420///         )
421///         .format(flexi_logger::colored_with_thread)
422///         .start()
423///         .expect("failed to initialize logger!");
424///
425///     siv.add_global_callback('~', show_flexi_logger_debug_console);  // Add binding to show flexi_logger debug view
426///
427///     // siv.run();
428/// }
429/// ```
430pub fn show_flexi_logger_debug_console(siv: &mut Cursive) {
431    siv.add_layer(
432        Dialog::around(FlexiLoggerView::scrollable().with_name(FLEXI_LOGGER_DEBUG_VIEW_NAME))
433            .title("Debug console"),
434    );
435}
436
437/// Hide the flexi_logger debug console (if visible).
438///
439/// # Add binding to hide flexi_logger debug view
440///
441/// ```rust
442/// use cursive::{Cursive, CursiveExt};
443/// use cursive_flexi_logger_view::hide_flexi_logger_debug_console;
444/// use flexi_logger::Logger;
445///
446/// fn main() {
447///     // we need to initialize cursive first, as the cursive-flexi-logger
448///     // needs a cursive callback sink to notify cursive about screen refreshs
449///     // when a new log message arrives
450///     let mut siv = Cursive::default();
451///
452///     Logger::try_with_env_or_str("trace")
453///         .expect("Could not create Logger from environment :(")
454///         .log_to_file_and_writer(
455///            flexi_logger::FileSpec::default()
456///                 .directory("logs")
457///                 .suppress_timestamp(),
458///             cursive_flexi_logger_view::cursive_flexi_logger(&siv)
459///         )
460///         .format(flexi_logger::colored_with_thread)
461///         .start()
462///         .expect("failed to initialize logger!");
463///
464///     siv.add_global_callback('~', hide_flexi_logger_debug_console);  // Add binding to hide flexi_logger debug view
465///
466///     // siv.run();
467/// }
468/// ```
469pub fn hide_flexi_logger_debug_console(siv: &mut Cursive) {
470    if let Some(pos) = siv
471        .screen_mut()
472        .find_layer_from_name(FLEXI_LOGGER_DEBUG_VIEW_NAME)
473    {
474        siv.screen_mut().remove_layer(pos);
475    }
476}
477
478/// Show the flexi_logger debug console, or hide it if it's already visible.
479///
480/// This is analog to [`Cursive::toggle_debug_console`](/cursive/latest/cursive/struct.Cursive.html#method.toggle_debug_console).
481///
482/// # Enable toggleable flexi_logger debug view
483///
484/// ```rust
485/// use cursive::{Cursive, CursiveExt};
486/// use cursive_flexi_logger_view::toggle_flexi_logger_debug_console;
487/// use flexi_logger::Logger;
488///
489/// fn main() {
490///     // we need to initialize cursive first, as the cursive-flexi-logger
491///     // needs a cursive callback sink to notify cursive about screen refreshs
492///     // when a new log message arrives
493///     let mut siv = Cursive::default();
494///
495///     Logger::try_with_env_or_str("trace")
496///         .expect("Could not create Logger from environment :(")
497///         .log_to_file_and_writer(
498///            flexi_logger::FileSpec::default()
499///                 .directory("logs")
500///                 .suppress_timestamp(),
501///             cursive_flexi_logger_view::cursive_flexi_logger(&siv)
502///         )
503///         .format(flexi_logger::colored_with_thread)
504///         .start()
505///         .expect("failed to initialize logger!");
506///
507///     siv.add_global_callback('~', toggle_flexi_logger_debug_console);  // Enable toggleable flexi_logger debug view
508///
509///     // siv.run();
510/// }
511/// ```
512pub fn toggle_flexi_logger_debug_console(siv: &mut Cursive) {
513    if let Some(pos) = siv
514        .screen_mut()
515        .find_layer_from_name(FLEXI_LOGGER_DEBUG_VIEW_NAME)
516    {
517        siv.screen_mut().remove_layer(pos);
518    } else {
519        show_flexi_logger_debug_console(siv);
520    }
521}