use chrono::Local;
use std::cell::RefCell;
use std::io::Write;
use std::sync::{Arc, Mutex};
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub enum LogLevel {
Verbose,
Debug,
Info,
Warning,
Error,
Fatal,
Fixed,
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
LogLevel::Verbose => "VERBOSE",
LogLevel::Debug => " DEBUG",
LogLevel::Info => " INFO",
LogLevel::Warning => "WARNING",
LogLevel::Error => " ERROR",
LogLevel::Fatal => " FATAL",
LogLevel::Fixed => " FIXED",
};
write!(f, "{}", s)
}
}
impl LogLevel {
fn color(&self) -> &'static str {
match self {
LogLevel::Verbose => "\x1b[90m", LogLevel::Debug => "\x1b[96m", LogLevel::Info => "\x1b[92m", LogLevel::Warning => "\x1b[93m", LogLevel::Error => "\x1b[91m", LogLevel::Fatal => "\x1b[95m", LogLevel::Fixed => "\x1b[97m", }
}
fn icon(&self) -> &'static str {
match self {
LogLevel::Verbose => "đŦ",
LogLevel::Debug => "đ",
LogLevel::Info => "âšī¸",
LogLevel::Warning => "â ī¸",
LogLevel::Error => "â",
LogLevel::Fatal => "đ",
LogLevel::Fixed => "â
",
}
}
}
thread_local! {
pub static LOG_MODULE: RefCell<&'static str> = const { RefCell::new("") };
}
#[macro_export]
macro_rules! log_module {
($name:expr) => {{
$crate::LOG_MODULE.with(|m| *m.borrow_mut() = $name);
}};
}
pub struct Logger {
buffer: String,
current_level: LogLevel,
pub console_threshold: LogLevel,
pub file_threshold: LogLevel,
pub file_logging_enabled: bool,
pub use_colors: bool,
pub include_date: bool,
pub use_icons_in_file: bool,
log_file: Option<std::fs::File>,
pub log_file_path: Option<String>,
}
impl Logger {
pub fn new() -> Self {
Self {
buffer: String::with_capacity(1024),
current_level: LogLevel::Info,
console_threshold: LogLevel::Verbose,
file_threshold: LogLevel::Verbose,
file_logging_enabled: false,
use_colors: true,
include_date: true,
use_icons_in_file: false,
log_file: None,
log_file_path: None,
}
}
pub fn append<T: std::fmt::Display>(&mut self, value: T) {
self.buffer.push_str(&format!("{} ", value));
}
pub fn append_bool(&mut self, value: bool) {
self.buffer.push_str(if value { "true " } else { "false " });
}
pub fn append_hex<T: std::fmt::UpperHex>(&mut self, value: T) {
self.buffer.push_str(&format!("0x{:X} ", value));
}
fn reset(&mut self) {
self.buffer.clear();
self.current_level = LogLevel::Info;
}
fn timestamp(&self) -> String {
let now = Local::now();
if self.include_date {
format!("{} | ", now.format("%Y-%m-%d %H:%M:%S%.6f"))
} else {
format!("{} | ", now.format("%H:%M:%S%.6f"))
}
}
pub fn print(&mut self) {
let timestamp = self.timestamp();
let module = crate::LOG_MODULE.with(|m| *m.borrow());
if self.current_level >= self.console_threshold {
let msg = if self.use_colors {
format!(
"{}{}{:>8} | {:<8} | {}\x1b[0m\n",
self.current_level.color(),
timestamp,
self.current_level,
module,
self.buffer
)
} else {
format!(
"{}{:>8} | {:<8} | {}\n",
timestamp, self.current_level, module, self.buffer
)
};
print!("{}", msg);
}
if self.file_logging_enabled {
if let Some(file) = &mut self.log_file {
let level_repr = if self.use_icons_in_file {
self.current_level.icon()
} else {
&self.current_level.to_string()
};
let file_message = format!(
"{}{:>8} | {:<8} | {}\n",
timestamp, level_repr, module, self.buffer
);
let _ = file.write_all(file_message.as_bytes());
}
}
self.reset();
}
pub fn set_level(&mut self, level: LogLevel) {
self.current_level = level;
}
pub fn set_console_threshold(&mut self, level: LogLevel) {
self.console_threshold = level;
}
pub fn set_file_threshold(&mut self, level: LogLevel) {
self.file_threshold = level;
}
pub fn enable_file_logging(&mut self) {
if !self.file_logging_enabled {
let filename = format!("log_{}.txt", chrono::Local::now().format("%Y%m%d_%H%M%S"));
self.log_file = Some(
std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&filename)
.unwrap(),
);
self.log_file_path = Some(filename);
self.file_logging_enabled = true;
}
}
pub fn disable_file_logging(&mut self) {
self.log_file = None;
self.file_logging_enabled = false;
self.log_file_path = None;
}
}
impl Default for Logger {
fn default() -> Self {
Self::new()
}
}
lazy_static::lazy_static! {
pub static ref LOGGER: Arc<Mutex<Logger>> = Arc::new(Mutex::new(Logger::new()));
}
#[macro_export]
macro_rules! log_str {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: &str = $v; logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_bool {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: bool = $v;
logger.append_bool($v);
}
};
}
#[macro_export]
macro_rules! log_ptr {
($v:expr) => {
|logger: &mut $crate::Logger| {
logger.append(format!("{:p}", $v));
}
};
}
#[macro_export]
macro_rules! log_hex8 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: u8 = $v;
logger.append_hex($v);
}
};
}
#[macro_export]
macro_rules! log_hex16 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: u16 = $v;
logger.append_hex($v);
}
};
}
#[macro_export]
macro_rules! log_hex32 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: u32 = $v;
logger.append_hex($v);
}
};
}
#[macro_export]
macro_rules! log_hex64 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: u64 = $v;
logger.append_hex($v);
}
};
}
#[macro_export]
macro_rules! log_i8 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: i8 = $v;
logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_i16 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: i16 = $v;
logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_i32 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: i32 = $v;
logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_i64 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: i64 = $v;
logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_u8 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: u8 = $v;
logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_u16 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: u16 = $v;
logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_u32 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: u32 = $v;
logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_u64 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: u64 = $v;
logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_f32 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: f32 = $v;
logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_f64 {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: f64 = $v;
logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_char {
($v:expr) => {
|logger: &mut $crate::Logger| {
let _: char = $v;
logger.append($v);
}
};
}
#[macro_export]
macro_rules! log_print {
($level:expr, $($val:expr),+ $(,)?) => {{
let mut logger = $crate::LOGGER.lock().unwrap();
logger.set_level($level);
$( $val(&mut logger); )+
logger.print();
}};
}
#[macro_export]
macro_rules! log_init {
($console:expr, $file:expr, $enable_file:expr, $enable_colors:expr, $include_date:expr, $use_icons:expr) => {{
let mut logger = $crate::LOGGER.lock().unwrap();
logger.set_console_threshold($console);
logger.set_file_threshold($file);
logger.use_colors = $enable_colors;
logger.include_date = $include_date;
logger.use_icons_in_file = $use_icons;
if $enable_file {
logger.enable_file_logging();
} else {
logger.disable_file_logging();
}
}};
}
#[macro_export]
macro_rules! log_deinit {
() => {{
let mut logger = $crate::LOGGER.lock().unwrap();
logger.disable_file_logging();
}};
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn reset_logger() {
let mut logger = LOGGER.lock().unwrap();
logger.disable_file_logging();
logger.buffer.clear();
logger.console_threshold = LogLevel::Verbose;
logger.file_threshold = LogLevel::Verbose;
logger.use_colors = false;
logger.include_date = false;
logger.use_icons_in_file = false;
LOG_MODULE.with(|m| *m.borrow_mut() = "");
}
#[test]
fn test_basic_logging() {
reset_logger();
log_module!("TESTMOD");
log_print!(
LogLevel::Info,
log_str!("Hello"),
log_i32!(42),
log_bool!(true)
);
let logger = LOGGER.lock().unwrap();
assert!(logger.buffer.is_empty());
}
#[test]
fn test_hex_logging() {
reset_logger();
log_module!("HEXMOD");
log_print!(
LogLevel::Debug,
log_hex8!(0xABu8),
log_hex16!(0x1234u16),
log_hex32!(0xDEADBEEFu32),
log_hex64!(0xDEADBEEFFEEDC0DEu64)
);
let logger = LOGGER.lock().unwrap();
assert!(logger.buffer.is_empty());
}
#[test]
fn test_pointer_logging() {
reset_logger();
log_module!("PTR");
let x = 123u32;
log_print!(LogLevel::Info, log_ptr!(&x));
let logger = LOGGER.lock().unwrap();
assert!(logger.buffer.is_empty());
}
#[test]
fn test_file_logging_creates_file() {
reset_logger();
log_module!("FILE");
{
let mut logger = LOGGER.lock().unwrap();
logger.enable_file_logging();
}
log_print!(
LogLevel::Info,
log_str!("File logging test"),
log_i32!(2025)
);
let logger = LOGGER.lock().unwrap();
assert!(logger.file_logging_enabled);
if let Some(path) = &logger.log_file_path {
assert!(Path::new(path).exists());
let _ = std::fs::remove_file(path);
}
}
#[test]
fn test_log_levels() {
reset_logger();
log_module!("LEVEL");
{
let mut logger = LOGGER.lock().unwrap();
logger.set_console_threshold(LogLevel::Error);
logger.set_file_threshold(LogLevel::Warning);
}
log_print!(LogLevel::Info, log_str!("This should not print"));
log_print!(LogLevel::Error, log_str!("This should print"));
let logger = LOGGER.lock().unwrap();
assert!(logger.buffer.is_empty());
}
#[test]
fn test_logging_all_types() {
reset_logger();
log_module!("ALLTYPES");
let x = 123;
log_print!(
LogLevel::Info,
log_str!("Hello"),
log_i8!(1),
log_i16!(2),
log_i32!(42),
log_i64!(12345),
log_u8!(0xAB),
log_u16!(0x1234u16),
log_u32!(0xDEADBEEFu32),
log_u64!(0xDEADBEEFFEEDC0DEu64),
log_f32!(3.14),
log_f64!(2.71828),
log_bool!(true),
log_ptr!(&x),
log_char!('X')
);
let logger = LOGGER.lock().unwrap();
assert!(logger.buffer.is_empty());
}
#[test]
fn test_file_logging_with_icons() {
reset_logger();
log_module!("ICONMOD");
{
let mut logger = LOGGER.lock().unwrap();
logger.use_icons_in_file = true;
logger.enable_file_logging();
}
log_print!(LogLevel::Warning, log_str!("Warning with icon"));
let logger = LOGGER.lock().unwrap();
assert!(logger.file_logging_enabled);
if let Some(path) = &logger.log_file_path {
assert!(Path::new(path).exists());
let _ = std::fs::remove_file(path);
}
}
#[test]
fn test_module_name_in_output_console() {
reset_logger();
log_module!("MODX");
log_print!(LogLevel::Info, log_str!("Test123"));
let logger = LOGGER.lock().unwrap();
assert!(logger.buffer.is_empty());
}
#[test]
fn test_module_name_in_file_output() {
reset_logger();
log_module!("FILEMOD");
{
let mut logger = LOGGER.lock().unwrap();
logger.enable_file_logging();
}
log_print!(LogLevel::Error, log_str!("ErrData"));
let path;
{
let logger = LOGGER.lock().unwrap();
path = logger.log_file_path.clone();
}
assert!(path.is_some());
let path = path.unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("FILEMOD"));
assert!(content.contains("ErrData"));
let _ = std::fs::remove_file(path);
}
#[test]
fn test_module_empty_when_not_set() {
reset_logger();
{
let mut logger = LOGGER.lock().unwrap();
logger.enable_file_logging();
}
log_print!(LogLevel::Info, log_str!("NoModuleTest"));
let path = LOGGER.lock().unwrap().log_file_path.clone().unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(!content.contains(" | | ")); assert!(content.contains("NoModuleTest"));
let _ = std::fs::remove_file(path);
}
}