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!(),
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!(),
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().leak(),
128 $crate::context::Level::Info,
129 $($arg)+
130 )
131 }),
132 ($($arg:tt)+) => (
133 $crate::private_exports::log!(
134 module_path!(),
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!(),
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.read_state.load(Ordering::Relaxed)),
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<Record> {
256 self.read_state
257 .store(self.cur_state.load(Ordering::Relaxed), Ordering::Relaxed);
258 self.list.lock().unwrap().last().cloned()
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 pub(crate) fn push_cmd_result(&self, cmd: String, result: Result<Option<Text>, Text>) {
270 let is_ok = result.is_ok();
271 let (Ok(Some(res)) | Err(res)) = result else {
272 return;
273 };
274
275 self.cur_state.fetch_add(1, Ordering::Relaxed);
276
277 let rec = Record {
278 metadata: log::MetadataBuilder::new()
279 .level(if is_ok { Level::Info } else { Level::Error })
280 .target(cmd.leak())
281 .build(),
282 module_path: None,
283 file: None,
284 line: None,
285 text: Box::leak(Box::new(res.no_selections())),
286 };
287
288 self.list.lock().unwrap().push(rec)
289 }
290
291 /// Pushes a new [`Record`] to Duat
292 #[doc(hidden)]
293 pub fn push_record(&self, rec: Record) {
294 self.cur_state.fetch_add(1, Ordering::Relaxed);
295 self.list.lock().unwrap().push(rec)
296 }
297
298 /// Returns the number of [`Record`]s in the [`Logs`]
299 pub fn len(&self) -> usize {
300 self.list.lock().unwrap().len()
301 }
302
303 /// Wether there are any [`Record`]s in the [`Logs`]
304 ///
305 /// It's pretty much never `true`
306 #[must_use]
307 pub fn is_empty(&self) -> bool {
308 self.len() == 0
309 }
310}
311
312impl log::Log for Logs {
313 fn enabled(&self, metadata: &log::Metadata) -> bool {
314 metadata.level() > log::Level::Debug
315 }
316
317 fn log(&self, rec: &log::Record) {
318 let rec = Record {
319 text: Box::leak(Box::new(
320 Text::from(std::fmt::format(*rec.args())).no_selections(),
321 )),
322 metadata: log::MetadataBuilder::new()
323 .level(rec.level())
324 .target(rec.target().to_string().leak())
325 .build(),
326 module_path: match rec.module_path_static() {
327 Some(module_path) => Some(module_path),
328 None => rec
329 .module_path()
330 .map(|mp| -> &str { mp.to_string().leak() }),
331 },
332 file: match rec.file_static() {
333 Some(file) => Some(file),
334 None => rec.file().map(|mp| -> &str { mp.to_string().leak() }),
335 },
336 line: rec.line(),
337 };
338
339 self.cur_state.fetch_add(1, Ordering::Relaxed);
340 self.list.lock().unwrap().push(rec)
341 }
342
343 fn flush(&self) {}
344}
345
346/// A record of something that happened in Duat
347///
348/// Differs from [`log::Record`] in that its argument isn't an
349/// [`std::fmt::Arguments`], but a [`Text`] instead.
350#[derive(Clone, Debug)]
351pub struct Record {
352 text: &'static Selectionless,
353 metadata: log::Metadata<'static>,
354 module_path: Option<&'static str>,
355 file: Option<&'static str>,
356 line: Option<u32>,
357}
358
359impl Record {
360 /// Creates a new [`Record`]
361 #[doc(hidden)]
362 pub fn new(
363 text: Text,
364 level: Level,
365 target: &'static str,
366 module_path: Option<&'static str>,
367 file: Option<&'static str>,
368 line: Option<u32>,
369 ) -> Self {
370 Self {
371 text: Box::leak(Box::new(text.no_selections())),
372 metadata: log::MetadataBuilder::new()
373 .level(level)
374 .target(target)
375 .build(),
376 module_path,
377 file,
378 line,
379 }
380 }
381
382 /// The [`Text`] of this [`Record`]
383 #[inline]
384 pub fn text(&self) -> &Text {
385 self.text
386 }
387
388 /// Metadata about the log directive
389 #[inline]
390 pub fn metadata(&self) -> &log::Metadata<'static> {
391 &self.metadata
392 }
393
394 /// The verbosity level of the message
395 #[inline]
396 pub fn level(&self) -> Level {
397 self.metadata.level()
398 }
399
400 /// The name of the target of the directive
401 #[inline]
402 pub fn target(&self) -> &'static str {
403 self.metadata.target()
404 }
405
406 /// The module path of the message
407 #[inline]
408 pub fn module_path(&self) -> Option<&'static str> {
409 self.module_path
410 }
411
412 /// The source file containing the message
413 #[inline]
414 pub fn file(&self) -> Option<&'static str> {
415 self.file
416 }
417
418 /// The line containing the message
419 #[inline]
420 pub fn line(&self) -> Option<u32> {
421 self.line
422 }
423}
424
425/// Sets the [`Logs`]. Must use [`Logs`] created in the runner
426/// app
427#[doc(hidden)]
428pub fn set_logs(logs: Logs) {
429 LOGS.set(logs).expect("setup ran twice");
430}