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!().to_string().leak(),
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!().to_string().leak(),
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(),
128                $crate::context::Level::Info,
129                $($arg)+
130            )
131        }),
132        ($($arg:tt)+) => (
133            $crate::private_exports::log!(
134                module_path!().to_string().leak(),
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!().to_string().leak(),
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.cur_state.load(Ordering::Relaxed) - 1),
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<(usize, Record)> {
256        self.read_state
257            .store(self.cur_state.load(Ordering::Relaxed), Ordering::Relaxed);
258        let list = self.list.lock().unwrap();
259        list.last().cloned().map(|last| (list.len() - 1, last))
260    }
261
262    /// Gets the last [`Record`] with a level from a list
263    pub fn last_with_levels(&self, levels: &[Level]) -> Option<(usize, Record)> {
264        self.read_state
265            .store(self.cur_state.load(Ordering::Relaxed), Ordering::Relaxed);
266        self.list
267            .lock()
268            .unwrap()
269            .iter()
270            .enumerate()
271            .rev()
272            .find_map(|(i, rec)| levels.contains(&rec.level()).then(|| (i, rec.clone())))
273    }
274
275    /// Wether there are new notifications or not
276    pub fn has_changed(&self) -> bool {
277        self.cur_state.load(Ordering::Relaxed) > self.read_state.load(Ordering::Relaxed)
278    }
279
280    /// Pushes a [`CmdResult`]
281    ///
282    /// [`CmdResult`]: crate::cmd::CmdResult
283    pub(crate) fn push_cmd_result(&self, cmd: String, result: Result<Option<Text>, Text>) {
284        let is_ok = result.is_ok();
285        let (Ok(Some(res)) | Err(res)) = result else {
286            return;
287        };
288
289        self.cur_state.fetch_add(1, Ordering::Relaxed);
290
291        let rec = Record {
292            metadata: log::MetadataBuilder::new()
293                .level(if is_ok { Level::Info } else { Level::Error })
294                .target(cmd.leak())
295                .build(),
296            module_path: None,
297            file: None,
298            line: None,
299            text: Box::leak(Box::new(res.no_selections())),
300        };
301
302        self.list.lock().unwrap().push(rec)
303    }
304
305    /// Pushes a new [`Record`] to Duat
306    #[doc(hidden)]
307    pub fn push_record(&self, rec: Record) {
308        self.cur_state.fetch_add(1, Ordering::Relaxed);
309        self.list.lock().unwrap().push(rec)
310    }
311
312    /// Returns the number of [`Record`]s in the [`Logs`]
313    pub fn len(&self) -> usize {
314        self.list.lock().unwrap().len()
315    }
316
317    /// Wether there are any [`Record`]s in the [`Logs`]
318    ///
319    /// It's pretty much never `true`
320    #[must_use]
321    pub fn is_empty(&self) -> bool {
322        self.len() == 0
323    }
324}
325
326impl log::Log for Logs {
327    fn enabled(&self, metadata: &log::Metadata) -> bool {
328        metadata.level() > log::Level::Debug
329    }
330
331    fn log(&self, rec: &log::Record) {
332        let rec = Record {
333            text: Box::leak(Box::new(
334                Text::from(std::fmt::format(*rec.args())).no_selections(),
335            )),
336            metadata: log::MetadataBuilder::new()
337                .level(rec.level())
338                .target(rec.target().to_string().leak())
339                .build(),
340            module_path: match rec.module_path_static() {
341                Some(module_path) => Some(module_path),
342                None => rec
343                    .module_path()
344                    .map(|mp| -> &str { mp.to_string().leak() }),
345            },
346            file: match rec.file_static() {
347                Some(file) => Some(file),
348                None => rec.file().map(|mp| -> &str { mp.to_string().leak() }),
349            },
350            line: rec.line(),
351        };
352
353        self.cur_state.fetch_add(1, Ordering::Relaxed);
354        self.list.lock().unwrap().push(rec)
355    }
356
357    fn flush(&self) {}
358}
359
360/// A record of something that happened in Duat
361///
362/// Differs from [`log::Record`] in that its argument isn't an
363/// [`std::fmt::Arguments`], but a [`Text`] instead.
364#[derive(Clone, Debug)]
365pub struct Record {
366    text: &'static Selectionless,
367    metadata: log::Metadata<'static>,
368    module_path: Option<&'static str>,
369    file: Option<&'static str>,
370    line: Option<u32>,
371}
372
373impl Record {
374    /// Creates a new [`Record`]
375    #[doc(hidden)]
376    pub fn new(
377        text: Text,
378        level: Level,
379        target: &'static str,
380        module_path: Option<&'static str>,
381        file: Option<&'static str>,
382        line: Option<u32>,
383    ) -> Self {
384        Self {
385            text: Box::leak(Box::new(text.no_selections())),
386            metadata: log::MetadataBuilder::new()
387                .level(level)
388                .target(target)
389                .build(),
390            module_path,
391            file,
392            line,
393        }
394    }
395
396    /// The [`Text`] of this [`Record`]
397    #[inline]
398    pub fn text(&self) -> &Text {
399        self.text
400    }
401
402    /// Metadata about the log directive
403    #[inline]
404    pub fn metadata(&self) -> log::Metadata<'static> {
405        self.metadata.clone()
406    }
407
408    /// The verbosity level of the message
409    #[inline]
410    pub fn level(&self) -> Level {
411        self.metadata.level()
412    }
413
414    /// The name of the target of the directive
415    #[inline]
416    pub fn target(&self) -> &'static str {
417        self.metadata.target()
418    }
419
420    /// The module path of the message
421    #[inline]
422    pub fn module_path(&self) -> Option<&'static str> {
423        self.module_path
424    }
425
426    /// The source file containing the message
427    #[inline]
428    pub fn file(&self) -> Option<&'static str> {
429        self.file
430    }
431
432    /// The line containing the message
433    #[inline]
434    pub fn line(&self) -> Option<u32> {
435        self.line
436    }
437}
438
439/// Sets the [`Logs`]. Must use [`Logs`] created in the runner
440/// app
441#[doc(hidden)]
442pub fn set_logs(logs: Logs) {
443    LOGS.set(logs).expect("setup ran twice");
444}