4 Commits
6 changed files with 247 additions and 77 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "termato"
version = "0.1.0"
version = "0.1.1"
edition = "2024"
repository = "https://github.com/StevanFreeborn/termato"
+6 -20
View File
@@ -1,8 +1,7 @@
use crate::constants::SECONDS_PER_MIN;
use crate::msg::Message;
use crate::notification::send_desktop_notification;
const SECONDS_PER_MIN: u32 = 60;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum TimerMode {
Work,
@@ -18,8 +17,7 @@ pub struct Termato {
pub time_remaining_in_sec: u32,
pub show_help: bool,
pub enable_notifications: bool,
pub enable_visualizer: bool,
pub should_quit: bool,
should_quit: bool,
}
impl Termato {
@@ -33,18 +31,16 @@ impl Termato {
time_remaining_in_sec: work_mins * SECONDS_PER_MIN,
show_help: false,
enable_notifications: false,
enable_visualizer: false,
should_quit: false,
}
}
pub fn with_notifications(mut self, enable: bool) -> Self {
self.enable_notifications = enable;
self
pub fn should_quit(&self) -> bool {
self.should_quit
}
pub fn with_visualizer(mut self, enable: bool) -> Self {
self.enable_visualizer = enable;
pub fn with_notifications(mut self, enable: bool) -> Self {
self.enable_notifications = enable;
self
}
@@ -120,7 +116,6 @@ mod tests {
assert_eq!(result.time_remaining_in_sec, 1500);
assert!(!result.show_help);
assert!(!result.enable_notifications);
assert!(!result.enable_visualizer);
assert!(!result.should_quit);
}
@@ -273,13 +268,4 @@ mod tests {
let result = result.with_notifications(true);
assert!(result.enable_notifications);
}
#[test]
fn termato_visualizer_when_called_it_should_verify_opt_in_behavior() {
let result = Termato::new(25, 5);
assert!(!result.enable_visualizer);
let result = result.with_visualizer(true);
assert!(result.enable_visualizer);
}
}
+201
View File
@@ -0,0 +1,201 @@
use std::env;
use crate::constants::{DEFAULT_BREAK_MINS, DEFAULT_WORK_MINS};
pub const FLAG_HELP: &str = "-h";
pub const FLAG_HELP_LONG: &str = "--help";
pub const FLAG_VERSION: &str = "-v";
pub const FLAG_VERSION_LONG: &str = "--version";
pub const FLAG_NOTIFY: &str = "-n";
pub const FLAG_NOTIFY_LONG: &str = "--notify";
pub const FLAG_VISUALIZER: &str = "-z";
pub const FLAG_VISUALIZER_LONG: &str = "--visualizer";
#[derive(Debug, PartialEq, Eq)]
pub struct CliConfig {
pub work_mins: u32,
pub break_mins: u32,
pub enable_notifications: bool,
pub enable_visualizer: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub enum CliAction {
Run(CliConfig),
PrintHelp,
PrintVersion,
Error(String),
}
pub struct Cli;
impl Cli {
pub fn parse() -> CliAction {
Self::parse_args(env::args().collect())
}
pub fn parse_args(args: Vec<String>) -> CliAction {
let mut enable_notifications = false;
let mut enable_visualizer = false;
let mut positional = Vec::new();
for arg in args.into_iter().skip(1) {
match arg.as_str() {
FLAG_HELP | FLAG_HELP_LONG => return CliAction::PrintHelp,
FLAG_VERSION | FLAG_VERSION_LONG => return CliAction::PrintVersion,
FLAG_NOTIFY | FLAG_NOTIFY_LONG => enable_notifications = true,
FLAG_VISUALIZER | FLAG_VISUALIZER_LONG => enable_visualizer = true,
"--" => {}
s if s.starts_with('-') => {
return CliAction::Error(format!("Unknown option: '{}'", s));
}
s => positional.push(s.to_string()),
}
}
let numbers: Vec<u32> = positional
.iter()
.filter_map(|s| s.parse::<u32>().ok())
.collect();
let work_mins = numbers.first().copied().unwrap_or(DEFAULT_WORK_MINS);
let break_mins = numbers.get(1).copied().unwrap_or(DEFAULT_BREAK_MINS);
if numbers.len() > 2 {
return CliAction::Error(format!("Unexpected argument: '{}'", positional[2]));
}
CliAction::Run(CliConfig {
work_mins,
break_mins,
enable_notifications,
enable_visualizer,
})
}
pub fn print_help() {
println!("Usage: termato [options] [work_minutes] [break_minutes]");
println!();
println!("Options:");
println!(
" {:<19} Enable desktop notifications",
format_args!("{}, {}", FLAG_NOTIFY, FLAG_NOTIFY_LONG)
);
println!(
" {:<19} Display visualizer of playing audio",
format_args!("{}, {}", FLAG_VISUALIZER, FLAG_VISUALIZER_LONG)
);
println!(
" {:<19} Print this help message",
format_args!("{}, {}", FLAG_HELP, FLAG_HELP_LONG)
);
println!(
" {:<19} Print the version number",
format_args!("{}, {}", FLAG_VERSION, FLAG_VERSION_LONG)
);
println!();
println!(
"Defaults: work_minutes = {}, break_minutes = {}",
DEFAULT_WORK_MINS, DEFAULT_BREAK_MINS
);
}
pub fn print_version() {
println!("termato version {}", env!("CARGO_PKG_VERSION"));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_defaults() {
let args = vec!["termato".to_string()];
assert_eq!(
Cli::parse_args(args),
CliAction::Run(CliConfig {
work_mins: 25,
break_mins: 5,
enable_notifications: false,
enable_visualizer: false,
})
);
}
#[test]
fn test_cargo_run_syntax_with_dot_and_dash_dash() {
let args = vec![
"target/debug/termato.exe".to_string(),
".".to_string(),
"--".to_string(),
"-z".to_string(),
];
assert_eq!(
Cli::parse_args(args),
CliAction::Run(CliConfig {
work_mins: 25,
break_mins: 5,
enable_notifications: false,
enable_visualizer: true,
})
);
}
#[test]
fn test_custom_time_and_flags() {
let args = vec![
"termato".to_string(),
"-n".to_string(),
"-z".to_string(),
"50".to_string(),
"10".to_string(),
];
assert_eq!(
Cli::parse_args(args),
CliAction::Run(CliConfig {
work_mins: 50,
break_mins: 10,
enable_notifications: true,
enable_visualizer: true,
})
);
}
#[test]
fn test_help_flag() {
let args = vec!["termato".to_string(), "--help".to_string()];
assert_eq!(Cli::parse_args(args), CliAction::PrintHelp);
}
#[test]
fn test_unknown_flag() {
let args = vec!["termato".to_string(), "--foo".to_string()];
assert_eq!(
Cli::parse_args(args),
CliAction::Error("Unknown option: '--foo'".to_string())
);
}
#[test]
fn test_non_numeric_positional_defaults() {
let args = vec!["termato".to_string(), "abc".to_string()];
assert_eq!(
Cli::parse_args(args),
CliAction::Run(CliConfig {
work_mins: 25,
break_mins: 5,
enable_notifications: false,
enable_visualizer: false,
})
);
}
}
+4
View File
@@ -0,0 +1,4 @@
pub const SECONDS_PER_MIN: u32 = 60;
pub const DEFAULT_WORK_MINS: u32 = 25;
pub const DEFAULT_BREAK_MINS: u32 = 5;
pub const VIZ_NUM_BARS: u16 = 64;
+23 -47
View File
@@ -1,5 +1,7 @@
mod app;
mod args;
mod audio;
mod constants;
mod events;
mod msg;
mod notification;
@@ -24,26 +26,27 @@ use msg::Message;
use terminal::TerminalGuard;
use ui::render_app;
use args::{Cli, CliAction};
use crate::constants::VIZ_NUM_BARS;
fn main() -> Result<(), io::Error> {
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|arg| arg == "-h" || arg == "--help") {
println!("Usage: termato [options] [work_minutes] [break_minutes]");
println!();
println!("Options:");
println!(" -n, --notify Enable desktop notifications");
println!(" -h, --help Print this help message");
println!(" -v, --version Print the version number");
println!(" -z, --visualizer Display visualizer of playing audio");
println!();
println!("Defaults: work_minutes = 25, break_minutes = 5");
let config = match Cli::parse() {
CliAction::PrintHelp => {
Cli::print_help();
return Ok(());
}
if args.iter().any(|arg| arg == "-v" || arg == "--version") {
println!("termato version {}", env!("CARGO_PKG_VERSION"));
CliAction::PrintVersion => {
Cli::print_version();
return Ok(());
}
CliAction::Error(err) => {
eprintln!("Error: {}", err);
eprintln!("Run 'termato --help' for usage instructions.");
std::process::exit(1);
}
CliAction::Run(cfg) => cfg,
};
let _guard = TerminalGuard::new()?;
@@ -51,41 +54,14 @@ fn main() -> Result<(), io::Error> {
let mut terminal = Terminal::new(backend)?;
let font = Toilet::smblock().unwrap();
let enable_visualizer = args.iter().any(|arg| arg == "-z" || arg == "--visualizer");
let enable_notifications = args.iter().any(|arg| arg == "-n" || arg == "--notify");
let visualizer = if enable_visualizer {
Some(AudioVisualizer::new(64))
let visualizer = if config.enable_visualizer {
Some(AudioVisualizer::new(VIZ_NUM_BARS as usize))
} else {
None
};
let is_flag = |arg: &str| {
arg == "-n"
|| arg == "--notify"
|| arg == "-z"
|| arg == "--visualizer"
|| arg == "-h"
|| arg == "--help"
|| arg == "-v"
|| arg == "--version"
};
let positional_args: Vec<&String> = args.iter().skip(1).filter(|arg| !is_flag(arg)).collect();
let work_mins = positional_args
.first()
.and_then(|s| s.parse().ok())
.unwrap_or(25);
let break_mins = positional_args
.get(1)
.and_then(|s| s.parse().ok())
.unwrap_or(5);
let mut termato = Termato::new(work_mins, break_mins)
.with_notifications(enable_notifications)
.with_visualizer(enable_visualizer);
let mut termato = Termato::new(config.work_mins, config.break_mins)
.with_notifications(config.enable_notifications);
let (tx, rx) = mpsc::channel();
@@ -98,7 +74,7 @@ fn main() -> Result<(), io::Error> {
}
});
while !termato.should_quit {
while !termato.should_quit() {
terminal.draw(|f| render_app(&termato, f, &font, visualizer.as_ref()))?;
if event::poll(Duration::from_millis(16))? {
+8 -5
View File
@@ -6,8 +6,11 @@ use ratatui::{
widgets::{Block, Borders, Clear, Paragraph},
};
use crate::app::{Termato, TimerMode};
use crate::audio::AudioVisualizer;
use crate::{audio::AudioVisualizer, constants::VIZ_NUM_BARS};
use crate::{
app::{Termato, TimerMode},
constants::SECONDS_PER_MIN,
};
// use unicode blocks for rendering
// Empty -> " "
@@ -52,8 +55,8 @@ pub fn render_app(
) {
let size = f.area();
let minutes = app.time_remaining_in_sec / 60;
let seconds = app.time_remaining_in_sec % 60;
let minutes = app.time_remaining_in_sec / SECONDS_PER_MIN;
let seconds = app.time_remaining_in_sec % SECONDS_PER_MIN;
let time_str = format!("{:02}:{:02}", minutes, seconds);
let time_color = if !app.is_running {
@@ -76,7 +79,7 @@ pub fn render_app(
let text_height = text_lines.len() as u16;
let viz_height = if visualizer.is_some() { 1 } else { 0 };
let viz_width = 64;
let viz_width = VIZ_NUM_BARS;
let content_height = text_height + viz_height;