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,
100    /// Wether to hide the `LogBook` by default
101    pub hidden: bool,
102    /// To which side to push the [`LogBook`] to
103    pub side: Side,
104    /// Requested height for the [`LogBook`], ignored if pushing
105    /// horizontally
106    pub height: f32,
107    /// Requested width for the [`LogBook`], ignored if pushing
108    /// vertically
109    pub width: f32,
110}
111
112impl LogBookOpts {
113    /// Push a [`LogBook`] around the given [`PushTarget`]
114    pub fn push_on(mut self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<LogBook> {
115        let logs = context::logs();
116
117        let mut text = Text::new();
118
119        let records = logs.get(..).unwrap();
120        let len_of_taken = records.len();
121        for rec_text in records.into_iter().filter_map(&mut self.fmt) {
122            text.insert_text(text.len(), rec_text);
123        }
124
125        let log_book = LogBook {
126            logs,
127            len_of_taken,
128            text,
129            has_updated_once: false,
130            fmt: self.fmt,
131            close_on_unfocus: self.close_on_unfocus,
132        };
133        let specs = match self.side {
134            Side::Right | Side::Left => PushSpecs {
135                side: self.side,
136                width: Some(self.width),
137                hidden: self.hidden,
138                ..Default::default()
139            },
140            Side::Above | Side::Below => PushSpecs {
141                side: self.side,
142                height: Some(self.height),
143                hidden: self.hidden,
144                ..Default::default()
145            },
146        };
147
148        push_target.push_outer(pa, log_book, specs)
149    }
150
151    /// Changes the way [`Record`]s are formatted by the [`LogBook`]
152    ///
153    /// This function returns an [`Option<Text>`], which means you can
154    /// filter out unnecessary [`Record`]s. By default, all valid
155    /// [`Record`]s (those with level [`Debug`] or higher.
156    ///
157    /// [`Debug`]: context::Level::Debug
158    pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
159        self.fmt = Box::new(fmt);
160    }
161}
162
163impl Default for LogBookOpts {
164    fn default() -> Self {
165        fn default_fmt(rec: Record) -> Option<Text> {
166            use duat_core::context::Level::*;
167            let mut builder = Text::builder();
168
169            match rec.level() {
170                Error => builder.push(txt!("[log_book.error][[ERROR]][log_book.colon]:  ")),
171                Warn => builder.push(txt!("[log_book.warn][[WARNING]][log_book.colon]:")),
172                Info => builder.push(txt!("[log_book.info][[INFO]][log_book.colon]:   ")),
173                Debug => builder.push(txt!("[log_book.debug][[DEBUG]][log_book.colon]:  ")),
174                Trace => unreachable!("Trace is not meant to be useable"),
175            };
176
177            builder.push(txt!(
178                "[log_book.bracket][] {}{Spacer}([log_book.location]{}[log_book.bracket])\n",
179                rec.text().clone(),
180                rec.location(),
181            ));
182
183            Some(builder.build())
184        }
185
186        Self {
187            fmt: Box::new(default_fmt),
188            close_on_unfocus: true,
189            hidden: true,
190            side: Side::Below,
191            height: 8.0,
192            width: 50.0,
193        }
194    }
195}