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}