From a62f54e85b01dbb0c25e7f26c29b1513231cdb99 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn Date: Tue, 28 Jul 2026 09:55:17 -0500 Subject: [PATCH] refactor: extract argument parsing to separate module --- src/args.rs | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 72 ++++++++-------------- 2 files changed, 190 insertions(+), 49 deletions(-) create mode 100644 src/args.rs diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..bc93d3a --- /dev/null +++ b/src/args.rs @@ -0,0 +1,167 @@ +use std::env; + +#[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) -> 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() { + "-h" | "--help" => return CliAction::PrintHelp, + "-v" | "--version" => return CliAction::PrintVersion, + "-n" | "--notify" => enable_notifications = true, + "-z" | "--visualizer" => enable_visualizer = true, + "--" => {} + s if s.starts_with('-') => { + return CliAction::Error(format!("Unknown option: '{}'", s)); + } + s => positional.push(s.to_string()), + } + } + + let numbers: Vec = positional + .iter() + .filter_map(|s| s.parse::().ok()) + .collect(); + + let work_mins = numbers.first().copied().unwrap_or(25); + let break_mins = numbers.get(1).copied().unwrap_or(5); + + 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!(" -n, --notify Enable desktop notifications"); + println!(" -z, --visualizer Display visualizer of playing audio"); + println!(" -h, --help Print this help message"); + println!(" -v, --version Print the version number"); + println!(); + println!("Defaults: work_minutes = 25, break_minutes = 5"); + } + + 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, + }) + ); + } +} diff --git a/src/main.rs b/src/main.rs index b0a7f50..ee93d9d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod app; +mod args; mod audio; mod events; mod msg; @@ -24,26 +25,25 @@ use msg::Message; use terminal::TerminalGuard; use ui::render_app; +use args::{Cli, CliAction}; + fn main() -> Result<(), io::Error> { - let args: Vec = 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"); - return Ok(()); - } - - if args.iter().any(|arg| arg == "-v" || arg == "--version") { - println!("termato version {}", env!("CARGO_PKG_VERSION")); - return Ok(()); - } + let config = match Cli::parse() { + CliAction::PrintHelp => { + Cli::print_help(); + return Ok(()); + } + 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 +51,15 @@ 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 { + let visualizer = if config.enable_visualizer { Some(AudioVisualizer::new(64)) } 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) + .with_visualizer(config.enable_visualizer); let (tx, rx) = mpsc::channel();