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}