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}