focuz 0.0.1

Lightweight Beautiful Terminal Pomodoro Timer
Documentation
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();

        // Handle SIGINT (Ctrl+C) signal
        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);

            // Display ASCII art time
            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;
        }

        // Restore terminal before checking completion
        terminal::disable_raw_mode()?;
        execute!(io::stdout(), cursor::Show)?;

        if elapsed >= self.duration {
            // Clear and show final state
            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 {
            // Clear screen and exit
            execute!(
                io::stdout(),
                terminal::Clear(ClearType::All),
                cursor::MoveTo(0, 0)
            )?;

            // Display cancellation message in large ASCII art
            let cancelled_message = get_ascii_cancelled();
            for line in cancelled_message.iter() {
                println!("{}", line.bright_red().bold());
            }
            println!();
        }

        Ok(())
    }
}