duat_utils/widgets/
log_book.rs

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