1#[derive(Debug, Clone)]
2pub enum LogOutput {
3 Log(log::Level),
4 LogTarget(log::Level, String),
5 StdOut,
6 StdErr,
7 #[cfg(feature = "log-to-file")]
8 File(std::sync::Arc<parking_lot::Mutex<std::fs::File>>),
9}
10
11impl From<log::Level> for LogOutput {
12 fn from(value: log::Level) -> Self {
13 Self::Log(value)
14 }
15}
16
17impl LogOutput {
18 fn print(&self, msg: &str) {
19 match self {
20 LogOutput::Log(level) => log::log!(*level, "{}", msg),
21 LogOutput::LogTarget(level, target) => {
22 log::log!(target: target, *level, "{}", msg)
23 }
24 LogOutput::StdOut => println!("{}", msg),
25 LogOutput::StdErr => eprintln!("{}", msg),
26 #[cfg(feature = "log-to-file")]
27 LogOutput::File(file) => {
28 use std::io::Write;
29
30 let mut file = file.lock();
31 file.write_all(msg.as_bytes()).ok();
32 file.write_all(b"\n").ok();
33 }
34 }
35 }
36}
37
38#[derive(Debug)]
39pub struct LogItem {
40 level: usize,
41 title: String,
42 value: Option<String>,
43}
44
45impl LogItem {
46 pub fn new<T: Into<String>, V: Into<String>>(level: usize, title: T, value: Option<V>) -> Self {
47 Self {
48 level,
49 title: title.into(),
50 value: value.map(Into::into),
51 }
52 }
53}
54
55impl<T: ToString, V: ToString> From<(usize, T, V)> for LogItem {
56 fn from((level, title, value): (usize, T, V)) -> Self {
57 Self::new(level, title.to_string(), Some(value.to_string()))
58 }
59}
60
61impl<T: ToString> From<(usize, T)> for LogItem {
62 fn from((level, value): (usize, T)) -> Self {
63 Self::new::<_, String>(level, value.to_string(), None)
64 }
65}
66
67pub struct Logger;
68
69impl Logger {
70 pub fn log<T>(output: &LogOutput, loggable: &T)
71 where
72 T: Loggable,
73 {
74 Self::log_impl(output, &loggable.as_log())
75 }
76
77 fn log_impl(output: &LogOutput, items: &[LogItem]) {
78 if let Some(v) = items.first() {
80 output.print(&v.title)
81 }
82
83 let right_align = items
84 .iter()
85 .skip(1)
86 .map(|v| v.title.len())
87 .max()
88 .unwrap_or(0);
89
90 items.iter().skip(1).for_each(|i| {
91 let LogItem {
92 level,
93 title,
94 value,
95 } = i;
96
97 let front_padding: String = (0..level * 2).map(|_| ' ').collect();
98
99 let (value, value_padding) = if let Some(value) = value {
100 let value_padding: String = (0..(right_align - title.len())).map(|_| ' ').collect();
101 (value.as_str(), value_padding)
102 } else {
103 ("", String::new())
104 };
105
106 let message = format!("{front_padding}{title}: {value_padding}{value}");
107 output.print(&message);
108 })
109 }
110}
111
112pub trait Loggable {
113 fn as_log(&self) -> Vec<LogItem>;
114}
115
116#[macro_export]
117macro_rules ! log_vec {
118 [$($msg:tt)*] => {
119 $crate::to_log!(vec: $($msg)*)
120 }
121}
122
123#[macro_export]
124macro_rules! to_log {
125 ([$($array:tt)*],) => {
126 vec![$($array)*]
127 };
128
129 ([$($array:tt)*], ($level:literal, $title:expr, $value:expr)) => {
130 $crate::to_log!([$($array)* ($level, $title, $value).into(),],)
131 };
132
133 ([$($array:tt)*], ($level:literal, $title:expr)) => {
134 $crate::to_log!([$($array)* ($level, $title, "").into(),],)
135 };
136
137 ([$($array:tt)*], ($level:literal, $title:expr, $value:expr), $($msg:tt)*) => {
138 $crate::to_log!([$($array)* ($level, $title, $value).into(),], $($msg)*)
139 };
140
141 ([$($array:tt)*], ($level:literal, $title:expr), $($msg:tt)*) => {
142 $crate::to_log!([$($array)* ($level, $title, "").into(),], $($msg)*)
143 };
144
145 (vec: $($msg:tt)*) => {
146 $crate::to_log!([], $($msg)*)
147 };
148}