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