feat(countdown): implement
This commit is contained in:
@@ -4,10 +4,10 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
atomic_enum = "0.2.0"
|
|
||||||
chrono = "0.4.31"
|
chrono = "0.4.31"
|
||||||
clap = { version = "4.4.18", features = ["derive", "cargo"] }
|
clap = { version = "4.4.18", features = ["derive", "cargo"] }
|
||||||
crossterm = "0.27.0"
|
crossterm = "0.27.0"
|
||||||
dirs = "5.0.1"
|
dirs = "5.0.1"
|
||||||
ini = "1.3.0"
|
ini = "1.3.0"
|
||||||
|
parse_duration = "2.1.1"
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ use crossterm::style::Color;
|
|||||||
use ini::configparser::ini::Ini;
|
use ini::configparser::ini::Ini;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
get_app_mode,
|
|
||||||
modes::debug,
|
modes::debug,
|
||||||
rendering::color::{generate_gradient, parse_hex_color, ComputableColor},
|
rendering::color::{generate_gradient, parse_hex_color, ComputableColor},
|
||||||
AppMode,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -21,14 +19,14 @@ pub struct Config {
|
|||||||
|
|
||||||
const DEFAULT_CONFIG: &str = include_str!("default_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();
|
let mut ini = Ini::new();
|
||||||
ini.load(&path.to_str().unwrap()).unwrap();
|
ini.load(&path.to_str().unwrap()).unwrap();
|
||||||
|
|
||||||
let config = Config {
|
let config = Config {
|
||||||
be_polite: ini.getbool("general", "polite").unwrap().unwrap(),
|
be_polite: ini.getbool("general", "polite").unwrap().unwrap(),
|
||||||
fps: ini.getuint("general", "fps").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(),
|
time_format: ini.get("format", "time").unwrap(),
|
||||||
date_format: ini.get("format", "date").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);
|
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();
|
let color_mode = ini.get("styling", "color_mode").unwrap();
|
||||||
|
|
||||||
match color_mode.as_str() {
|
match color_mode.as_str() {
|
||||||
@@ -59,7 +57,7 @@ fn load_color(ini: &Ini) -> ComputableColor {
|
|||||||
return ComputableColor::from(load_ansi_color(color));
|
return ComputableColor::from(load_ansi_color(color));
|
||||||
}
|
}
|
||||||
"gradient" => {
|
"gradient" => {
|
||||||
return load_gradient(ini);
|
return load_gradient(ini, debug_mode);
|
||||||
}
|
}
|
||||||
_ => panic!("ERROR: Invalid color mode: {}", color_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());
|
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 keys = Vec::new();
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
@@ -109,9 +107,7 @@ fn load_gradient(ini: &Ini) -> ComputableColor {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if get_app_mode() != AppMode::Debug
|
if !debug_mode && ini.getbool("gradient", "gradient_loop").unwrap().unwrap() {
|
||||||
&& ini.getbool("gradient", "gradient_loop").unwrap().unwrap()
|
|
||||||
{
|
|
||||||
let mut loop_keys = keys.clone();
|
let mut loop_keys = keys.clone();
|
||||||
loop_keys.reverse();
|
loop_keys.reverse();
|
||||||
for i in 1..loop_keys.len() {
|
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
|
debug::DEBUG_COLOR_DISPLAY_SIZE * 2
|
||||||
} else {
|
} else {
|
||||||
ini.getuint("gradient", "gradient_steps")
|
ini.getuint("gradient", "gradient_steps")
|
||||||
|
|||||||
61
src/main.rs
61
src/main.rs
@@ -2,10 +2,8 @@ use core::panic;
|
|||||||
use std::{
|
use std::{
|
||||||
io::{self, Write},
|
io::{self, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::atomic::Ordering,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use atomic_enum::atomic_enum;
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use config::write_default_config;
|
use config::write_default_config;
|
||||||
use crossterm::{cursor, execute, terminal};
|
use crossterm::{cursor, execute, terminal};
|
||||||
@@ -39,37 +37,15 @@ struct Cli {
|
|||||||
enum Commands {
|
enum Commands {
|
||||||
Debug {},
|
Debug {},
|
||||||
Chrono {},
|
Chrono {},
|
||||||
}
|
Countdown {
|
||||||
|
#[arg(required = true)]
|
||||||
#[atomic_enum]
|
start: Vec<String>,
|
||||||
#[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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> io::Result<()> {
|
fn main() -> io::Result<()> {
|
||||||
let cli = Cli::parse();
|
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
|
// Load config
|
||||||
let mut default_generated = false;
|
let mut default_generated = false;
|
||||||
let config_file = if let Some(custom_config) = cli.config {
|
let config_file = if let Some(custom_config) = cli.config {
|
||||||
@@ -110,26 +86,31 @@ fn main() -> io::Result<()> {
|
|||||||
panic!("ERROR: Configuration file not found");
|
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();
|
let mut stdout = io::stdout();
|
||||||
|
|
||||||
match get_app_mode() {
|
if debug_mode {
|
||||||
AppMode::Debug => {
|
debug::print_debug_infos(&mut config)?;
|
||||||
debug::print_debug_infos(&mut config)?;
|
return Ok(());
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch to alternate screen, hide the cursor and enable raw mode
|
// Switch to alternate screen, hide the cursor and enable raw mode
|
||||||
execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;
|
execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;
|
||||||
let _ = terminal::enable_raw_mode()?;
|
let _ = terminal::enable_raw_mode()?;
|
||||||
|
|
||||||
match get_app_mode() {
|
match &cli.command {
|
||||||
AppMode::Clock => modes::clock::main_loop(&mut config)?,
|
Some(Commands::Chrono {}) => modes::chrono::main_loop(&mut config)?,
|
||||||
AppMode::Chrono => modes::chrono::main_loop(&mut config)?,
|
Some(Commands::Countdown { start }) => {
|
||||||
AppMode::Debug => unreachable!(),
|
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
|
// Disale raw mode, leave the alternate screen and show the cursor back
|
||||||
let _ = terminal::disable_raw_mode().unwrap();
|
let _ = terminal::disable_raw_mode().unwrap();
|
||||||
|
|||||||
157
src/modes/countdown.rs
Normal file
157
src/modes/countdown.rs
Normal file
@@ -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<Instant>,
|
||||||
|
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(());
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod chrono;
|
pub mod chrono;
|
||||||
pub mod clock;
|
pub mod clock;
|
||||||
|
pub mod countdown;
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
|
|||||||
Reference in New Issue
Block a user