toast_logger_win/
toast_logger.rs1use std::{
2 fmt, mem,
3 sync::{Mutex, OnceLock},
4};
5
6use log::Log;
7
8use crate::{BufferedRecord, Notification, Notifier, Result};
9
10type LogRecordFormatter =
11 dyn Fn(&mut dyn fmt::Write, &log::Record) -> fmt::Result + Send + Sync + 'static;
12type NotificationCreator =
13 dyn Fn(&[BufferedRecord]) -> Result<Notification> + Send + Sync + 'static;
14
15struct ToastLoggerConfig {
16 max_level: log::LevelFilter,
17 is_auto_flush: bool,
18 application_id: String,
19 formatter: Box<LogRecordFormatter>,
20 create_notification: Box<NotificationCreator>,
21}
22
23impl Default for ToastLoggerConfig {
24 fn default() -> Self {
25 Self {
26 max_level: log::LevelFilter::Error,
27 is_auto_flush: true,
28 application_id: Self::DEFAULT_APP_ID.into(),
29 formatter: Box::new(Self::default_formatter),
30 create_notification: Box::new(Notification::new_with_records),
31 }
32 }
33}
34
35impl ToastLoggerConfig {
36 const DEFAULT_APP_ID: &str =
38 r"{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe";
39
40 fn default_formatter(buf: &mut dyn fmt::Write, record: &log::Record) -> fmt::Result {
41 write!(buf, "{}: {}", record.level(), record.args())
42 }
43
44 fn create_notifier(&self) -> Result<Notifier> {
45 Notifier::new_with_application_id(&self.application_id)
46 }
47}
48
49#[derive(Default)]
60pub struct ToastLoggerBuilder {
61 config: ToastLoggerConfig,
62}
63
64impl ToastLoggerBuilder {
65 fn new() -> Self {
66 Self::default()
67 }
68
69 pub fn init(&mut self) -> Result<()> {
72 ToastLogger::init(self.build_config())
73 }
74
75 #[deprecated(since = "0.2.0", note = "Use `init()` instead")]
76 pub fn init_logger(&mut self) -> Result<()> {
77 self.init()
78 }
79
80 pub fn build(&mut self) -> Result<ToastLogger> {
85 ToastLogger::new(self.build_config())
86 }
87
88 fn build_config(&mut self) -> ToastLoggerConfig {
89 mem::take(&mut self.config)
90 }
91
92 pub fn max_level(&mut self, level: log::LevelFilter) -> &mut Self {
95 self.config.max_level = level;
96 self
97 }
98
99 pub fn auto_flush(&mut self, is_auto_flush: bool) -> &mut Self {
123 self.config.is_auto_flush = is_auto_flush;
124 self
125 }
126
127 pub fn application_id(&mut self, application_id: &str) -> &mut Self {
137 self.config.application_id = application_id.into();
138 self
139 }
140
141 pub fn format<F>(&mut self, formatter: F) -> &mut Self
163 where
164 F: Fn(&mut dyn fmt::Write, &log::Record) -> fmt::Result + Send + Sync + 'static,
165 {
166 self.config.formatter = Box::new(formatter);
167 self
168 }
169
170 pub fn create_notification<F>(&mut self, create: F) -> &mut Self
182 where
183 F: Fn(&[BufferedRecord]) -> Result<Notification> + Send + Sync + 'static,
184 {
185 self.config.create_notification = Box::new(create);
186 self
187 }
188}
189
190pub struct ToastLogger {
207 config: ToastLoggerConfig,
208 notifier: Notifier,
209 records: Mutex<Vec<BufferedRecord>>,
210}
211
212static INSTANCE: OnceLock<ToastLogger> = OnceLock::new();
213
214impl ToastLogger {
215 pub fn builder() -> ToastLoggerBuilder {
218 ToastLoggerBuilder::new()
219 }
220
221 fn init(config: ToastLoggerConfig) -> Result<()> {
222 log::set_max_level(config.max_level);
223 if INSTANCE.set(Self::new(config)?).is_err() {
224 panic!("ToastLogger already initialized.");
225 }
226 log::set_logger(INSTANCE.get().unwrap())?;
227 Ok(())
228 }
229
230 fn new(config: ToastLoggerConfig) -> Result<Self> {
231 let notifier = config.create_notifier()?;
232 Ok(Self {
233 config,
234 notifier,
235 records: Mutex::new(Vec::new()),
236 })
237 }
238
239 pub fn flush() -> Result<()> {
246 let logger = INSTANCE.get().ok_or_else(|| crate::Error::NotInitialized)?;
247 logger.flush_result()
248 }
249
250 fn take_records(&self) -> Option<Vec<BufferedRecord>> {
251 let mut records = self.records.lock().unwrap();
252 if records.is_empty() {
253 return None;
254 }
255 Some(mem::take(&mut *records))
256 }
257
258 fn log_result(&self, record: &log::Record) -> Result<()> {
259 if !self.enabled(record.metadata()) {
260 return Ok(());
261 }
262
263 let mut text = String::new();
264 (self.config.formatter)(&mut text, record)?;
265 if text.is_empty() {
266 return Ok(());
267 }
268 let buffered_record = BufferedRecord::new_with_formatted_args(record, &text);
269
270 if self.config.is_auto_flush {
271 self.show_notification(&[buffered_record])?;
272 return Ok(());
273 }
274
275 let mut records = self.records.lock().unwrap();
277 records.push(buffered_record);
278 Ok(())
279 }
280
281 fn flush_result(&self) -> Result<()> {
282 if let Some(records) = self.take_records() {
283 return self.show_notification(&records);
284 }
285 Ok(())
286 }
287
288 fn show_notification(&self, records: &[BufferedRecord]) -> Result<()> {
289 let notification = (self.config.create_notification)(records)?;
290 self.notifier.show(¬ification)?;
291 Ok(())
292 }
293}
294
295impl log::Log for ToastLogger {
296 fn enabled(&self, metadata: &log::Metadata) -> bool {
297 metadata.level() <= self.config.max_level
298 }
299
300 fn log(&self, record: &log::Record) {
301 if let Err(error) = self.log_result(record) {
302 eprintln!("Error while logging: {error}");
303 }
304 }
305
306 fn flush(&self) {
307 if let Err(error) = self.flush_result() {
308 eprintln!("Error flushing: {error}");
309 }
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn builder_default() {
319 let builder = ToastLogger::builder();
320 assert_eq!(builder.config.max_level, log::LevelFilter::Error);
321 assert!(builder.config.is_auto_flush);
322 assert_eq!(
323 builder.config.application_id,
324 ToastLoggerConfig::DEFAULT_APP_ID
325 );
326 }
327
328 #[test]
329 fn max_level() -> Result<()> {
330 let logger = ToastLogger::builder()
331 .max_level(log::LevelFilter::Info)
332 .auto_flush(false)
333 .build()?;
334 let info = log::Record::builder()
335 .level(log::Level::Info)
336 .args(format_args!("test"))
337 .build();
338 let debug = log::Record::builder()
339 .level(log::Level::Debug)
340 .args(format_args!("test"))
341 .build();
342 logger.log(&debug);
343 assert_eq!(logger.take_records(), None);
344 logger.log(&info);
345 assert_eq!(
346 logger.take_records().unwrap_or_default(),
347 [BufferedRecord {
348 level: log::Level::Info,
349 args: "INFO: test".into()
350 }]
351 );
352 Ok(())
353 }
354
355 #[test]
356 fn format() -> Result<()> {
357 let logger = ToastLogger::builder()
358 .max_level(log::LevelFilter::Info)
359 .auto_flush(false)
360 .format(|buf: &mut dyn fmt::Write, record: &log::Record| buf.write_fmt(*record.args()))
361 .build()?;
362 let info = log::Record::builder()
363 .level(log::Level::Info)
364 .args(format_args!("test"))
365 .build();
366 logger.log(&info);
367 assert_eq!(
368 logger.take_records().unwrap_or_default(),
369 [BufferedRecord {
370 level: log::Level::Info,
371 args: "test".into()
372 }]
373 );
374 Ok(())
375 }
376}