duat_base/widgets/
log_book.rs

1//! Widget to display the history of notifications sent to Duat
2//!
3//! This widget is kind of an extended version of the
4//! [`Notifications`] widget. It is normally placed at the bottom of
5//! the screen, but you can also place it somewhere else. The messages
6//! can be formatted differently, and you can also filter the things
7//! that you don't care about.
8//!
9//! [`Notifications`]: super::Notifications
10use duat_core::{
11    context::{self, Handle, Logs, Record},
12    data::Pass,
13    opts::PrintOpts,
14    text::{Spacer, Text, txt},
15    ui::{PushSpecs, PushTarget, Side, Widget},
16};
17
18/// A [`Widget`] to display [`Logs`] sent to Duat
19pub struct LogBook {
20    logs: Logs,
21    len_of_taken: usize,
22    text: Text,
23    fmt: Box<dyn FnMut(Record) -> Option<Text> + Send>,
24    has_updated_once: bool,
25    /// Wether to close this [`Widget`] after unfocusing, `true` by
26    /// default
27    pub close_on_unfocus: bool,
28}
29
30impl LogBook {
31    /// Reformats this `LogBook`
32    pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
33        self.fmt = Box::new(fmt)
34    }
35
36    /// Returns a [`LogBookOpts`], so you can push `LogBook`s around
37    pub fn builder() -> LogBookOpts {
38        LogBookOpts::default()
39    }
40}
41
42impl Widget for LogBook {
43    fn update(pa: &mut Pass, handle: &Handle<Self>) {
44        let (lb, area) = handle.write_with_area(pa);
45
46        let Some(new_records) = lb.logs.get(lb.len_of_taken..) else {
47            return;
48        };
49
50        let records_were_added = !new_records.is_empty();
51        lb.len_of_taken += new_records.len();
52
53        for rec_text in new_records.into_iter().filter_map(&mut lb.fmt) {
54            lb.text.insert_text(lb.text.len(), rec_text);
55        }
56
57        if !lb.has_updated_once {
58            area.scroll_ver(&lb.text, i32::MAX, lb.get_print_opts());
59            lb.has_updated_once = true;
60        } else if records_were_added {
61            area.scroll_ver(&lb.text, i32::MAX, lb.get_print_opts());
62        }
63    }
64
65    fn needs_update(&self, _: &Pass) -> bool {
66        self.logs.has_changed()
67    }
68
69    fn text(&self) -> &Text {
70        &self.text
71    }
72
73    fn text_mut(&mut self) -> &mut Text {
74        &mut self.text
75    }
76
77    fn get_print_opts(&self) -> PrintOpts {
78        let mut opts = PrintOpts::new();
79        opts.wrap_lines = true;
80        opts.wrap_on_word = true;
81        opts
82    }
83
84    fn on_focus(pa: &mut Pass, handle: &Handle<Self>) {
85        handle.area().reveal(pa).unwrap();
86    }
87
88    fn on_unfocus(pa: &mut Pass, handle: &Handle<Self>) {
89        if handle.read(pa).close_on_unfocus {
90            handle.area().hide(pa).unwrap()
91        }
92    }
93}
94
95/// Configuration for the [`LogBook`]
96pub struct LogBookOpts {
97    fmt: Box<dyn FnMut(Record) -> Option<Text> + Send>,
98    /// Wether to close the `LogBook` when unfocusing
99    pub close_on_unfocus: bool = true,
100    /// Wether to hide the `LogBook` by default
101    pub hidden: bool = true,
102    /// To which side to push the [`LogBook`] to
103    pub side: Side = Side::Below,
104    /// Requested height for the [`LogBook`], ignored if pushing horizontally
105    pub height: f32 = 8.0,
106    /// Requested width for the [`LogBook`], ignored if pushing vertically
107    pub width: f32 = 50.0,
108}
109
110impl LogBookOpts {
111    /// Push a [`LogBook`] around the given [`PushTarget`]
112    pub fn push_on(mut self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<LogBook> {
113        let logs = context::logs();
114
115        let mut text = Text::new();
116
117        let records = logs.get(..).unwrap();
118        let len_of_taken = records.len();
119        for rec_text in records.into_iter().filter_map(&mut self.fmt) {
120            text.insert_text(text.len(), rec_text);
121        }
122
123        let log_book = LogBook {
124            logs,
125            len_of_taken,
126            text,
127            has_updated_once: false,
128            fmt: self.fmt,
129            close_on_unfocus: self.close_on_unfocus,
130        };
131        let specs = match self.side {
132            Side::Right | Side::Left => PushSpecs {
133                side: self.side,
134                width: Some(self.width),
135                hidden: self.hidden,
136                ..
137            },
138            Side::Above | Side::Below => PushSpecs {
139                side: self.side,
140                height: Some(self.height),
141                hidden: self.hidden,
142                ..
143            },
144        };
145
146        push_target.push_outer(pa, log_book, specs)
147    }
148
149    /// Changes the way [`Record`]s are formatted by the [`LogBook`]
150    ///
151    /// This function returns an [`Option<Text>`], which means you can
152    /// filter out unnecessary [`Record`]s. By default, all valid
153    /// [`Record`]s (those with level [`Debug`] or higher.
154    ///
155    /// [`Debug`]: context::Level::Debug
156    pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
157        self.fmt = Box::new(fmt);
158    }
159}
160
161impl Default for LogBookOpts {
162    fn default() -> Self {
163        fn default_fmt(rec: Record) -> Option<Text> {
164            use duat_core::context::Level::*;
165            let mut builder = Text::builder();
166
167            match rec.level() {
168                Error => builder.push(txt!("[log_book.error][[ERROR]][log_book.colon]:  ")),
169                Warn => builder.push(txt!("[log_book.warn][[WARNING]][log_book.colon]:")),
170                Info => builder.push(txt!("[log_book.info][[INFO]][log_book.colon]:   ")),
171                Debug => builder.push(txt!("[log_book.debug][[DEBUG]][log_book.colon]:  ")),
172                Trace => unreachable!("Trace is not meant to be useable"),
173            };
174
175            builder.push(txt!(
176                "[log_book.bracket][] {}{Spacer}([log_book.location]{}[log_book.bracket])\n",
177                rec.text().clone(),
178                rec.location(),
179            ));
180
181            Some(builder.build())
182        }
183
184        Self { fmt: Box::new(default_fmt), .. }
185    }
186}