duat_utils/widgets/
log_book.rs

1use std::marker::PhantomData;
2
3use duat_core::{
4    context::{Logs, Record},
5    prelude::*,
6    ui::Side,
7};
8
9use crate::modes::Pager;
10
11/// A [`Widget`] to display [`Logs`] sent to Duat
12pub struct LogBook {
13    logs: Logs,
14    len_of_taken: usize,
15    text: Text,
16    format_rec: Box<dyn FnMut(Record) -> Option<Text>>,
17    close_on_unfocus: bool,
18}
19
20impl<U: Ui> Widget<U> for LogBook {
21    type Cfg = LogBookCfg<U>;
22
23    fn cfg() -> Self::Cfg {
24        LogBookCfg {
25            format_rec: Box::new(|rec| {
26                use context::Level::*;
27                let mut builder = match rec.level() {
28                    Error => txt!("[log_book.error][[ERROR]][log_book.colon]: "),
29                    Warn => txt!("[log_book.warn][[WARNING]][log_book.colon]: "),
30                    Info => txt!("[log_book.info][[INFO]][log_book.colon]: "),
31                    Debug => txt!("[log_book.debug][[DEBUG]][log_book.colon]: "),
32                    Trace => unreachable!("Trace is not meant to be useable"),
33                };
34
35                builder.push(txt!(
36                    "[log_book.bracket]([log_book.target]{}[log_book.bracket])[] {}",
37                    rec.target(),
38                    rec.text().clone()
39                ));
40
41                Some(builder.build())
42            }),
43            close_on_unfocus: true,
44            hidden: true,
45            side: Side::Below,
46            _ghost: PhantomData,
47        }
48    }
49
50    fn update(pa: &mut Pass, handle: Handle<Self, U>)
51    where
52        Self: Sized,
53    {
54        handle.write(pa, |lb, area| {
55            let Some(new_records) = lb.logs.get(lb.len_of_taken..) else {
56                return;
57            };
58
59            let records_were_added = !new_records.is_empty();
60            lb.len_of_taken += new_records.len();
61
62            for rec_text in new_records.into_iter().filter_map(&mut lb.format_rec) {
63                lb.text.insert_text(lb.text.len(), rec_text);
64            }
65
66            if records_were_added {
67                area.scroll_to_points(&lb.text, lb.text.len(), Widget::<U>::print_cfg(lb));
68            }
69        });
70    }
71
72    fn needs_update(&self) -> bool {
73        self.logs.has_changed()
74    }
75
76    fn text(&self) -> &Text {
77        &self.text
78    }
79
80    fn text_mut(&mut self) -> &mut Text {
81        &mut self.text
82    }
83
84    fn once() -> Result<(), Text> {
85        form::set_weak("log_book.error", "default.error");
86        form::set_weak("log_book.warn", "default.warn");
87        form::set_weak("log_book.info", "default.info");
88        form::set_weak("log_book.debug", "default.debug");
89        form::set_weak("log_book.colon", "prompt.colon");
90        form::set_weak("log_book.bracket", "punctuation.bracket");
91        form::set_weak("log_book.target", "module");
92
93        cmd::add!("logs", |pa| {
94            mode::set(Pager::<LogBook, U>::new());
95            Ok(None)
96        })
97        .unwrap();
98
99        Ok(())
100    }
101
102    fn print_cfg(&self) -> PrintCfg {
103        PrintCfg::new().edge_wrapped().with_scrolloff(0, 0)
104    }
105
106    fn on_focus(_: &mut Pass, handle: Handle<Self, U>) {
107        handle.area().reveal().unwrap();
108    }
109
110    fn on_unfocus(pa: &mut Pass, handle: Handle<Self, U>) {
111        handle.read(pa, |lb, area| {
112            if lb.close_on_unfocus {
113                area.hide().unwrap()
114            }
115        });
116    }
117}
118
119/// [`WidgetCfg`] for the [`LogBook`]
120pub struct LogBookCfg<U> {
121    format_rec: Box<dyn FnMut(Record) -> Option<Text>>,
122    close_on_unfocus: bool,
123    hidden: bool,
124    side: Side,
125    _ghost: PhantomData<U>,
126}
127
128impl<U> LogBookCfg<U> {
129    /// Have the [`LogBook`] be open by default
130    pub fn open_by_default(self) -> Self {
131        Self { hidden: false, ..self }
132    }
133
134    /// Keeps the [`LogBook`] open when unfocused, as opposed to
135    /// hiding it
136    pub fn keep_open_on_unfocus(self) -> Self {
137        Self { close_on_unfocus: false, ..self }
138    }
139
140    /// Changes the way [`Record`]s are formatted by the [`LogBook`]
141    ///
142    /// This function returns an [`Option<Text>`], which means you can
143    /// filter out unnecessary [`Record`]s. By default, all valid
144    /// [`Record`]s (those with level [`Debug`] or higher.
145    ///
146    /// [`Debug`]: context::Level::Debug
147    pub fn formatted(self, format_rec: impl FnMut(Record) -> Option<Text> + 'static) -> Self {
148        Self { format_rec: Box::new(format_rec), ..self }
149    }
150
151    /// Pushes the [`LogBook`] to the right, as opposed to below
152    pub fn on_the_right(self) -> Self {
153        Self { side: Side::Right, ..self }
154    }
155
156    /// Pushes the [`LogBook`] to the left, as opposed to below
157    pub fn on_the_left(self) -> Self {
158        Self { side: Side::Left, ..self }
159    }
160
161    /// Pushes the [`LogBook`] above, as opposed to below
162    pub fn above(self) -> Self {
163        Self { side: Side::Above, ..self }
164    }
165}
166
167impl<U: Ui> WidgetCfg<U> for LogBookCfg<U> {
168    type Widget = LogBook;
169
170    fn build(mut self, _: &mut Pass, _: Option<FileHandle<U>>) -> (Self::Widget, PushSpecs) {
171        let logs = context::logs();
172
173        let mut text = Text::new();
174
175        let records = logs.get(..).unwrap();
176        let len_of_taken = records.len();
177        for rec_text in records.into_iter().filter_map(&mut self.format_rec) {
178            text.insert_text(text.len(), rec_text);
179        }
180
181        let lb = LogBook {
182            logs,
183            len_of_taken,
184            text,
185            format_rec: self.format_rec,
186            close_on_unfocus: self.close_on_unfocus,
187        };
188
189        let specs = match self.side {
190            Side::Right => PushSpecs::right().with_hor_len(30.0),
191            Side::Left => PushSpecs::left().with_hor_len(30.0),
192            Side::Above => PushSpecs::above().with_ver_len(10.0),
193            Side::Below => PushSpecs::below().with_ver_len(10.0),
194        };
195
196        (lb, if self.hidden { specs.hidden() } else { specs })
197    }
198}