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