lang_interpreter/
terminal_io.rs

1#[cfg(feature = "custom-logging")]
2#[doc(hidden)]
3pub mod custom_logging;
4#[cfg(feature = "custom-logging")]
5#[doc(inline)]
6pub use custom_logging::{Logger, DefaultLogger};
7
8#[cfg(all(
9    feature = "custom-logging",
10    feature = "wasm",
11))]
12#[doc(inline)]
13pub use custom_logging::wasm::JsConsoleLogger;
14
15use std::error::Error;
16use std::ffi::OsString;
17use std::io;
18use std::fs::File;
19use std::io::Write;
20use chrono::Local;
21
22#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
23#[repr(i8)]
24pub enum Level {
25    #[default]
26    NotSet = -1,
27    User = 0,
28    Debug = 1,
29    Config = 2,
30    Info = 3,
31    Warning = 4,
32    Error = 5,
33    Critical = 6,
34}
35
36impl Level {
37    pub(crate) const VALUES: [Self; 8] = [
38        Self::NotSet,
39        Self::User,
40        Self::Debug,
41        Self::Config,
42        Self::Info,
43        Self::Warning,
44        Self::Error,
45        Self::Critical,
46    ];
47
48    pub fn level(&self) -> i8 {
49        *self as i8
50    }
51
52    pub fn name(&self) -> String {
53        match self {
54            Level::NotSet => "Not set",
55            Level::User => "User",
56            Level::Debug => "Debug",
57            Level::Config => "Config",
58            Level::Info => "Info",
59            Level::Warning => "Warning",
60            Level::Error => "Error",
61            Level::Critical => "Critical",
62        }.to_string()
63    }
64
65    pub fn should_log(&self, log_level: Level) -> bool {
66        log_level == Level::User || *self as i8 >= log_level as i8
67    }
68}
69
70#[derive(Debug)]
71pub struct TerminalIO {
72    file: Option<File>,
73    log_level: Level,
74
75    #[cfg(feature = "custom-logging")]
76    logger: Box<dyn Logger>,
77}
78
79impl TerminalIO {
80    const TIME_FORMAT: &'static str = "%d.%m.%Y|%H:%M:%S";
81
82    pub fn new(log_file: Option<OsString>) -> Result<Self, io::Error> {
83        let file = if let Some(log_file) = log_file {
84            Some(File::create(log_file)?)
85        }else {
86            None
87        };
88
89        #[cfg(not(feature = "custom-logging"))]
90        {
91            Ok(Self {
92                file,
93                log_level: Level::NotSet,
94            })
95        }
96        #[cfg(feature = "custom-logging")]
97        {
98            #[cfg(not(feature = "wasm"))]
99            {
100                Ok(Self {
101                    file,
102                    log_level: Level::NotSet,
103                    logger: Box::new(DefaultLogger),
104                })
105            }
106            #[cfg(feature = "wasm")]
107            {
108                Ok(Self {
109                    file,
110                    log_level: Level::NotSet,
111                    logger: Box::new(JsConsoleLogger),
112                })
113            }
114        }
115    }
116
117    /// Creates a [TerminalIO] instance with a custom logger
118    ///
119    /// This feature is only available if the `custom-logging` feature is enabled
120    #[cfg(feature = "custom-logging")]
121    pub fn with_custom_logger(log_file: Option<OsString>, logger: Box<dyn Logger>) -> Result<Self, io::Error> {
122        let file = if let Some(log_file) = log_file {
123            Some(File::create(log_file)?)
124        }else {
125            None
126        };
127
128        Ok(Self {
129            file,
130            log_level: Level::NotSet,
131            logger,
132        })
133    }
134
135    pub fn set_level(&mut self, level: Level) {
136        self.log_level = level;
137    }
138
139    fn log_internal(&mut self, lvl: Level, txt: &str, tag: &str) {
140        if !lvl.should_log(self.log_level) {
141            return;
142        }
143
144        let current_time = Local::now().format(Self::TIME_FORMAT).to_string();
145
146        let log = format!(
147            "[{}][{}][{}]: {}",
148            lvl.name(),
149            current_time,
150            tag,
151            txt,
152        );
153
154        #[cfg(not(feature = "custom-logging"))]
155        {
156            #[cfg(not(feature = "wasm"))]
157            {
158                println!("{log}");
159            }
160            #[cfg(feature = "wasm")]
161            {
162                match lvl {
163                    Level::NotSet | Level::User | Level::Config => web_sys::console::log_1(&log.as_str().into()),
164                    Level::Debug => web_sys::console::debug_1(&log.as_str().into()),
165                    Level::Info => web_sys::console::info_1(&log.as_str().into()),
166                    Level::Warning => web_sys::console::warn_1(&log.as_str().into()),
167                    Level::Error | Level::Critical => web_sys::console::error_1(&log.as_str().into()),
168                }
169            }
170        }
171        #[cfg(feature = "custom-logging")]
172        self.logger.log(lvl, &current_time, txt, tag);
173
174        if let Some(file) = &mut self.file {
175            let err = write!(file, "{log}");
176            if let Err(e) = err {
177                //Do not use the log_stack_trace method to avoid a stack overflow
178                #[cfg(not(feature = "wasm"))]
179                {
180                    eprintln!("{e}");
181                }
182                #[cfg(feature = "wasm")]
183                {
184                    web_sys::console::error_1(&e.to_string().into());
185                }
186            }
187        }
188    }
189    
190    pub fn log(&mut self, lvl: Level, txt: impl Into<String>, tag: impl Into<String>) {
191        self.log_internal(lvl, &txt.into(), &tag.into());
192    }
193    
194    pub fn log_stack_trace(&mut self, error: Box<dyn Error>, tag: impl Into<String>) {
195        self.log(Level::Error, error.to_string(), tag);
196    }
197}