Skip to main content

cba/bog/
global.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Display},
4    io::{Write, stderr, stdout},
5    sync::Mutex,
6    u8,
7};
8
9use crate::bait::{MutexExt, OptionExt};
10
11use super::{Bg, BogFmter, BogLevel, Fg};
12
13/// ---------- Global bogger instance --------------
14#[allow(non_camel_case_types)]
15struct GLOBAL_BOGGER_STRUCT {
16    formatter: Box<dyn BogFmter + Send + Sync>,
17    writer: Box<dyn Write + Send + Sync>,
18    min_level: (u8, BogLevel),
19    downcast_to: (u8, BogLevel),
20    pub prefix: String,
21    pub suffix: String,
22    pub tag_override: Option<String>,
23    /// Some(true) => only log, Some(false) => Only bog, None => Both
24    pub log: Option<bool>,
25}
26
27// since stderr has an internal lock i guess this isn't a huge deal anyways
28static GLOBAL_BOGGER: Mutex<Option<GLOBAL_BOGGER_STRUCT>> = Mutex::new(None);
29
30fn init_(logger: Box<dyn BogFmter + Send + Sync>, write: Box<dyn Write + Send + Sync>) {
31    let bogger = GLOBAL_BOGGER_STRUCT {
32        formatter: logger,
33        writer: write,
34        downcast_to: (255, BogLevel::ERROR),
35        min_level: (0, BogLevel::DEBUG),
36        prefix: String::new(),
37        suffix: String::new(),
38        tag_override: None,
39        log: None,
40    };
41
42    *GLOBAL_BOGGER.lock().unwrap() = Some(bogger);
43}
44// -------- (Internal) methods on global  ----------
45impl GLOBAL_BOGGER_STRUCT {
46    fn bog(&mut self, mut level: BogLevel, tag: &str, msg: &str) {
47        // Determine priority
48        let pri = self.formatter.priority(&level);
49        if pri < self.min_level.0 {
50            return;
51        }
52        if pri > self.downcast_to.0 {
53            level = self.downcast_to.1;
54        }
55
56        if self.log != Some(false) {
57            let level = match level {
58                BogLevel::ERROR => Some(log::Level::Error),
59                BogLevel::WARN => Some(log::Level::Warn),
60                BogLevel::INFO => Some(log::Level::Info),
61                BogLevel::DEBUG => Some(log::Level::Debug),
62                BogLevel::___ => Some(log::Level::Trace),
63                _ => None,
64            };
65            if let Some(lvl) = level {
66                log::log!(lvl, "{}{}{}", self.prefix, msg, self.suffix);
67            }
68        }
69        if self.log != Some(true) {
70            // Determine effective tag
71            let effective_tag = self.tag_override.as_deref().unwrap_or(tag);
72
73            // Format message with prefix and suffix
74            let mut formatted = if !self.prefix.is_empty() {
75                let mut prefixed_msg = self.prefix.clone();
76                prefixed_msg.push_str(&msg);
77                self.formatter.format(level, effective_tag, &prefixed_msg)
78            } else {
79                self.formatter.format(level, effective_tag, msg)
80            };
81
82            if !self.suffix.is_empty() {
83                formatted.push_str(&self.suffix);
84            }
85            formatted.push('\n');
86
87            // Write to writer
88            let _ = self.writer.write_all(formatted.as_bytes());
89        }
90    }
91
92    fn pause(&mut self) {
93        self.min_level.0 = u8::MAX;
94    }
95
96    fn resume(&mut self) {
97        self.min_level.0 = self.formatter.priority(&self.min_level.1)
98    }
99
100    fn filter_below(&mut self, lvl: BogLevel) {
101        self.min_level = (self.formatter.priority(&lvl), lvl);
102    }
103
104    /// Show only messages with this priority and higher.
105    /// On resume, displays all messages.
106    fn filter_below_priority(&mut self, v: u8) {
107        self.min_level = (v, BogLevel::___);
108    }
109
110    fn downcast_above(&mut self, lvl: BogLevel) {
111        self.downcast_to = (self.formatter.priority(&lvl), lvl);
112    }
113
114    fn bounds(&self) -> ((u8, BogLevel), (u8, BogLevel)) {
115        (self.min_level, self.downcast_to)
116    }
117
118    pub fn set_bounds(&mut self, bounds: ((u8, BogLevel), (u8, BogLevel))) {
119        self.min_level = bounds.0;
120        self.downcast_to = bounds.1;
121    }
122}
123
124// ------- CONTEXT --------
125/// Context for temporary bogger settings. Use with [`Bogger::with`].
126pub struct BogContext {
127    /// [lower, upper]. Filter out messages below lower and downcast messages above upper.
128    bounds: [Option<BogLevel>; 2],
129    /// Whether to pause logging.
130    pause: bool,
131    /// Prefix to prepend to all messages.
132    prefix: Option<String>,
133    /// Suffix to append to all messages.
134    suffix: Option<String>,
135    /// Override tag for all messages.
136    tag_override: Option<String>,
137    /// Whether to only log (true), [`bog`] only (false), or both (None).
138    log: Option<bool>,
139}
140
141impl BogContext {
142    pub fn new() -> Self {
143        Self {
144            bounds: [None, None],
145            pause: false,
146            prefix: None,
147            suffix: None,
148            tag_override: None,
149            log: None,
150        }
151    }
152
153    pub fn upper(mut self, level: BogLevel) -> Self {
154        self.bounds[1] = Some(level);
155        self
156    }
157
158    pub fn lower(mut self, level: BogLevel) -> Self {
159        self.bounds[0] = Some(level);
160        self
161    }
162
163    pub fn pause(mut self, pause: bool) -> Self {
164        self.pause = pause;
165        self
166    }
167
168    pub fn prefix<S: Into<String>>(mut self, prefix: S) -> Self {
169        self.prefix = Some(prefix.into());
170        self
171    }
172
173    pub fn suffix<S: Into<String>>(mut self, suffix: S) -> Self {
174        self.suffix = Some(suffix.into());
175        self
176    }
177
178    pub fn tag<S: Into<String>>(mut self, tag: S) -> Self {
179        self.tag_override = Some(tag.into());
180        self
181    }
182
183    pub fn log(mut self, log: Option<bool>) -> Self {
184        self.log = log;
185        self
186    }
187}
188
189// --------- EXPORTS/MAIN API ---------
190// convenience reexport
191
192pub struct BOGGER {}
193// organize under namespace
194impl BOGGER {
195    // don't panic
196    /// Log a message at the given level with optional tag.
197    #[inline]
198    pub fn bog(level: BogLevel, tag: &str, msg: &str) {
199        if let Some(b) = GLOBAL_BOGGER._lock().as_mut() {
200            b.bog(level, tag, msg);
201        }
202    }
203
204    /// Set the minimum level to log.
205    #[inline]
206    pub fn filter_below(lvl: BogLevel) {
207        if let Some(b) = GLOBAL_BOGGER._lock().as_mut() {
208            b.filter_below(lvl);
209        }
210    }
211
212    /// Downcast messages above the given level to this level.
213    #[inline]
214    pub fn downcast_above(lvl: BogLevel) {
215        if let Some(b) = GLOBAL_BOGGER._lock().as_mut() {
216            b.downcast_above(lvl);
217        }
218    }
219
220    /// Temporarily apply a BogContext while executing a closure.
221    #[inline]
222    pub fn with<T>(context: BogContext, f: impl FnOnce() -> T) -> T {
223        let (prev_bounds, prev_paused, prev_prefix, prev_suffix, prev_tag) = {
224            if let Some(b) = GLOBAL_BOGGER._lock().as_mut() {
225                // Save previous state
226                let prev_bounds = b.bounds();
227                let prev_paused = prev_bounds.0.0 == u8::MAX;
228                let prev_prefix = b.prefix.clone();
229                let prev_suffix = b.suffix.clone();
230                let prev_tag = b.tag_override.clone();
231
232                // Apply new context
233                if let Some(level) = context.bounds[0] {
234                    b.filter_below(level);
235                }
236                if let Some(level) = context.bounds[1] {
237                    b.downcast_above(level);
238                }
239                if let Some(ref prefix) = context.prefix {
240                    b.prefix = prefix.clone();
241                }
242                if let Some(ref suffix) = context.suffix {
243                    b.suffix = suffix.clone();
244                }
245                if let Some(ref tag) = context.tag_override {
246                    b.tag_override = Some(tag.clone());
247                }
248                if context.pause {
249                    b.pause();
250                }
251
252                (
253                    Some(prev_bounds),
254                    Some(prev_paused),
255                    Some(prev_prefix),
256                    Some(prev_suffix),
257                    prev_tag,
258                )
259            } else {
260                (None, None, None, None, None)
261            }
262        };
263
264        // Execute the closure
265        let result = f();
266
267        // Restore previous state
268        if let Some(b) = GLOBAL_BOGGER._lock().as_mut() {
269            if let Some(bounds) = prev_bounds {
270                b.set_bounds(bounds);
271            }
272            if let Some(paused) = prev_paused {
273                if paused {
274                    b.pause();
275                } else {
276                    b.resume();
277                }
278            }
279            if let Some(prefix) = prev_prefix {
280                b.prefix = prefix;
281            }
282            if let Some(suffix) = prev_suffix {
283                b.suffix = suffix;
284            }
285            if let Some(tag) = prev_tag {
286                b.tag_override = Some(tag);
287            } else if context.tag_override.is_some() {
288                b.tag_override = None
289            }
290        }
291
292        result
293    }
294
295    /// Execute a closure while pausing logging.
296    #[inline]
297    pub fn paused<T>(f: impl FnOnce() -> T) -> T {
298        BOGGER::pause();
299        let ret = f();
300        BOGGER::resume();
301        ret
302    }
303
304    /// Pause logging.
305    #[inline]
306    pub fn pause() {
307        if let Some(b) = GLOBAL_BOGGER._lock().as_mut() {
308            b.pause();
309        }
310    }
311
312    /// Resume logging.
313    #[inline]
314    pub fn resume() {
315        if let Some(b) = GLOBAL_BOGGER._lock().as_mut() {
316            b.resume();
317        }
318    }
319}
320
321// ----------- INITIALIZATION -------------
322pub fn init_bogger(fg: bool, output_stderr: bool) {
323    let writer: Box<dyn Write + Send + Sync> = if output_stderr {
324        Box::new(stderr())
325    } else {
326        Box::new(stdout())
327    };
328
329    if fg {
330        init_(Box::new(Fg {}), writer);
331    } else {
332        init_(Box::new(Bg {}), writer);
333    }
334}
335
336/// Initialize the global log filter based on a numeric verbosity level. [`init_bogger`] must be called beforehand.
337///
338/// The verbosity value maps to a minimum [`BogLevel`] that will be emitted:
339///
340/// - `0` → silence all
341/// - `1` → show `NOTE`, `EMPTY` and `CUSTOM` messages only
342/// - `2` → show `ERROR` and above
343/// - `3` → show `WARN` and above
344/// - `4` → show `INFO` and above
345/// - `5` → show `_WRN`/`_NFO` and above
346/// - `6` → show `DEBUG` and above
347/// - `7` → show all messages
348///
349/// Note that the ordering is dependant on the default implementation of [`BogFmter::priority`]. If overridden in a non-compatible way, we recommended calling [`BOGGER::filter_below`] directly to avoid confusion.
350pub fn init_filter(verbosity: u8) {
351    let level = match verbosity {
352        0 => {
353            GLOBAL_BOGGER
354                ._lock()
355                .as_mut()
356                .unwrap()
357                .filter_below_priority(u8::MAX);
358            return;
359        }
360        1 => BogLevel::NOTE,
361        2 => BogLevel::ERROR,
362        3 => BogLevel::WARN,
363        4 => BogLevel::INFO,
364        5 => BogLevel::_WRN,
365        6 => BogLevel::DEBUG,
366        _ => BogLevel::___,
367    };
368    log::debug!("Bogging level initialized at {level:?}");
369    BOGGER::filter_below(level);
370}
371
372// ----------- MACROS ------------------
373#[macro_export]
374macro_rules! ibog {
375    // With tag expressions
376    ($($harg:expr),* ; $($arg:expr),*) => {{
377        $crate::BOGGER::bog(
378            $crate::bog::BogLevel::INFO,
379            &format!($($harg),*),
380            &format!($($arg),*),
381        );
382    }};
383    // Without tag
384    ($($arg:expr),*) => {{
385        $crate::BOGGER::bog(
386            $crate::bog::BogLevel::INFO,
387            "",
388            &format!($($arg),*),
389        );
390    }};
391}
392
393#[macro_export]
394macro_rules! dbog {
395    ($($harg:expr),* ; $($arg:expr),*) => {{
396        $crate::BOGGER::bog(
397            $crate::bog::BogLevel::DEBUG,
398            &format!($($harg),*),
399            &format!($($arg),*),
400        );
401    }};
402    ($($arg:expr),*) => {{
403        $crate::BOGGER::bog(
404            $crate::bog::BogLevel::DEBUG,
405            "",
406            &format!($($arg),*),
407        );
408    }};
409}
410
411#[macro_export]
412macro_rules! ebog {
413    ($($harg:expr),* ; $($arg:expr),*) => {{
414        $crate::BOGGER::bog(
415            $crate::bog::BogLevel::ERROR,
416            &format!($($harg),*),
417            &format!($($arg),*),
418        );
419    }};
420    ($($arg:expr),*) => {{
421        $crate::BOGGER::bog(
422            $crate::bog::BogLevel::ERROR,
423            "",
424            &format!($($arg),*),
425        );
426    }};
427}
428
429#[macro_export]
430macro_rules! wbog {
431    ($($harg:expr),* ; $($arg:expr),*) => {{
432        $crate::BOGGER::bog(
433            $crate::bog::BogLevel::WARN,
434            &format!($($harg),*),
435            &format!($($arg),*),
436        );
437    }};
438    ($($arg:expr),*) => {{
439        $crate::BOGGER::bog(
440            $crate::bog::BogLevel::WARN,
441            "",
442            &format!($($arg),*),
443        );
444    }};
445}
446
447#[macro_export]
448macro_rules! nbog {
449    ($($harg:expr),* ; $($arg:expr),*) => {{
450        $crate::BOGGER::bog(
451            $crate::bog::BogLevel::NOTE,
452            &format!($($harg),*),
453            &format!($($arg),*),
454        );
455    }};
456    ($($arg:expr),*) => {{
457        $crate::BOGGER::bog(
458            $crate::bog::BogLevel::NOTE,
459            "",
460            &format!($($arg),*),
461        );
462    }};
463}
464
465#[macro_export]
466macro_rules! mbog {
467    ($($harg:expr),* ; $($arg:expr),*) => {{
468        $crate::BOGGER::bog(
469            $crate::bog::BogLevel::EMPTY,
470            &format!($($harg),*),
471            &format!($($arg),*),
472        );
473    }};
474    ($($arg:expr),*) => {{
475        $crate::BOGGER::bog(
476            $crate::bog::BogLevel::EMPTY,
477            "",
478            &format!($($arg),*),
479        );
480    }};
481}
482
483#[macro_export]
484macro_rules! cbog {
485    ($discriminant:literal ; $($harg:expr),* ; $($arg:expr),*) => {{
486        $crate::BOGGER::bog(
487            $crate::bog::BogLevel::CUSTOM($discriminant),
488            &format!($($harg),*),
489            &format!($($arg),*),
490        );
491    }};
492    ($discriminant:literal ; $($arg:expr),*) => {{
493        $crate::BOGGER::bog(
494            $crate::bog::BogLevel::CUSTOM($discriminant),
495            "",
496            &format!($($arg),*),
497        );
498    }};
499}
500
501#[macro_export]
502macro_rules! _wbog {
503    ($($harg:expr),* ; $($arg:expr),*) => {{
504        $crate::BOGGER::bog(
505            $crate::bog::BogLevel::_WRN,
506            &format!($($harg),*),
507            &format!($($arg),*),
508        );
509    }};
510    ($($arg:expr),*) => {{
511        $crate::BOGGER::bog(
512            $crate::bog::BogLevel::_WRN,
513            "",
514            &format!($($arg),*),
515        );
516    }};
517}
518
519#[macro_export]
520macro_rules! _ibog {
521    ($($harg:expr),* ; $($arg:expr),*) => {{
522        $crate::BOGGER::bog(
523            $crate::bog::BogLevel::_NFO,
524            &format!($($harg),*),
525            &format!($($arg),*),
526        );
527    }};
528    ($($arg:expr),*) => {{
529        $crate::BOGGER::bog(
530            $crate::bog::BogLevel::_NFO,
531            "",
532            &format!($($arg),*),
533        );
534    }};
535}
536
537// ----------- RESULT -----------------
538
539/// # Example
540/// ```rust
541/// use cba::bog::{BogOkExt, BogUnwrapExt};
542///
543/// fn fallible_result() -> Result<i32, Box<dyn std::error::Error>> {
544///     Ok(42)
545/// }
546///
547/// fn process(x: i32) {
548///     println!("Processing {}", x);
549/// }
550///
551/// if let Some(x) = fallible_result()._ebog() {
552///     process(x);
553/// }
554/// ```
555
556#[easy_ext::ext(BogOkExt)]
557pub impl<T, E: Display> Result<T, E> {
558    fn _bog_<'a>(self, level: BogLevel, tag: impl Into<Cow<'a, str>>) -> Option<T> {
559        match self {
560            Ok(val) => Some(val),
561            Err(e) => {
562                BOGGER::bog(level, &tag.into(), &e.to_string());
563                None
564            }
565        }
566    }
567
568    fn _ebog_<'a>(self, tag: impl Into<Cow<'a, str>>) -> Option<T> {
569        self._bog_(BogLevel::ERROR, tag)
570    }
571
572    fn _ibog_<'a>(self, tag: impl Into<Cow<'a, str>>) -> Option<T> {
573        self._bog_(BogLevel::INFO, tag)
574    }
575
576    fn _dbog_<'a>(self, tag: impl Into<Cow<'a, str>>) -> Option<T> {
577        self._bog_(BogLevel::DEBUG, tag)
578    }
579
580    fn _wbog_<'a>(self, tag: impl Into<Cow<'a, str>>) -> Option<T> {
581        self._bog_(BogLevel::WARN, tag)
582    }
583
584    fn _bog(self, level: BogLevel) -> Option<T> {
585        self._bog_(level, "")
586    }
587
588    fn _ebog(self) -> Option<T> {
589        self._ebog_("")
590    }
591
592    fn __ebog(self) -> T {
593        self._ebog_("").or_exit()
594    }
595
596    fn _wbog(self) -> Option<T> {
597        self._wbog_("")
598    }
599
600    fn _dbog(self) -> Option<T> {
601        self._dbog_("")
602    }
603    fn _ibog(self) -> Option<T> {
604        self._ibog_("")
605    }
606}
607
608#[easy_ext::ext(BogUnwrapExt)]
609pub impl<T> Option<T> {
610    /// Unwrap or bog and exit
611    fn _bog_<'a>(
612        self,
613        level: BogLevel,
614        tag: impl Into<Cow<'a, str>>,
615        msg: impl Into<Cow<'a, str>>,
616    ) -> T {
617        match self {
618            Some(val) => val,
619            None => {
620                BOGGER::bog(level, &tag.into(), &msg.into());
621                std::process::exit(1);
622            }
623        }
624    }
625
626    /// Unwrap or bog and exit
627    fn _bog<'a>(self, level: BogLevel, msg: impl Into<Cow<'a, str>>) -> T {
628        self._bog_(level, "", msg)
629    }
630
631    /// Unwrap or err and exit
632    fn _ebog<'a>(self, msg: impl Into<Cow<'a, str>>) -> T {
633        self._bog(BogLevel::ERROR, msg)
634    }
635
636    /// Unwrap or err and exit
637    fn _ebog_<'a>(self, tag: impl Into<Cow<'a, str>>, msg: impl Into<Cow<'a, str>>) -> T {
638        self._bog_(BogLevel::ERROR, tag, msg)
639    }
640
641    fn bog_<'a>(
642        self,
643        level: BogLevel,
644        tag: impl Into<Cow<'a, str>>,
645        msg: impl Into<Cow<'a, str>>,
646    ) -> Option<T> {
647        match self {
648            Some(val) => Some(val),
649            None => {
650                BOGGER::bog(level, &tag.into(), &msg.into());
651                None
652            }
653        }
654    }
655    fn bog<'a>(self, level: BogLevel, msg: impl Into<Cow<'a, str>>) -> Option<T> {
656        self.bog_(level, "", msg)
657    }
658    fn ebog<'a>(self, msg: impl Into<Cow<'a, str>>) -> Option<T> {
659        self.bog(BogLevel::ERROR, msg)
660    }
661    fn ebog_<'a>(self, msg: impl Into<Cow<'a, str>>) -> Option<T> {
662        self.bog(BogLevel::ERROR, msg)
663    }
664}
665
666#[cfg(test)]
667mod test {
668    use super::*;
669
670    #[test]
671    fn show_fg_bogger() {
672        init_bogger(true, false);
673        // DEBUG messages
674        dbog!("DEBUG message: {}", 3.14159);
675        dbog!("val"; "DEBUG values: x={}, y={}", 10, 20);
676
677        // INFO messages
678        ibog!("INFO message: {}", 42);
679        ibog!("Created Directory"; "~/archr/Desktop");
680
681        // WARN messages
682        wbog!("WARN message: {}", "disk almost full");
683        wbog!("NoSpace"; "WARN message: {} attempts left", 3);
684
685        // ERROR messages
686        ebog!("ERROR message: {}", "file not found");
687        ebog!("404"; "Not found");
688
689        // NOTE messages
690        nbog!("justification");
691        nbog!("NOTE"; "ancillary");
692        mbog!("justification");
693        mbog!("FULL TAG"; "ancillary");
694
695        // CUSTOM / NOTE-like messages using cbog
696        cbog!("NOTE"; "Custom note message: {}", "all good");
697        cbog!("NOTE"; ""; "Custom note with tag: {}", 123);
698        cbog!("CUSTOM"; "Custom discriminant"; "Message with both tag and content");
699    }
700
701    #[test]
702    fn show_bg_bogger() {
703        init_bogger(false, true);
704        // DEBUG messages
705        dbog!("DEBUG message: {}", 3.14159);
706        dbog!("val"; "DEBUG values: x={}, y={}", 10, 20);
707
708        // INFO messages
709        ibog!("INFO message: {}", 42);
710        ibog!("Urgent"; "INFO message number {}", 7);
711
712        // WARN messages
713        wbog!("WARN message: {}", "disk almost full");
714        wbog!("NoSpace"; "WARN message: {} attempts left", 3);
715
716        // ERROR messages
717        ebog!("ERROR message: {}", "file not found");
718        ebog!("404"; "Not found");
719
720        // NOTE messages
721        nbog!("justification");
722        nbog!("NOTE"; "ancillary");
723        mbog!("justification");
724        mbog!("FULL"; "ancillary");
725
726        // CUSTOM
727        cbog!("NOTE"; "Custom note message: {}", "all good");
728        cbog!("NOTE"; ""; "Custom note with tag: {}", 123);
729        cbog!("CUSTOM"; "Custom discriminant"; "Message with both tag and content");
730    }
731
732    #[test]
733    fn min_level_and_downcast_combined() {
734        init_bogger(true, false);
735
736        // drop DEBUG/INFO entirely
737        BOGGER::filter_below(/* WARN priority */ BogLevel::INFO);
738        // downcast ERROR to WARN
739        BOGGER::downcast_above(BogLevel::WARN);
740
741        dbog!("debug filtered");
742        ibog!("info normal");
743        ebog!("error shown as warn");
744    }
745}
746
747// ----------------------------------------------------------
748impl fmt::Debug for GLOBAL_BOGGER_STRUCT {
749    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
750        let mut ds = f.debug_struct("GLOBAL_BOGGER_STRUCT");
751
752        ds.field("min_level", &self.min_level)
753            .field("downcast_to", &self.downcast_to)
754            .field("prefix", &self.prefix)
755            .field("suffix", &self.suffix)
756            .field("tag_override", &self.tag_override)
757            .field("log", &self.log);
758
759        // Opaque fields (trait objects / non-debuggable)
760        ds.field("formatter", &"dyn BogFmter")
761            .field("writer", &"dyn Write");
762
763        ds.finish()
764    }
765}