use crate::{
db::{breaks::Breaks, pauses::Pauses, workdays::Workdays},
libs::{config::Config, formatter::format_duration, messages::Message, pause::Pause, productivity::Productivity, report},
msg_error, msg_info, msg_print, msg_success,
};
use anyhow::Result;
use chrono::{Duration, Local, NaiveDate, NaiveDateTime};
use clap::Args;
use dialoguer::{theme::ColorfulTheme, Input, Select};
#[derive(Debug, Args)]
pub struct BreaksArgs {
#[arg(long, short)]
minutes: Option<u64>,
#[arg(long)]
force: bool,
}
#[derive(Debug, Clone)]
pub struct BreakOption {
pub start: NaiveDateTime,
pub end: NaiveDateTime,
pub duration: Duration,
pub description: String,
}
pub async fn cmd(args: BreaksArgs) -> Result<()> {
let today = Local::now().date_naive();
let config = Config::read()?;
let productivity_config = config.productivity.unwrap_or_default();
if let Some(minutes) = args.minutes {
handle_automatic_break_placement(today, minutes, &productivity_config, args.force).await
} else {
handle_interactive_break_creation(today, &productivity_config, args.force).await
}
}
async fn handle_automatic_break_placement(
date: NaiveDate,
minutes: u64,
productivity_config: &crate::libs::config::ProductivityConfig,
_force: bool,
) -> Result<()> {
if minutes < productivity_config.min_break_duration {
msg_error!(Message::BreakDurationPrompt {
min_duration: productivity_config.min_break_duration,
max_duration: productivity_config.max_break_duration,
});
return Ok(());
}
if minutes > productivity_config.max_break_duration {
msg_error!(Message::BreakDurationPrompt {
min_duration: productivity_config.min_break_duration,
max_duration: productivity_config.max_break_duration,
});
return Ok(());
}
let workday = match Workdays::new()?.fetch(date)? {
Some(wd) => wd,
None => {
msg_error!("No workday found for today");
return Ok(());
}
};
let config = Config::read()?;
let monitor_config = config.monitor.unwrap_or_default();
let pauses = Pauses::new()?
.set_min_duration(monitor_config.min_pause_duration)
.get_workday_pauses(&workday)?;
let break_options = find_break_placement_options(&workday, &pauses, minutes, monitor_config.min_work_interval)?;
if break_options.is_empty() {
msg_error!(Message::NoValidBreakPlacement);
return Ok(());
}
let break_option = &break_options[0];
let break_record = crate::db::breaks::Break {
id: None,
date,
start: break_option.start,
end: break_option.end,
duration: break_option.duration,
reason: None,
created_at: None,
};
let breaks_db = Breaks::new()?;
breaks_db.insert(&break_record)?;
msg_success!(Message::BreakCreated {
start_time: break_option.start.format("%H:%M").to_string(),
end_time: break_option.end.format("%H:%M").to_string(),
duration_minutes: minutes,
});
show_updated_productivity(date).await?;
Ok(())
}
async fn handle_interactive_break_creation(date: NaiveDate, productivity_config: &crate::libs::config::ProductivityConfig, _force: bool) -> Result<()> {
msg_print!(Message::BreakInteractivePrompt);
let theme = ColorfulTheme::default();
let duration_input: String = Input::with_theme(&theme)
.with_prompt(&format!(
"Enter break duration ({}-{} minutes)",
productivity_config.min_break_duration, productivity_config.max_break_duration
))
.interact_text()?;
let minutes: u64 = match duration_input.parse() {
Ok(m) if m >= productivity_config.min_break_duration && m <= productivity_config.max_break_duration => m,
_ => {
msg_error!(Message::BreakDurationPrompt {
min_duration: productivity_config.min_break_duration,
max_duration: productivity_config.max_break_duration,
});
return Ok(());
}
};
let workday = match Workdays::new()?.fetch(date)? {
Some(wd) => wd,
None => {
msg_error!("No workday found for today");
return Ok(());
}
};
let config = Config::read()?;
let monitor_config = config.monitor.unwrap_or_default();
let pauses = Pauses::new()?
.set_min_duration(monitor_config.min_pause_duration)
.get_workday_pauses(&workday)?;
let break_options = find_break_placement_options(&workday, &pauses, minutes, monitor_config.min_work_interval)?;
if break_options.is_empty() {
msg_error!(Message::NoValidBreakPlacement);
return Ok(());
}
msg_print!(Message::BreakPlacementOptions);
let option_labels: Vec<String> = break_options
.iter()
.enumerate()
.map(|(i, opt)| {
format!(
"{}. {} - {} ({} min) - {}",
i + 1,
opt.start.format("%H:%M"),
opt.end.format("%H:%M"),
opt.duration.num_minutes(),
opt.description
)
})
.collect();
let selection = Select::with_theme(&theme)
.with_prompt("Select break placement")
.items(&option_labels)
.default(0)
.interact()?;
let chosen_option = &break_options[selection];
let break_record = crate::db::breaks::Break {
id: None,
date,
start: chosen_option.start,
end: chosen_option.end,
duration: chosen_option.duration,
reason: None,
created_at: None,
};
let breaks_db = Breaks::new()?;
breaks_db.insert(&break_record)?;
msg_success!(Message::BreakCreated {
start_time: chosen_option.start.format("%H:%M").to_string(),
end_time: chosen_option.end.format("%H:%M").to_string(),
duration_minutes: minutes,
});
show_updated_productivity(date).await?;
Ok(())
}
fn find_break_placement_options(
workday: &crate::db::workdays::Workday,
pauses: &[Pause],
duration_minutes: u64,
min_work_interval: u64,
) -> Result<Vec<BreakOption>> {
let mut options = Vec::new();
let current_time = Local::now().naive_local();
let workday_end = workday.end.unwrap_or(current_time);
let intervals = report::calculate_work_intervals(workday, pauses);
let break_duration = Duration::minutes(duration_minutes as i64);
if intervals.is_empty() {
return Ok(options);
}
let longest_interval = intervals.iter().max_by_key(|interval| interval.duration.num_minutes());
if let Some(interval) = longest_interval {
let required_time = break_duration + Duration::minutes(min_work_interval as i64 * 2);
if interval.duration >= required_time && interval.end <= current_time {
let interval_mid = interval.start + (interval.duration / 2);
let break_start = interval_mid - (break_duration / 2);
let break_end = break_start + break_duration;
options.push(BreakOption {
start: break_start,
end: break_end,
duration: break_duration,
description: "Middle of longest work period".to_string(),
});
}
}
for (i, pause) in pauses.iter().enumerate() {
if let Some(pause_end) = pause.end {
let next_pause_start = pauses.get(i + 1).map(|p| p.start).unwrap_or(workday_end.min(current_time));
let available_time = next_pause_start - pause_end;
let required_time = break_duration + Duration::minutes(min_work_interval as i64);
if available_time >= required_time && pause_end + break_duration <= current_time {
options.push(BreakOption {
start: pause_end,
end: pause_end + break_duration,
duration: break_duration,
description: format!("After {} pause", format_duration(&pause.duration.unwrap_or_default())),
});
}
}
}
for pause in pauses.iter() {
let work_start = workday.start;
let available_time = pause.start - work_start;
let required_time = break_duration + Duration::minutes(min_work_interval as i64);
if available_time >= required_time {
let break_end = pause.start - Duration::minutes(min_work_interval as i64);
let break_start = break_end - break_duration;
if break_start >= work_start && break_end <= current_time {
options.push(BreakOption {
start: break_start,
end: break_end,
duration: break_duration,
description: format!("Before {} pause", format_duration(&pause.duration.unwrap_or_default())),
});
}
}
}
options.sort_by_key(|opt| opt.start);
options.dedup_by(|a, b| {
(a.start - b.start).num_minutes().abs() < 5 });
options.truncate(3);
Ok(options)
}
async fn show_updated_productivity(date: NaiveDate) -> Result<()> {
let workday = Workdays::new()?.fetch(date)?.expect("Workday should exist");
let productivity = Productivity::new(&workday)?.calculate_productivity();
msg_info!(Message::ProductivityRecalculated(productivity));
Ok(())
}