use std::process::ExitCode;
use clap::{ArgAction, Parser, Subcommand};
use reqwest::blocking::Client;
use crate::commands::config::{ConfigCommandOptions, config_command};
use crate::commands::ramadan::{RamadanCommandOptions, ramadan_command, to_json_error_payload};
use crate::ramadan_config::clear_ramadan_config;
#[derive(Debug, Parser)]
#[command(name = "ramadan-cli")]
#[command(about = "Ramadan CLI for Sehar and Iftar timings")]
#[command(
long_about = "Ramadan-first CLI for Sehar and Iftar timings. Default run shows today's view."
)]
#[command(version, disable_version_flag = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(
index = 1,
help = "City name (e.g. \"San Francisco\", \"sf\", \"Vancouver\", \"Lahore\")"
)]
pub city_arg: Option<String>,
#[arg(short = 'c', long = "city", help = "City")]
pub city_opt: Option<String>,
#[arg(short = 'a', long = "all", action = ArgAction::SetTrue, help = "Show complete Ramadan month")]
pub all: bool,
#[arg(short = 'n', long = "number", value_parser = parse_roza_number, help = "Show a specific roza day (1-30)", value_name = "number")]
pub number: Option<usize>,
#[arg(short = 'p', long = "plain", action = ArgAction::SetTrue, help = "Plain text output")]
pub plain: bool,
#[arg(short = 'j', long = "json", action = ArgAction::SetTrue, help = "JSON output")]
pub json: bool,
#[arg(
long = "first-roza-date",
help = "Set and use a custom first roza date",
value_name = "YYYY-MM-DD"
)]
pub first_roza_date: Option<String>,
#[arg(long = "clear-first-roza-date", action = ArgAction::SetTrue, help = "Clear custom first roza date and use API Ramadan date")]
pub clear_first_roza_date: bool,
#[arg(short = 'v', long = "version", action = ArgAction::SetTrue, help = "Print version only")]
pub version: bool,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
#[command(about = "Clear saved Ramadan CLI configuration")]
Reset,
#[command(about = "Configure saved Ramadan CLI settings (non-interactive)")]
Config {
#[arg(long, help = "Save city")]
city: Option<String>,
#[arg(long, help = "Save country")]
country: Option<String>,
#[arg(long, help = "Save latitude (-90 to 90)")]
latitude: Option<String>,
#[arg(long, help = "Save longitude (-180 to 180)")]
longitude: Option<String>,
#[arg(long, help = "Save calculation method (0-23)")]
method: Option<String>,
#[arg(long, help = "Save school (0=Shafi, 1=Hanafi)")]
school: Option<String>,
#[arg(long, help = "Save timezone (e.g., America/Los_Angeles)")]
timezone: Option<String>,
#[arg(long, action = ArgAction::SetTrue, help = "Show current configuration")]
show: bool,
#[arg(long, action = ArgAction::SetTrue, help = "Clear saved configuration")]
clear: bool,
},
}
fn parse_roza_number(value: &str) -> Result<usize, String> {
let parsed = value
.parse::<usize>()
.map_err(|_| "Roza number must be between 1 and 30.".to_string())?;
if !(1..=30).contains(&parsed) {
return Err("Roza number must be between 1 and 30.".to_string());
}
Ok(parsed)
}
pub fn run() -> ExitCode {
let cli = Cli::parse();
if cli.version {
println!("{}", env!("CARGO_PKG_VERSION"));
return ExitCode::SUCCESS;
}
let client = match Client::builder().build() {
Ok(client) => client,
Err(error) => {
eprintln!("Failed to initialize HTTP client: {error}");
return ExitCode::from(1);
}
};
match cli.command {
Some(Commands::Reset) => match clear_ramadan_config() {
Ok(()) => {
println!("Configuration reset.");
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("{error}");
ExitCode::from(1)
}
},
Some(Commands::Config {
city,
country,
latitude,
longitude,
method,
school,
timezone,
show,
clear,
}) => {
let options = ConfigCommandOptions {
city,
country,
latitude,
longitude,
method,
school,
timezone,
show,
clear,
};
match config_command(&options) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("{error}");
ExitCode::from(1)
}
}
}
None => {
let options = RamadanCommandOptions {
city: cli.city_arg.or(cli.city_opt),
all: cli.all,
roza_number: cli.number,
plain: cli.plain,
json: cli.json,
first_roza_date: cli.first_roza_date,
clear_first_roza_date: cli.clear_first_roza_date,
};
match ramadan_command(&client, &options) {
Ok(Some(output)) => {
println!(
"{}",
serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string())
);
ExitCode::SUCCESS
}
Ok(None) => ExitCode::SUCCESS,
Err(error) => {
if error.to_string() == "SETUP_CANCELLED" {
return ExitCode::SUCCESS;
}
if options.json {
let payload = to_json_error_payload(&error);
eprintln!(
"{}",
serde_json::to_string(&payload).unwrap_or_else(|_| {
"{\"ok\":false,\"error\":{\"code\":\"UNKNOWN_ERROR\",\"message\":\"unknown error\"}}".to_string()
})
);
} else {
eprintln!("{error}");
}
ExitCode::from(1)
}
}
}
}
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::{Cli, Commands};
#[test]
fn parses_version_flag() {
let parsed = Cli::try_parse_from(["ramadan-cli", "-v"]).expect("should parse");
assert!(parsed.version);
}
#[test]
fn rejects_invalid_roza_number() {
let parsed = Cli::try_parse_from(["ramadan-cli", "-n", "31"]);
assert!(parsed.is_err());
}
#[test]
fn parses_config_subcommand_show() {
let parsed =
Cli::try_parse_from(["ramadan-cli", "config", "--show"]).expect("should parse");
match parsed.command {
Some(Commands::Config { show, .. }) => assert!(show),
_ => panic!("expected config subcommand"),
}
}
}