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}