clock-rs 0.2.1

A modern, terminal-based digital clock
use std::{
    io::{self, BufWriter, Write},
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
    time::Duration,
};

use clap::Parser;
use crossterm::{
    cursor::{Hide, MoveTo, Show},
    event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
    execute,
    terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};

#[cfg(unix)]
use signal_hook::{consts, flag};

use crate::{
    cli::args::{Args, Mode, TimerArgs},
    clock::{
        counter::{Counter, CounterType},
        mode::ClockMode,
        time_zone::TimeZone,
        Clock,
    },
    config::Config,
    error::Error,
};

pub struct State {
    clock: Clock,
}

impl State {
    pub fn new() -> Result<Self, Error> {
        let args = Args::parse();
        let mut config = Config::parse()?;
        let mode = args.mode.clone();

        args.overwrite(&mut config)?;

        let clock_mode = Self::clock_mode(mode, &config)?;
        let mut clock = Clock::new(config, clock_mode);

        let (width, height) = terminal::size().map_err(Error::Io)?;
        clock.update_padding(width, height)?;

        Ok(Self { clock })
    }

    fn clock_mode(mode: Option<Mode>, config: &Config) -> Result<ClockMode, Error> {
        let TimerArgs {
            seconds,
            minutes,
            hours,
            kill,
        } = match mode {
            Some(Mode::Clock) | None => {
                return Ok(ClockMode::Time {
                    time_zone: TimeZone::from_utc(config.date.utc),
                    date_format: config.date.fmt.clone(),
                });
            }
            Some(Mode::Stopwatch) => {
                return Ok(ClockMode::Counter(Counter::new(CounterType::Stopwatch)))
            }
            Some(Mode::Timer(timer_args)) => timer_args,
        };

        let total_seconds = match (seconds, minutes, hours) {
            (None, None, None) => Counter::DEFAULT_TIMER_DURATION,
            _ => {
                let seconds = seconds.unwrap_or_default();
                let minutes = minutes.unwrap_or_default();
                let hours = hours.unwrap_or_default();
                let total_seconds = hours * 3600 + minutes * 60 + seconds;

                if total_seconds > Counter::MAX_TIMER_DURATION {
                    return Err(Error::TimerDurationTooLong {
                        hours,
                        minutes,
                        seconds,
                    });
                }

                total_seconds
            }
        };

        Ok(ClockMode::Counter(Counter::new(CounterType::Timer {
            duration: Duration::from_secs(total_seconds),
            kill,
        })))
    }

    pub fn run(mut self) -> Result<(), Error> {
        terminal::enable_raw_mode()?;
        execute!(io::stdout(), EnterAlternateScreen, Hide)?;

        let reload_config = Arc::new(AtomicBool::new(false));

        #[cfg(unix)]
        flag::register(consts::SIGUSR1, Arc::clone(&reload_config))?;

        loop {
            if reload_config.swap(false, Ordering::Relaxed) {
                self.reload_config()?;
            }

            self.render()?;

            if !event::poll(self.clock.interval)? {
                continue;
            }

            match event::read()? {
                Event::Key(key_event) => match key_event {
                    KeyEvent {
                        code: KeyCode::Esc | KeyCode::Char('Q' | 'q'),
                        modifiers: KeyModifiers::NONE,
                        ..
                    }
                    | KeyEvent {
                        code: KeyCode::Char('c'),
                        modifiers: KeyModifiers::CONTROL,
                        ..
                    } => return Ok(()),
                    KeyEvent {
                        code: KeyCode::Char('r'),
                        modifiers: KeyModifiers::CONTROL,
                        ..
                    } => reload_config.store(true, Ordering::Relaxed),
                    KeyEvent {
                        code: KeyCode::Char(character @ ('P' | 'p' | 'R' | 'r')),
                        kind: KeyEventKind::Press,
                        modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
                        ..
                    } => {
                        let ClockMode::Counter(counter) = &mut self.clock.mode else {
                            continue;
                        };

                        match character {
                            'P' | 'p' => counter.toggle_pause(),
                            _ => counter.restart(),
                        }

                        let (width, height) = terminal::size()?;
                        self.refresh_display(width, height)?;
                    }
                    _ => (),
                },
                Event::Resize(width, height) => self.refresh_display(width, height)?,

                _ => (),
            }
        }
    }

    pub fn exit() {
        execute!(io::stdout(), LeaveAlternateScreen, Show).expect(
            "error: failed to leave alternate screen, you might have to restart your terminal",
        );
        terminal::disable_raw_mode()
            .expect("error: failed to disable raw mode, you might have to restart your terminal");
    }

    fn refresh_display(&mut self, width: u16, height: u16) -> Result<(), Error> {
        execute!(io::stdout(), Clear(ClearType::All))?;
        self.clock.update_padding(width, height)
    }

    fn reload_config(&mut self) -> Result<(), Error> {
        let clock = &mut self.clock;
        let config = Config::parse()?;

        clock.color = config.general.color;
        clock.interval = Duration::from_millis(config.general.interval);
        clock.blink = config.general.blink;
        clock.bold = config.general.bold;

        clock.x_pos = config.position.x;
        clock.y_pos = config.position.y;

        clock.use_12h = config.date.use_12h;
        clock.hide_seconds = config.date.hide_seconds;

        if let ClockMode::Time {
            time_zone,
            date_format,
        } = &mut self.clock.mode
        {
            *time_zone = TimeZone::from_utc(config.date.utc);
            *date_format = config.date.fmt;
        }

        let (width, height) = terminal::size()?;
        self.refresh_display(width, height)
    }

    fn render(&self) -> Result<(), Error> {
        let (width, height) = terminal::size()?;

        if self.clock.is_too_large(width, height) {
            return Ok(());
        }

        let mut stdout = io::stdout();

        execute!(stdout, MoveTo(0, self.clock.padding.top))?;

        let lock = stdout.lock();
        let mut buffered_writer = BufWriter::new(lock);

        self.clock.fmt(&mut buffered_writer)?;
        buffered_writer.flush()?;

        Ok(())
    }
}

impl Drop for State {
    fn drop(&mut self) {
        Self::exit();
    }
}