From 35e847841c51d41e30cf6d49216b257058dcbee8 Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Sun, 21 Jan 2024 21:58:13 +0100 Subject: [PATCH] feat(countdown): implement --- Cargo.toml | 2 +- src/config.rs | 18 ++--- src/main.rs | 61 ++++++---------- src/modes/countdown.rs | 157 +++++++++++++++++++++++++++++++++++++++++ src/modes/mod.rs | 1 + 5 files changed, 187 insertions(+), 52 deletions(-) create mode 100644 src/modes/countdown.rs diff --git a/Cargo.toml b/Cargo.toml index ff853cf..9a7b616 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] -atomic_enum = "0.2.0" chrono = "0.4.31" clap = { version = "4.4.18", features = ["derive", "cargo"] } crossterm = "0.27.0" dirs = "5.0.1" ini = "1.3.0" +parse_duration = "2.1.1" diff --git a/src/config.rs b/src/config.rs index bc1ceb4..3ecf8bd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,10 +5,8 @@ use crossterm::style::Color; use ini::configparser::ini::Ini; use crate::{ - get_app_mode, modes::debug, rendering::color::{generate_gradient, parse_hex_color, ComputableColor}, - AppMode, }; pub struct Config { @@ -21,14 +19,14 @@ pub struct Config { const DEFAULT_CONFIG: &str = include_str!("default_config"); -pub fn load_from_file(path: PathBuf) -> Config { +pub fn load_from_file(path: PathBuf, debug_mode: bool) -> Config { let mut ini = Ini::new(); ini.load(&path.to_str().unwrap()).unwrap(); let config = Config { be_polite: ini.getbool("general", "polite").unwrap().unwrap(), fps: ini.getuint("general", "fps").unwrap().unwrap(), - color: load_color(&ini), + color: load_color(&ini, debug_mode), time_format: ini.get("format", "time").unwrap(), date_format: ini.get("format", "date").unwrap(), }; @@ -42,7 +40,7 @@ pub fn write_default_config(path: PathBuf) -> () { let _ = fs::write(path, DEFAULT_CONFIG); } -fn load_color(ini: &Ini) -> ComputableColor { +fn load_color(ini: &Ini, debug_mode: bool) -> ComputableColor { let color_mode = ini.get("styling", "color_mode").unwrap(); match color_mode.as_str() { @@ -59,7 +57,7 @@ fn load_color(ini: &Ini) -> ComputableColor { return ComputableColor::from(load_ansi_color(color)); } "gradient" => { - return load_gradient(ini); + return load_gradient(ini, debug_mode); } _ => panic!("ERROR: Invalid color mode: {}", color_mode), } @@ -100,7 +98,7 @@ fn load_ansi_color(value: i64) -> Color { return Color::AnsiValue(value.try_into().unwrap()); } -fn load_gradient(ini: &Ini) -> ComputableColor { +fn load_gradient(ini: &Ini, debug_mode: bool) -> ComputableColor { let mut keys = Vec::new(); let mut i = 0; @@ -109,9 +107,7 @@ fn load_gradient(ini: &Ini) -> ComputableColor { i += 1; } - if get_app_mode() != AppMode::Debug - && ini.getbool("gradient", "gradient_loop").unwrap().unwrap() - { + if !debug_mode && ini.getbool("gradient", "gradient_loop").unwrap().unwrap() { let mut loop_keys = keys.clone(); loop_keys.reverse(); for i in 1..loop_keys.len() { @@ -119,7 +115,7 @@ fn load_gradient(ini: &Ini) -> ComputableColor { } } - let steps: usize = if get_app_mode() == AppMode::Debug { + let steps: usize = if debug_mode { debug::DEBUG_COLOR_DISPLAY_SIZE * 2 } else { ini.getuint("gradient", "gradient_steps") diff --git a/src/main.rs b/src/main.rs index afe38d4..e8c8cbb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,8 @@ use core::panic; use std::{ io::{self, Write}, path::PathBuf, - sync::atomic::Ordering, }; -use atomic_enum::atomic_enum; use clap::{Parser, Subcommand}; use config::write_default_config; use crossterm::{cursor, execute, terminal}; @@ -39,37 +37,15 @@ struct Cli { enum Commands { Debug {}, Chrono {}, -} - -#[atomic_enum] -#[derive(PartialEq)] -pub enum AppMode { - Clock = 0, - Debug, - Chrono, -} - -static APP_MODE: AtomicAppMode = AtomicAppMode::new(AppMode::Debug); - -pub fn get_app_mode() -> AppMode { - return APP_MODE.load(Ordering::Relaxed); -} - -pub fn set_app_mode(mode: AppMode) { - return APP_MODE.store(mode, Ordering::Relaxed); + Countdown { + #[arg(required = true)] + start: Vec, + }, } fn main() -> io::Result<()> { let cli = Cli::parse(); - match &cli.command { - Some(Commands::Debug {}) => { - set_app_mode(AppMode::Debug); - } - Some(Commands::Chrono {}) => set_app_mode(AppMode::Chrono), - _ => set_app_mode(AppMode::Clock), - } - // Load config let mut default_generated = false; let config_file = if let Some(custom_config) = cli.config { @@ -110,26 +86,31 @@ fn main() -> io::Result<()> { panic!("ERROR: Configuration file not found"); } - let mut config = config::load_from_file(config_file); + let debug_mode = match &cli.command { + Some(Commands::Debug {}) => true, + _ => false, + }; + let mut config = config::load_from_file(config_file, debug_mode); let mut stdout = io::stdout(); - match get_app_mode() { - AppMode::Debug => { - debug::print_debug_infos(&mut config)?; - return Ok(()); - } - _ => {} + if debug_mode { + debug::print_debug_infos(&mut config)?; + return Ok(()); } // Switch to alternate screen, hide the cursor and enable raw mode execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?; let _ = terminal::enable_raw_mode()?; - match get_app_mode() { - AppMode::Clock => modes::clock::main_loop(&mut config)?, - AppMode::Chrono => modes::chrono::main_loop(&mut config)?, - AppMode::Debug => unreachable!(), - }; + match &cli.command { + Some(Commands::Chrono {}) => modes::chrono::main_loop(&mut config)?, + Some(Commands::Countdown { start }) => { + let start = start.join(" "); + modes::countdown::main_loop(&mut config, &start)? + } + Some(Commands::Debug {}) => unreachable!(), + None => modes::clock::main_loop(&mut config)?, + } // Disale raw mode, leave the alternate screen and show the cursor back let _ = terminal::disable_raw_mode().unwrap(); diff --git a/src/modes/countdown.rs b/src/modes/countdown.rs new file mode 100644 index 0000000..1f6a1a9 --- /dev/null +++ b/src/modes/countdown.rs @@ -0,0 +1,157 @@ +use std::{ + io::{self, Write}, + thread, + time::{Duration, Instant}, +}; + +use crossterm::{ + event::{self, Event, KeyCode, KeyModifiers}, + queue, + terminal::{self, ClearType}, +}; + +use crate::utils; +use crate::{ + config::Config, + rendering::{self, symbols}, +}; + +struct Countdown { + duration: Duration, + end_time: Option, + paused_duration: Duration, +} + +impl Countdown { + fn new(duration: Duration) -> Self { + let end_time = Some(Instant::now() + duration + Duration::from_secs(1)); + Countdown { + duration, + end_time, + paused_duration: Duration::ZERO, + } + } + + fn toggle_pause(&mut self) { + if let Some(end_time) = self.end_time { + self.paused_duration += end_time.duration_since(Instant::now()); + self.end_time = None; + } else { + self.end_time = Some(Instant::now() + self.paused_duration); + self.paused_duration = Duration::ZERO; + } + } + + fn time_left(&self) -> Duration { + if let Some(end_time) = self.end_time { + let remaining_time = if Instant::now() < end_time { + end_time.duration_since(Instant::now()) + } else { + Duration::ZERO + }; + + remaining_time - self.paused_duration + } else { + self.paused_duration + } + } + + fn is_finished(&self) -> bool { + self.time_left().as_secs() == 0 + } + + fn is_paused(&self) -> bool { + return self.end_time.is_none(); + } + + fn reset(&mut self) { + self.end_time = Some(Instant::now() + self.duration + Duration::from_secs(1)); + self.paused_duration = Duration::ZERO; + self.toggle_pause(); + } +} + +pub fn main_loop(config: &mut Config, start: &str) -> io::Result<()> { + let mut stdout = io::stdout(); + + let duration = parse_duration::parse(start).unwrap(); + let mut countdown = Countdown::new(duration); + + let mut quit = false; + while !quit { + // Handle events + while event::poll(Duration::ZERO)? { + match event::read()? { + Event::Key(e) => match e.code { + // Handle CTRL-C + KeyCode::Char('c') => { + if e.modifiers.contains(KeyModifiers::CONTROL) { + quit = true; + } + } + // Handle pause + KeyCode::Char(' ') => { + countdown.toggle_pause(); + } + // Handle reset + KeyCode::Char('r') => { + countdown.reset(); + } + _ => {} + }, + _ => {} + } + } + + // Clear frame + queue!(stdout, terminal::Clear(ClearType::All))?; + + // Render + render_frame(&config, &countdown)?; + + config.color.update(); + + stdout.flush()?; + + thread::sleep(Duration::from_millis(1000 / config.fps)); + } + + return Ok(()); +} + +fn render_frame(config: &Config, countdown: &Countdown) -> io::Result<()> { + let color = config.color.get_value(); + + // Display time + let remaining = utils::format_duration(countdown.time_left()); + rendering::draw_time(&remaining, color)?; + + // Display pause state + let (width, height) = terminal::size()?; + let y = height / 2 + symbols::SYMBOL_HEIGHT as u16 / 2 + 2; + if countdown.is_paused() { + let text = "[PAUSE]"; + let x = width / 2 - (text.len() as u16) / 2; + + rendering::draw_text( + text, + x, + y - symbols::SYMBOL_HEIGHT as u16 - symbols::SYMBOL_HEIGHT as u16 / 2, + color, + )?; + } + // Display finish state + else if countdown.is_finished() { + let text = "[FINISHED]"; + let x = width / 2 - (text.len() as u16) / 2; + + rendering::draw_text( + text, + x, + y - symbols::SYMBOL_HEIGHT as u16 - symbols::SYMBOL_HEIGHT as u16 / 2, + color, + )?; + } + + return Ok(()); +} diff --git a/src/modes/mod.rs b/src/modes/mod.rs index eefc37a..3b59ebd 100644 --- a/src/modes/mod.rs +++ b/src/modes/mod.rs @@ -1,3 +1,4 @@ pub mod chrono; pub mod clock; +pub mod countdown; pub mod debug;