use crate::db::pauses::Pauses;
use crate::db::workdays::Workdays;
use crate::libs::config::Config;
use crate::libs::messages::Message;
use crate::libs::view::View;
use crate::{msg_error, msg_print, msg_success};
use anyhow::{Result, bail};
use chrono::{Duration, Local, NaiveDate, NaiveTime, TimeDelta};
use clap::{Args, Subcommand};
use dialoguer::{Confirm, theme::ColorfulTheme};
use std::io::IsTerminal;
#[derive(Debug, Args)]
pub struct PausesArgs {
#[command(subcommand)]
command: Option<PausesCommand>,
#[arg(long, short, default_value = "today")]
date: String,
#[arg(long, short, help = "Minimum pause duration in minutes")]
min_duration: Option<u64>,
}
#[derive(Debug, Subcommand)]
enum PausesCommand {
#[command(about = "Record a pause that the monitor missed")]
Add(AddArgs),
#[command(about = "List pauses for a given date")]
List(ListArgs),
#[command(about = "Remove a pause record")]
Remove(RemoveArgs),
}
#[derive(Debug, Args)]
struct AddArgs {
#[arg(long, short, value_name = "HH:MM", help = "Start time of the absence")]
start: String,
#[arg(long, short, value_name = "N", help = "Duration in minutes")]
minutes: u64,
#[arg(long, short, default_value = "today", help = "Date of the absence (YYYY-MM-DD or 'today')")]
date: String,
#[arg(long, help = "Exempt this pause from filtering and merging")]
keep: bool,
#[arg(long, short, help = "Note describing the absence")]
reason: Option<String>,
}
#[derive(Debug, Args)]
struct ListArgs {
#[arg(long, short, default_value = "today", help = "Date to fetch pauses for (YYYY-MM-DD or 'today')")]
date: String,
#[arg(long, short, help = "Minimum pause duration in minutes")]
min_duration: Option<u64>,
}
#[derive(Debug, Args)]
struct RemoveArgs {
#[arg(value_name = "ID", help = "Id of the pause to remove")]
id: i32,
#[arg(long, short = 'y', help = "Do not ask for confirmation")]
yes: bool,
}
pub async fn cmd(args: PausesArgs) -> Result<()> {
match args.command {
Some(PausesCommand::Add(add_args)) => add(add_args),
Some(PausesCommand::Remove(remove_args)) => remove(remove_args),
Some(PausesCommand::List(list_args)) => list(&list_args.date, list_args.min_duration),
None => list(&args.date, args.min_duration),
}
}
fn list(date_str: &str, min_duration_override: Option<u64>) -> Result<()> {
let date = parse_date(date_str)?;
let config = Config::read()?;
let min_duration = min_duration_override.unwrap_or(config.monitor.unwrap_or_default().min_pause_duration);
let pauses_db = Pauses::new()?.set_min_duration(min_duration);
let pauses = match Workdays::new()?.fetch(date)? {
Some(workday) => pauses_db.get_workday_pauses(&workday)?,
None => pauses_db.get_daily_pauses(date)?,
};
let total_pause_time = pauses.iter().filter_map(|p| p.duration).fold(Duration::zero(), |acc, d| acc + d);
msg_print!(Message::PausesTitle(date.format("%B %-d, %Y").to_string()), true);
View::pauses(&pauses, total_pause_time)?;
Ok(())
}
fn add(args: AddArgs) -> Result<()> {
if args.minutes == 0 {
bail!("duration must be at least 1 minute");
}
let date = parse_date(&args.date)?;
let time = NaiveTime::parse_from_str(&args.start, "%H:%M").map_err(|_| anyhow::anyhow!("invalid start time '{}' - expected HH:MM", args.start))?;
let start = date.and_time(time);
let duration = TimeDelta::minutes(args.minutes as i64);
let end = start + duration;
let pauses = Pauses::new()?;
if let Some(existing) = pauses.find_overlapping(start, end)? {
msg_error!(Message::ManualPauseOverlaps {
start_time: existing.start.format("%H:%M").to_string(),
end_time: existing.end.map(|e| e.format("%H:%M").to_string()).unwrap_or_else(|| "…".to_string()),
});
return Ok(());
}
pauses.insert_manual(start, duration, args.keep, args.reason.as_deref())?;
msg_success!(Message::ManualPauseCreated {
start_time: start.format("%H:%M").to_string(),
end_time: end.format("%H:%M").to_string(),
duration_minutes: args.minutes,
});
Ok(())
}
fn remove(args: RemoveArgs) -> Result<()> {
let pauses = Pauses::new()?;
if !args.yes {
if !std::io::stdin().is_terminal() {
bail!("refusing to remove pause {} without --yes outside an interactive terminal", args.id);
}
let confirmed = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(format!("Remove pause {}?", args.id))
.default(false)
.interact()?;
if !confirmed {
return Ok(());
}
}
let deleted = pauses.delete_many(&[args.id])?;
if deleted == 0 {
msg_error!(Message::ManualPauseNotFound(args.id));
} else {
msg_success!(Message::ManualPauseRemoved(args.id));
}
Ok(())
}
fn parse_date(date_str: &str) -> Result<NaiveDate> {
if date_str.to_lowercase() == "today" {
Ok(Local::now().date_naive())
} else {
Ok(NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?)
}
}