1use notify_rust::Notification;
2use chrono::Local;
3use std::env;
4use std::fs;
5use std::io::Write;
6use backtrace::Backtrace;
7use std::sync::mpsc::{channel, Sender};
8use std::thread;
9use std::panic;
10
11pub struct VersaLog {
12 mode: String,
13 tag: String,
14 showFile: bool,
15 showTag: bool,
16 notice: bool,
17 enableall: bool,
18 allsave: bool,
19 savelevels: Vec<String>,
20 silent: bool,
21 tx: Option<Sender<(String, String)>>,
22}
23
24static COLORS: &[(&str, &str)] = &[
25 ("INFO", "\x1b[32m"),
26 ("ERROR", "\x1b[31m"),
27 ("WARNING", "\x1b[33m"),
28 ("DEBUG", "\x1b[36m"),
29 ("CRITICAL", "\x1b[35m"),
30];
31
32static SYMBOLS: &[(&str, &str)] = &[
33 ("INFO", "[+]"),
34 ("ERROR", "[-]"),
35 ("WARNING", "[!]"),
36 ("DEBUG", "[D]"),
37 ("CRITICAL", "[C]"),
38];
39
40static RESET: &str = "\x1b[0m";
41
42static VALID_MODES: &[&str] = &["simple", "simple2", "detailed", "file"];
43static VALID_SAVE_LEVELS: &[&str] = &["INFO", "ERROR", "WARNING", "DEBUG", "CRITICAL"];
44
45pub fn NewVersaLog(mode: &str, show_file: bool, show_tag: bool, tag: &str, enable_all: bool, notice: bool, all_save: bool, save_levels: Vec<String>) -> VersaLog {
46 let mode = mode.to_lowercase();
47 let tag = tag.to_string();
48
49 if !VALID_MODES.contains(&mode.as_str()) {
50 panic!("Invalid mode '{}' specified. Valid modes are: simple, simple2, detailed, file", mode);
51 }
52
53 let mut showFile = show_file;
54 let mut showTag = show_tag;
55 let mut notice_enabled = notice;
56 let mut allsave = all_save;
57 let mut savelevels = save_levels;
58
59 if enable_all {
60 showFile = true;
61 showTag = true;
62 notice_enabled = true;
63 allsave = true;
64 }
65
66 if mode == "file" {
67 showFile = true;
68 }
69
70 if allsave {
71 if savelevels.is_empty() {
72 savelevels = VALID_SAVE_LEVELS.iter().map(|s| s.to_string()).collect();
73 } else {
74 for level in &savelevels {
75 if !VALID_SAVE_LEVELS.contains(&level.as_str()) {
76 panic!("Invalid saveLevels specified. Valid levels are: {:?}", VALID_SAVE_LEVELS);
77 }
78 }
79 }
80 }
81
82 let tx = if allsave {
83 let (tx, rx) = channel::<(String, String)>();
84 thread::spawn(move || {
85 while let Ok((log_text, _level)) = rx.recv() {
86 let cwd = env::current_dir().unwrap_or_else(|_| env::current_dir().unwrap());
87 let log_dir = cwd.join("log");
88 if !log_dir.exists() {
89 let _ = fs::create_dir_all(&log_dir);
90 }
91 let today = Local::now().format("%Y-%m-%d").to_string();
92 let log_file = log_dir.join(format!("{}.log", today));
93 let log_entry = format!("{}\n", log_text);
94 let _ = fs::OpenOptions::new()
95 .create(true)
96 .append(true)
97 .write(true)
98 .open(&log_file)
99 .and_then(|mut file| file.write_all(log_entry.as_bytes()));
100 }
101 });
102 Some(tx)
103 } else { None };
104
105 VersaLog {
106 mode,
107 tag,
108 showFile,
109 showTag,
110 notice: notice_enabled,
111 enableall: enable_all,
112 allsave,
113 savelevels,
114 silent: false,
115 tx,
116 }
117}
118
119pub fn NewVersaLogSimple(mode: &str, tag: &str) -> VersaLog {
120 NewVersaLog(mode, false, false, tag, false, false, false, Vec::new())
121}
122
123pub fn NewVersaLogSimple2(mode: &str, tag: &str, enable_all: bool) -> VersaLog {
124 NewVersaLog(mode, false, false, tag, enable_all, false, false, Vec::new())
125}
126
127impl VersaLog {
128 pub fn log(&self, msg: String, level: String, tags: &[&str]) {
129 let level = level.to_uppercase();
130
131 let color = COLORS.iter()
132 .find(|(l, _)| *l == level)
133 .map(|(_, c)| *c)
134 .unwrap_or("");
135 let symbol = SYMBOLS.iter()
136 .find(|(l, _)| *l == level)
137 .map(|(_, s)| *s)
138 .unwrap_or("");
139
140 let caller = if self.showFile || self.mode == "file" {
141 self.get_caller()
142 } else {
143 String::new()
144 };
145
146 let final_tag = if !tags.is_empty() && !tags[0].is_empty() {
147 tags[0].to_string()
148 } else if self.showTag && !self.tag.is_empty() {
149 self.tag.clone()
150 } else {
151 String::new()
152 };
153
154 let (output, plain) = match self.mode.as_str() {
155 "simple" => {
156 if self.showFile {
157 if !final_tag.is_empty() {
158 let output = format!("[{}][{}]{}{}{} {}", caller, final_tag, color, symbol, RESET, msg);
159 let plain = format!("[{}][{}]{} {}", caller, final_tag, symbol, msg);
160 (output, plain)
161 } else {
162 let output = format!("[{}]{}{}{} {}", caller, color, symbol, RESET, msg);
163 let plain = format!("[{}]{} {}", caller, symbol, msg);
164 (output, plain)
165 }
166 } else {
167 if !final_tag.is_empty() {
168 let output = format!("[{}]{}{}{} {}", final_tag, color, symbol, RESET, msg);
169 let plain = format!("[{}]{} {}", final_tag, symbol, msg);
170 (output, plain)
171 } else {
172 let output = format!("{}{}{} {}", color, symbol, RESET, msg);
173 let plain = format!("{} {}", symbol, msg);
174 (output, plain)
175 }
176 }
177 },
178 "simple2" => {
179 let timestamp = self.get_time();
180 if self.showFile {
181 if !final_tag.is_empty() {
182 let output = format!("[{}] [{}][{}]{}{}{} {}", timestamp, caller, final_tag, color, symbol, RESET, msg);
183 let plain = format!("[{}] [{}][{}]{} {}", timestamp, caller, final_tag, symbol, msg);
184 (output, plain)
185 } else {
186 let output = format!("[{}] [{}]{}{}{} {}", timestamp, caller, color, symbol, RESET, msg);
187 let plain = format!("[{}] [{}]{} {}", timestamp, caller, symbol, msg);
188 (output, plain)
189 }
190 } else {
191 let output = format!("[{}] {}{}{} {}", timestamp, color, symbol, RESET, msg);
192 let plain = format!("[{}] {} {}", timestamp, symbol, msg);
193 (output, plain)
194 }
195 },
196 "file" => {
197 let output = format!("[{}]{}{}[{}]{}", caller, color, level, RESET, msg);
198 let plain = format!("[{}][{}] {}", caller, level, msg);
199 (output, plain)
200 },
201 _ => {
202 let timestamp = self.get_time();
203 let mut output = format!("[{}]{}{}[{}]", timestamp, color, level, RESET);
204 let mut plain = format!("[{}][{}]", timestamp, level);
205
206 if !final_tag.is_empty() {
207 output.push_str(&format!("[{}]", final_tag));
208 plain.push_str(&format!("[{}]", final_tag));
209 }
210
211 if self.showFile {
212 output.push_str(&format!("[{}]", caller));
213 plain.push_str(&format!("[{}]", caller));
214 }
215
216 output.push_str(&format!(" : {}", msg));
217 plain.push_str(&format!(" : {}", msg));
218
219 (output, plain)
220 }
221 };
222
223 if !self.silent {
224 println!("{}", output);
225 }
226 self.save_log(plain, level.clone());
227
228 if self.notice && (level == "ERROR" || level == "CRITICAL") {
229 let _ = Notification::new()
230 .summary(&format!("{} Log notice", level))
231 .body(&msg)
232 .show();
233 }
234 }
235
236 pub fn set_silent(&mut self, silent: bool) {
237 self.silent = silent;
238 }
239
240 pub fn install_panic_hook(self: std::sync::Arc<Self>) {
241 let logger = self.clone();
242 panic::set_hook(Box::new(move |info| {
243 let payload = info.payload();
244 let msg = if let Some(s) = payload.downcast_ref::<&str>() {
245 (*s).to_string()
246 } else if let Some(s) = payload.downcast_ref::<String>() {
247 s.clone()
248 } else {
249 "unknown panic".to_string()
250 };
251
252 let mut details = String::new();
253 if let Some(loc) = info.location() {
254 details.push_str(&format!("at {}:{}:{}\n", loc.file(), loc.line(), loc.column()));
255 }
256 let bt = Backtrace::new();
257 details.push_str(&format!("{:?}", bt));
258
259 logger.Critical_no_tag(&format!("Unhandled panic: {}\n{}", msg, details));
260 }));
261 }
262
263 pub fn Info(&self, msg: &str, tags: &[&str]) {
264 self.log(msg.to_string(), "INFO".to_string(), tags);
265 }
266
267 pub fn Error(&self, msg: &str, tags: &[&str]) {
268 self.log(msg.to_string(), "ERROR".to_string(), tags);
269 }
270
271 pub fn Warning(&self, msg: &str, tags: &[&str]) {
272 self.log(msg.to_string(), "WARNING".to_string(), tags);
273 }
274
275 pub fn Debug(&self, msg: &str, tags: &[&str]) {
276 self.log(msg.to_string(), "DEBUG".to_string(), tags);
277 }
278
279 pub fn Critical(&self, msg: &str, tags: &[&str]) {
280 self.log(msg.to_string(), "CRITICAL".to_string(), tags);
281 }
282
283 pub fn info(&self, msg: &str, tags: &[&str]) {
284 self.Info(msg, tags);
285 }
286
287 pub fn error(&self, msg: &str, tags: &[&str]) {
288 self.Error(msg, tags);
289 }
290
291 pub fn warning(&self, msg: &str, tags: &[&str]) {
292 self.Warning(msg, tags);
293 }
294
295 pub fn debug(&self, msg: &str, tags: &[&str]) {
296 self.Debug(msg, tags);
297 }
298
299 pub fn critical(&self, msg: &str, tags: &[&str]) {
300 self.Critical(msg, tags);
301 }
302
303 pub fn Info_no_tag(&self, msg: &str) {
304 self.log(msg.to_string(), "INFO".to_string(), &[]);
305 }
306
307 pub fn Error_no_tag(&self, msg: &str) {
308 self.log(msg.to_string(), "ERROR".to_string(), &[]);
309 }
310
311 pub fn Warning_no_tag(&self, msg: &str) {
312 self.log(msg.to_string(), "WARNING".to_string(), &[]);
313 }
314
315 pub fn Debug_no_tag(&self, msg: &str) {
316 self.log(msg.to_string(), "DEBUG".to_string(), &[]);
317 }
318
319 pub fn Critical_no_tag(&self, msg: &str) {
320 self.log(msg.to_string(), "CRITICAL".to_string(), &[]);
321 }
322
323 fn get_time(&self) -> String {
324 Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
325 }
326
327 fn get_caller(&self) -> String {
328 let bt = Backtrace::new();
329 if let Some(frame) = bt.frames().get(3) {
330 if let Some(symbol) = frame.symbols().first() {
331 if let Some(file) = symbol.filename() {
332 if let Some(file_name) = file.file_name() {
333 if let Some(line) = symbol.lineno() {
334 return format!("{}:{}", file_name.to_string_lossy(), line);
335 }
336 }
337 }
338 }
339 }
340 "unknown:0".to_string()
341 }
342
343 fn save_log(&self, log_text: String, level: String) {
344 if !self.allsave || !self.savelevels.contains(&level) {
345 return;
346 }
347
348 if let Some(tx) = &self.tx {
349 let _ = tx.send((log_text, level));
350 return;
351 }
352
353 let cwd = env::current_dir().unwrap_or_else(|_| env::current_dir().unwrap());
354 let log_dir = cwd.join("log");
355 if !log_dir.exists() {
356 let _ = fs::create_dir_all(&log_dir);
357 }
358 let today = Local::now().format("%Y-%m-%d").to_string();
359 let log_file = log_dir.join(format!("{}.log", today));
360 let log_entry = format!("{}\n", log_text);
361 let _ = fs::OpenOptions::new()
362 .create(true)
363 .append(true)
364 .write(true)
365 .open(&log_file)
366 .and_then(|mut file| file.write_all(log_entry.as_bytes()));
367 }
368}