pub mod display;
pub mod sound;
use std::{
io::{self, Write},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::{Duration, Instant},
};
use colored::Colorize;
use crossterm::{
cursor,
event::{self, Event, KeyCode},
execute,
terminal::{self, ClearType},
};
use tokio::{signal, time::sleep};
use crate::timer::{
display::{get_ascii_cancelled, get_ascii_colon, get_ascii_digit},
sound::{play_end_sound, play_start_sound},
};
pub struct Timer {
pub duration: Duration,
pub start_time: Option<Instant>,
pub is_running: Arc<AtomicBool>,
}
impl Timer {
pub fn new(duration: Duration) -> Self {
Timer {
duration,
start_time: None,
is_running: Arc::new(AtomicBool::new(false)),
}
}
pub fn format_time_as_ascii(total_secs: u64) -> Vec<String> {
let hours = total_secs / 3600;
let mins = (total_secs % 3600) / 60;
let secs = total_secs % 60;
let digits = if hours > 0 {
let hour_tens = (hours / 10 % 10) as u8;
let hour_ones = (hours % 10) as u8;
let min_tens = (mins / 10) as u8;
let min_ones = (mins % 10) as u8;
let sec_tens = (secs / 10) as u8;
let sec_ones = (secs % 10) as u8;
vec![
get_ascii_digit(hour_tens),
get_ascii_digit(hour_ones),
get_ascii_colon(),
get_ascii_digit(min_tens),
get_ascii_digit(min_ones),
get_ascii_colon(),
get_ascii_digit(sec_tens),
get_ascii_digit(sec_ones),
]
} else if mins > 0 {
let min_tens = (mins / 10) as u8;
let min_ones = (mins % 10) as u8;
let sec_tens = (secs / 10) as u8;
let sec_ones = (secs % 10) as u8;
vec![
get_ascii_digit(min_tens),
get_ascii_digit(min_ones),
get_ascii_colon(),
get_ascii_digit(sec_tens),
get_ascii_digit(sec_ones),
]
} else {
let sec_tens = (secs / 10) as u8;
let sec_ones = (secs % 10) as u8;
vec![get_ascii_digit(sec_tens), get_ascii_digit(sec_ones)]
};
let mut lines = vec![String::new(); 5];
for line_idx in 0..5 {
for digit in &digits {
lines[line_idx].push_str(digit[line_idx]);
}
}
lines
}
pub async fn start(&mut self) -> io::Result<()> {
self.is_running.store(true, Ordering::SeqCst);
self.start_time = Some(Instant::now());
play_start_sound().await;
terminal::enable_raw_mode()?;
execute!(
io::stdout(),
terminal::Clear(ClearType::All),
cursor::Hide,
cursor::MoveTo(0, 0)
)?;
let is_running_ctrl = self.is_running.clone();
tokio::spawn(async move {
signal::ctrl_c().await.unwrap();
is_running_ctrl.store(false, Ordering::SeqCst);
});
let is_running = self.is_running.clone();
tokio::spawn(async move {
loop {
if event::poll(Duration::from_millis(100)).unwrap() {
if let Event::Key(key) = event::read().unwrap() {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => {
is_running.store(false, Ordering::SeqCst);
break;
}
KeyCode::Char('c')
if key.modifiers.contains(event::KeyModifiers::CONTROL) =>
{
is_running.store(false, Ordering::SeqCst);
break;
}
_ => {}
}
}
}
}
});
let mut elapsed = Duration::from_secs(0);
while elapsed < self.duration && self.is_running.load(Ordering::SeqCst) {
elapsed = self.start_time.unwrap().elapsed();
let remaining = self.duration.saturating_sub(elapsed);
let ascii_lines = Self::format_time_as_ascii(remaining.as_secs());
for (i, line) in ascii_lines.iter().enumerate() {
execute!(
io::stdout(),
cursor::MoveTo(0, i as u16),
terminal::Clear(ClearType::CurrentLine)
)?;
print!("{}", line.bright_cyan().bold());
io::stdout().flush()?;
}
sleep(Duration::from_millis(100)).await;
}
terminal::disable_raw_mode()?;
execute!(io::stdout(), cursor::Show)?;
if elapsed >= self.duration {
execute!(
io::stdout(),
terminal::Clear(ClearType::All),
cursor::MoveTo(0, 0)
)?;
let ascii_lines = Self::format_time_as_ascii(0);
for line in ascii_lines.iter() {
println!("{}", line.bright_green().bold());
}
play_end_sound().await;
} else {
execute!(
io::stdout(),
terminal::Clear(ClearType::All),
cursor::MoveTo(0, 0)
)?;
let cancelled_message = get_ascii_cancelled();
for line in cancelled_message.iter() {
println!("{}", line.bright_red().bold());
}
println!();
}
Ok(())
}
}