duat_core/context/
log.rs

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