use std::fmt::{ Display, Formatter, Result };
use std::thread;
use std::time::Duration;
use std::sync::{ Arc, RwLock };
use std::io::prelude::*;
use std::io;
use chrono::{ Timelike, Utc };
use colored::*;
pub enum LogIcon {
Tick,
Cross,
Info,
Warning,
Heart
}
impl Display for LogIcon {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let (mut t, mut c, mut i, mut w, mut h) = ("✔", "✖", "ℹ", "⚠", "♥");
if cfg!(windows) {
t = "√";
c = "×";
i = "i";
w = "‼";
h = "♥";
}
match *self {
LogIcon::Tick => write!(f, "{}", t),
LogIcon::Cross => write!(f, "{}", c),
LogIcon::Info => write!(f, "{}", i),
LogIcon::Warning => write!(f, "{}", w),
LogIcon::Heart => write!(f, "{}", h)
}
}
}
macro_rules! output {
($text:expr, $self:ident) => (
match $self.same_line {
true => print!("{}{}", $self.timestamp(), $text),
false => println!("{}{}", $self.timestamp(), $text)
}
);
($text:expr, $icon:expr, $self:ident) => (
match $self.same_line {
true => print!("{} {}{}", $icon, $self.timestamp(), $text),
false => println!("{} {}{}", $icon, $self.timestamp(), $text)
}
$self.same_line = false;
);
($text:expr, $icon:expr, $self:ident, $e:ident) => (
match $self.same_line {
true => eprint!("{} {}{}", $icon, $self.timestamp(), $text),
false => eprintln!("{} {}{}", $icon, $self.timestamp(), $text)
}
$self.same_line = false;
);
}
pub struct Logger {
is_loading: Arc<RwLock<bool>>,
loading_message: String, loading_handle: Option<thread::JoinHandle<()>>,
with_timestamp: bool,
same_line: bool
}
impl Logger {
pub fn new(timestamp: bool) -> Logger {
Logger {
is_loading : Arc::new(RwLock::new(false)),
loading_message : String::from(""),
loading_handle : None,
with_timestamp : if timestamp { true } else { false },
same_line : false
}
}
pub fn log<T: Display>(&mut self, message: T) -> &mut Logger {
output!(message, self);
self
}
pub fn info<T: Display>(&mut self, message: T) -> &mut Logger {
self.done();
let icon = format!("{}", LogIcon::Info);
output!(message, icon.cyan(), self);
self
}
pub fn success<T: Display>(&mut self, message: T) -> &mut Logger {
self.done();
let icon = format!("{}", LogIcon::Tick);
output!(message, icon.green(), self);
self
}
pub fn warn<T: Display>(&mut self, message: T) -> &mut Logger {
self.done();
let icon = format!("{}", LogIcon::Warning);
output!(message, icon.yellow(), self, true);
self
}
pub fn error<T: Display>(&mut self, message: T) -> &mut Logger {
self.done();
let icon = format!("{}", LogIcon::Cross);
output!(message, icon.red(), self, true);
self
}
pub fn newline(&mut self, amount: usize) -> &mut Logger {
self.done();
print!("{}", "\n".repeat(amount));
self
}
pub fn indent(&mut self, amount: usize) -> &mut Logger {
self.done();
print!("{}", "\t".repeat(amount));
self
}
pub fn loading<T: Display>(&mut self, message: T) -> &mut Logger {
let mut status = self.is_loading.write().unwrap();
*status = true;
drop(status);
let status = self.is_loading.clone();
let thread_message = message.to_string().clone();
self.loading_message = message.to_string();
self.loading_handle = Some(thread::spawn(move || {
let frames: [&str; 6] = ["⠦", "⠇", "⠋", "⠙", "⠸", "⠴"];
let mut i = 1;
while *status.read().unwrap() {
if i == frames.len() {
i = 0;
}
print!("\r{} {}", frames[i].cyan(), thread_message);
io::stdout().flush().unwrap();
thread::sleep(Duration::from_millis(100));
i = i + 1;
}
}));
self
}
pub fn done(&mut self) -> &mut Logger {
if !*self.is_loading.read().unwrap() {
return self;
}
let mut status = self.is_loading.write().unwrap();
*status = false;
drop(status);
self.loading_handle
.take().expect("Called stop on a non-existing thread")
.join().expect("Could not join spawned thread");
let clearing_length = self.loading_message.len() + 5;
print!("\r{}\r", " ".repeat(clearing_length));
io::stdout().flush().unwrap();
self
}
pub fn same(&mut self) -> &mut Logger {
self.same_line = true;
self
}
fn timestamp(&self) -> ColoredString {
if !self.with_timestamp {
return String::from("").normal();
}
let now = Utc::now();
let (is_pm, hour) = now.hour12();
format!(
"{:02}:{:02}:{:02}.{:03} {} > ",
hour,
now.minute(),
now.second(),
now.nanosecond() / 1_000_000,
if is_pm { "PM" } else { "AM" }
).bold()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timestamp() {
let mut logger = Logger::new(false);
assert_eq!(logger.with_timestamp, false);
logger.info("It doesn't have a timestamp");
let mut logger = Logger::new(true);
assert_eq!(logger.with_timestamp, true);
logger.info("It has a timestamp");
}
#[test]
fn loading() {
let mut logger = Logger::new(false);
logger.loading("Loading in the middle of a test is not good!");
logger.done().success("Done loading!");
logger.info("About to load again");
logger
.loading("Loading something else")
.done()
.error("Done loading instantly lol");
}
#[test]
fn same() {
let mut logger = Logger::new(false);
logger
.same().success("This is on one line")
.indent(1)
.info("This is on the same line!!!")
.error("But this one isn't");
logger.same();
assert!(logger.same_line);
}
#[test]
fn it_works() {
let mut logger = Logger::new(true);
logger
.info("Somebody")
.error("Once")
.warn("Told")
.success("Me")
.newline(5)
.log("A basic log eh")
.indent(2)
.info("If it didn't crash it's fine");
assert_eq!(logger.with_timestamp, true);
}
}