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}