duat_core/context/
log.rs

1use std::sync::{
2    Mutex, OnceLock,
3    atomic::{AtomicUsize, Ordering},
4};
5
6pub use log::{Level, Metadata};
7
8pub use self::macros::*;
9use crate::text::{Selectionless, Text};
10
11mod macros {
12    /// Logs an error to Duat
13    ///
14    /// Use this, as opposed to [`warn!`], [`info!`] or [`debug!`],
15    /// if you want to tell the user that something explicitely
16    /// failed, and they need to find a workaround, like failing
17    /// to write to/read from a file, for example.
18    ///
19    /// This error follows the same construction as the [`txt!`]
20    /// macro, and will create a [`Record`] inside of the [`Logs`],
21    /// which can be accessed by anyone, at any time.
22    ///
23    /// The [`Record`] added to the [`Logs`] is related to
24    /// [`log::Record`], from the [`log`] crate. But it differs in the
25    /// sense that it is always `'static`, and instead of having an
26    /// [`std::fmt::Arguments`] inside, it contains a [`Text`], making
27    /// it a better fit for Duat.
28    ///
29    /// The connection to [`log::Record`] also means that external
30    /// libraries can log information using the [`log`] crate, and it
31    /// will also show up in Duat's [`Logs`]s, but reformatted to be a
32    /// [`Text`] instead.
33    ///
34    /// [`txt!`]: crate::text::txt
35    /// [`Record`]: super::Record
36    /// [`Logs`]: super::Logs
37    /// [`Text`]: crate::text::Text
38    pub macro error {
39        (target: $target:expr, $($arg:tt)+) => ({
40            $crate::private_exports::log!(
41                $target.to_string().leak(),
42                $crate::context::Level::Error,
43                $($arg)+
44            )
45        }),
46        ($($arg:tt)+) => (
47            $crate::private_exports::log!(
48                module_path!(),
49                $crate::context::Level::Error,
50                $($arg)+
51            )
52        )
53    }
54
55    /// Logs an warning to Duat
56    ///
57    /// Use this, as opposed to [`error!`], [`info!`] or [`debug!`],
58    /// if you want to tell the user that something was partially
59    /// successful, or that a failure happened, but
60    /// it's near inconsequential.
61    ///
62    /// This error follows the same construction as the [`txt!`]
63    /// macro, and will create a [`Record`] inside of the [`Logs`],
64    /// which can be accessed by anyone, at any time.
65    ///
66    /// The [`Record`] added to the [`Logs`] is related to
67    /// [`log::Record`], from the [`log`] crate. But it differs in the
68    /// sense that it is always `'static`, and instead of having an
69    /// [`std::fmt::Arguments`] inside, it contains a [`Text`], making
70    /// it a better fit for Duat.
71    ///
72    /// The connection to [`log::Record`] also means that external
73    /// libraries can log information using the [`log`] crate, and it
74    /// will also show up in Duat's [`Logs`]s, but reformatted to be a
75    /// [`Text`] instead.
76    ///
77    /// [`txt!`]: crate::text::txt
78    /// [`Record`]: super::Record
79    /// [`Logs`]: super::Logs
80    /// [`Text`]: crate::text::Text
81    pub macro warn {
82        (target: $target:expr, $($arg:tt)+) => ({
83            $crate::private_exports::log!(
84                $target.to_string().leak(),
85                $crate::context::Level::Warn,
86                $($arg)+
87            )
88        }),
89        ($($arg:tt)+) => (
90            $crate::private_exports::log!(
91                module_path!(),
92                $crate::context::Level::Warn,
93                $($arg)+
94            )
95        )
96    }
97
98    /// Logs an info to Duat
99    ///
100    /// Use this, as opposed to [`error!`], [`warn!`] or [`debug!],
101    /// when you want to tell the user that something was
102    /// successful, and it is important for them to know it was
103    /// successful.
104    ///
105    /// This error follows the same construction as the [`txt!`]
106    /// macro, and will create a [`Record`] inside of the [`Logs`],
107    /// which can be accessed by anyone, at any time.
108    ///
109    /// The [`Record`] added to the [`Logs`] is related to
110    /// [`log::Record`], from the [`log`] crate. But it differs in the
111    /// sense that it is always `'static`, and instead of having an
112    /// [`std::fmt::Arguments`] inside, it contains a [`Text`], making
113    /// it a better fit for Duat.
114    ///
115    /// The connection to [`log::Record`] also means that external
116    /// libraries can log information using the [`log`] crate, and it
117    /// will also show up in Duat's [`Logs`]s, but reformatted to be a
118    /// [`Text`] instead.
119    ///
120    /// [`txt!`]: crate::text::txt
121    /// [`Record`]: super::Record
122    /// [`Logs`]: super::Logs
123    /// [`Text`]: crate::text::Text
124    pub macro info {
125        (target: $target:expr, $($arg:tt)+) => ({
126            $crate::private_exports::log!(
127                $target.to_string().leak(),
128                $crate::context::Level::Info,
129                $($arg)+
130            )
131        }),
132        ($($arg:tt)+) => (
133            $crate::private_exports::log!(
134                module_path!(),
135                $crate::context::Level::Info,
136                $($arg)+
137            )
138        )
139    }
140
141    /// Logs an debug information to Duat
142    ///
143    /// Use this, as opposed to [`error!`], [`warn!`] or [`info!`],
144    /// when you want to tell the user that something was
145    /// successful, but it is not that important, or the success is
146    /// only a smaller part of some bigger operation, or the success
147    /// is part of something that was done "silently".
148    ///
149    /// This error follows the same construction as the [`txt!`]
150    /// macro, and will create a [`Record`] inside of the [`Logs`],
151    /// which can be accessed by anyone, at any time.
152    ///
153    /// The [`Record`] added to the [`Logs`] is related to
154    /// [`log::Record`], from the [`log`] crate. But it differs in the
155    /// sense that it is always `'static`, and instead of having an
156    /// [`std::fmt::Arguments`] inside, it contains a [`Text`], making
157    /// it a better fit for Duat.
158    ///
159    /// The connection to [`log::Record`] also means that external
160    /// libraries can log information using the [`log`] crate, and it
161    /// will also show up in Duat's [`Logs`]s, but reformatted to be a
162    /// [`Text`] instead.
163    ///
164    /// [`txt!`]: crate::text::txt
165    /// [`Record`]: super::Record
166    /// [`Logs`]: super::Logs
167    /// [`Text`]: crate::text::Text
168    pub macro debug {
169        (target: $target:expr, $($arg:tt)+) => ({
170            $crate::private_exports::log!(
171                $target.to_string().leak(),
172                $crate::context::Level::Debug,
173                $($arg)+
174            )
175        }),
176        ($($arg:tt)+) => (
177            $crate::private_exports::log!(
178                module_path!(),
179                $crate::context::Level::Debug,
180                $($arg)+
181            )
182        )
183    }
184}
185
186static LOGS: OnceLock<Logs> = OnceLock::new();
187
188/// Notifications for duat
189///
190/// This is a mutable, shareable, [`Send`]/[`Sync`] list of
191/// notifications in the form of [`Text`]s, you can read this,
192/// send new notifications, and check for updates, just like with
193/// [`RwData`]s and [`Handle`]s.
194///
195/// [`RwData`]: crate::data::RwData
196/// [`Handle`]: super::Handle
197pub fn logs() -> Logs {
198    LOGS.get().unwrap().clone()
199}
200
201/// The notifications sent to Duat.
202///
203/// This can include command results, failed mappings,
204/// recompilation messages, and any other thing that you want
205/// to notify about. In order to set the level of severity for these
206/// messages, use the [`error!`], [`warn!`] and [`info!`] macros.
207#[derive(Debug)]
208pub struct Logs {
209    list: &'static Mutex<Vec<Record>>,
210    cutoffs: &'static Mutex<Vec<usize>>,
211    cur_state: &'static AtomicUsize,
212    read_state: AtomicUsize,
213}
214
215impl Clone for Logs {
216    fn clone(&self) -> Self {
217        Self {
218            list: self.list,
219            cutoffs: self.cutoffs,
220            cur_state: self.cur_state,
221            read_state: AtomicUsize::new(self.read_state.load(Ordering::Relaxed)),
222        }
223    }
224}
225
226impl Logs {
227    /// Creates a new [`Logs`]
228    #[doc(hidden)]
229    pub fn new() -> Self {
230        Self {
231            list: Box::leak(Box::default()),
232            cutoffs: Box::leak(Box::default()),
233            cur_state: Box::leak(Box::new(AtomicUsize::new(1))),
234            read_state: AtomicUsize::new(0),
235        }
236    }
237
238    /// Returns an owned valued of a [`SliceIndex`]
239    ///
240    /// - `&'static Log` for `usize`;
241    /// - [`Vec<&'static Log>`] for `impl RangeBounds<usize>`;
242    ///
243    /// [`SliceIndex`]: std::slice::SliceIndex
244    pub fn get<I>(&self, i: I) -> Option<<I::Output as ToOwned>::Owned>
245    where
246        I: std::slice::SliceIndex<[Record]>,
247        I::Output: ToOwned,
248    {
249        self.read_state
250            .store(self.cur_state.load(Ordering::Relaxed), Ordering::Relaxed);
251        self.list.lock().unwrap().get(i).map(ToOwned::to_owned)
252    }
253
254    /// Returns the last [`Record`], if there was one
255    pub fn last(&self) -> Option<Record> {
256        self.read_state
257            .store(self.cur_state.load(Ordering::Relaxed), Ordering::Relaxed);
258        self.list.lock().unwrap().last().cloned()
259    }
260
261    /// Wether there are new notifications or not
262    pub fn has_changed(&self) -> bool {
263        self.cur_state.load(Ordering::Relaxed) > self.read_state.load(Ordering::Relaxed)
264    }
265
266    /// Pushes a [`CmdResult`]
267    ///
268    /// [`CmdResult`]: crate::cmd::CmdResult
269    pub(crate) fn push_cmd_result(&self, cmd: String, result: Result<Option<Text>, Text>) {
270        let is_ok = result.is_ok();
271        let (Ok(Some(res)) | Err(res)) = result else {
272            return;
273        };
274
275        self.cur_state.fetch_add(1, Ordering::Relaxed);
276
277        let rec = Record {
278            metadata: log::MetadataBuilder::new()
279                .level(if is_ok { Level::Info } else { Level::Error })
280                .target(cmd.leak())
281                .build(),
282            module_path: None,
283            file: None,
284            line: None,
285            text: Box::leak(Box::new(res.no_selections())),
286        };
287
288        self.list.lock().unwrap().push(rec)
289    }
290
291    /// Pushes a new [`Record`] to Duat
292    #[doc(hidden)]
293    pub fn push_record(&self, rec: Record) {
294        self.cur_state.fetch_add(1, Ordering::Relaxed);
295        self.list.lock().unwrap().push(rec)
296    }
297
298    /// Returns the number of [`Record`]s in the [`Logs`]
299    pub fn len(&self) -> usize {
300        self.list.lock().unwrap().len()
301    }
302
303    /// Wether there are any [`Record`]s in the [`Logs`]
304    ///
305    /// It's pretty much never `true`
306    #[must_use]
307    pub fn is_empty(&self) -> bool {
308        self.len() == 0
309    }
310}
311
312impl log::Log for Logs {
313    fn enabled(&self, metadata: &log::Metadata) -> bool {
314        metadata.level() > log::Level::Debug
315    }
316
317    fn log(&self, rec: &log::Record) {
318        let rec = Record {
319            text: Box::leak(Box::new(
320                Text::from(std::fmt::format(*rec.args())).no_selections(),
321            )),
322            metadata: log::MetadataBuilder::new()
323                .level(rec.level())
324                .target(rec.target().to_string().leak())
325                .build(),
326            module_path: match rec.module_path_static() {
327                Some(module_path) => Some(module_path),
328                None => rec
329                    .module_path()
330                    .map(|mp| -> &str { mp.to_string().leak() }),
331            },
332            file: match rec.file_static() {
333                Some(file) => Some(file),
334                None => rec.file().map(|mp| -> &str { mp.to_string().leak() }),
335            },
336            line: rec.line(),
337        };
338
339        self.cur_state.fetch_add(1, Ordering::Relaxed);
340        self.list.lock().unwrap().push(rec)
341    }
342
343    fn flush(&self) {}
344}
345
346/// A record of something that happened in Duat
347///
348/// Differs from [`log::Record`] in that its argument isn't an
349/// [`std::fmt::Arguments`], but a [`Text`] instead.
350#[derive(Clone, Debug)]
351pub struct Record {
352    text: &'static Selectionless,
353    metadata: log::Metadata<'static>,
354    module_path: Option<&'static str>,
355    file: Option<&'static str>,
356    line: Option<u32>,
357}
358
359impl Record {
360    /// Creates a new [`Record`]
361    #[doc(hidden)]
362    pub fn new(
363        text: Text,
364        level: Level,
365        target: &'static str,
366        module_path: Option<&'static str>,
367        file: Option<&'static str>,
368        line: Option<u32>,
369    ) -> Self {
370        Self {
371            text: Box::leak(Box::new(text.no_selections())),
372            metadata: log::MetadataBuilder::new()
373                .level(level)
374                .target(target)
375                .build(),
376            module_path,
377            file,
378            line,
379        }
380    }
381
382    /// The [`Text`] of this [`Record`]
383    #[inline]
384    pub fn text(&self) -> &Text {
385        self.text
386    }
387
388    /// Metadata about the log directive
389    #[inline]
390    pub fn metadata(&self) -> &log::Metadata<'static> {
391        &self.metadata
392    }
393
394    /// The verbosity level of the message
395    #[inline]
396    pub fn level(&self) -> Level {
397        self.metadata.level()
398    }
399
400    /// The name of the target of the directive
401    #[inline]
402    pub fn target(&self) -> &'static str {
403        self.metadata.target()
404    }
405
406    /// The module path of the message
407    #[inline]
408    pub fn module_path(&self) -> Option<&'static str> {
409        self.module_path
410    }
411
412    /// The source file containing the message
413    #[inline]
414    pub fn file(&self) -> Option<&'static str> {
415        self.file
416    }
417
418    /// The line containing the message
419    #[inline]
420    pub fn line(&self) -> Option<u32> {
421        self.line
422    }
423}
424
425/// Sets the [`Logs`]. Must use [`Logs`] created in the runner
426/// app
427#[doc(hidden)]
428pub fn set_logs(logs: Logs) {
429    LOGS.set(logs).expect("setup ran twice");
430}