1use crate::{
27 db::{breaks::Breaks, pauses::Pauses, workdays::Workdays},
28 libs::{config::Config, formatter::format_duration, messages::Message, pause::Pause, productivity::Productivity, report},
29 msg_error, msg_info, msg_print, msg_success,
30};
31use anyhow::Result;
32use chrono::{Duration, Local, NaiveDate, NaiveDateTime};
33use clap::Args;
34use dialoguer::{Input, Select, theme::ColorfulTheme};
35
36#[derive(Debug, Args)]
41pub struct BreaksArgs {
42 #[arg(long, short)]
48 minutes: Option<u64>,
49
50 #[arg(long)]
55 force: bool,
56}
57
58#[derive(Debug, Clone)]
63pub struct BreakOption {
64 pub start: NaiveDateTime,
66 pub end: NaiveDateTime,
68 pub duration: Duration,
70 pub description: String,
72}
73
74pub async fn cmd(args: BreaksArgs) -> Result<()> {
88 let today = Local::now().date_naive();
89
90 let config = Config::read()?;
92 let productivity_config = config.productivity.unwrap_or_default();
93
94 if let Some(minutes) = args.minutes {
95 handle_automatic_break_placement(today, minutes, &productivity_config, args.force).await
96 } else {
97 handle_interactive_break_creation(today, &productivity_config, args.force).await
98 }
99}
100
101async fn handle_automatic_break_placement(
107 date: NaiveDate,
108 minutes: u64,
109 productivity_config: &crate::libs::config::ProductivityConfig,
110 _force: bool,
111) -> Result<()> {
112 if minutes < productivity_config.min_break_duration {
114 msg_error!(Message::BreakDurationPrompt {
115 min_duration: productivity_config.min_break_duration,
116 max_duration: productivity_config.max_break_duration,
117 });
118 return Ok(());
119 }
120
121 if minutes > productivity_config.max_break_duration {
122 msg_error!(Message::BreakDurationPrompt {
123 min_duration: productivity_config.min_break_duration,
124 max_duration: productivity_config.max_break_duration,
125 });
126 return Ok(());
127 }
128
129 let workday = match Workdays::new()?.fetch(date)? {
131 Some(wd) => wd,
132 None => {
133 msg_error!("No workday found for today");
134 return Ok(());
135 }
136 };
137
138 let config = Config::read()?;
139 let monitor_config = config.monitor.unwrap_or_default();
140 let pauses = Pauses::new()?
141 .set_min_duration(monitor_config.min_pause_duration)
142 .get_workday_pauses(&workday)?;
143
144 let break_options = find_break_placement_options(&workday, &pauses, minutes, monitor_config.min_work_interval)?;
146
147 if break_options.is_empty() {
148 msg_error!(Message::NoValidBreakPlacement);
149 return Ok(());
150 }
151
152 let break_option = &break_options[0];
154
155 let break_record = crate::db::breaks::Break {
157 id: None,
158 date,
159 start: break_option.start,
160 end: break_option.end,
161 duration: break_option.duration,
162 reason: None,
163 created_at: None,
164 };
165
166 let breaks_db = Breaks::new()?;
167 breaks_db.insert(&break_record)?;
168
169 msg_success!(Message::BreakCreated {
170 start_time: break_option.start.format("%H:%M").to_string(),
171 end_time: break_option.end.format("%H:%M").to_string(),
172 duration_minutes: minutes,
173 });
174
175 show_updated_productivity(date).await?;
177
178 Ok(())
179}
180
181async fn handle_interactive_break_creation(date: NaiveDate, productivity_config: &crate::libs::config::ProductivityConfig, _force: bool) -> Result<()> {
186 msg_print!(Message::BreakInteractivePrompt);
187
188 let theme = ColorfulTheme::default();
190 let duration_input: String = Input::with_theme(&theme)
191 .with_prompt(format!(
192 "Enter break duration ({}-{} minutes)",
193 productivity_config.min_break_duration, productivity_config.max_break_duration
194 ))
195 .interact_text()?;
196
197 let minutes: u64 = match duration_input.parse() {
198 Ok(m) if m >= productivity_config.min_break_duration && m <= productivity_config.max_break_duration => m,
199 _ => {
200 msg_error!(Message::BreakDurationPrompt {
201 min_duration: productivity_config.min_break_duration,
202 max_duration: productivity_config.max_break_duration,
203 });
204 return Ok(());
205 }
206 };
207
208 let workday = match Workdays::new()?.fetch(date)? {
210 Some(wd) => wd,
211 None => {
212 msg_error!("No workday found for today");
213 return Ok(());
214 }
215 };
216
217 let config = Config::read()?;
218 let monitor_config = config.monitor.unwrap_or_default();
219 let pauses = Pauses::new()?
220 .set_min_duration(monitor_config.min_pause_duration)
221 .get_workday_pauses(&workday)?;
222
223 let break_options = find_break_placement_options(&workday, &pauses, minutes, monitor_config.min_work_interval)?;
225
226 if break_options.is_empty() {
227 msg_error!(Message::NoValidBreakPlacement);
228 return Ok(());
229 }
230
231 msg_print!(Message::BreakPlacementOptions);
233 let option_labels: Vec<String> = break_options
234 .iter()
235 .enumerate()
236 .map(|(i, opt)| {
237 format!(
238 "{}. {} - {} ({} min) - {}",
239 i + 1,
240 opt.start.format("%H:%M"),
241 opt.end.format("%H:%M"),
242 opt.duration.num_minutes(),
243 opt.description
244 )
245 })
246 .collect();
247
248 let selection = Select::with_theme(&theme)
249 .with_prompt("Select break placement")
250 .items(&option_labels)
251 .default(0)
252 .interact()?;
253
254 let chosen_option = &break_options[selection];
255
256 let break_record = crate::db::breaks::Break {
258 id: None,
259 date,
260 start: chosen_option.start,
261 end: chosen_option.end,
262 duration: chosen_option.duration,
263 reason: None,
264 created_at: None,
265 };
266
267 let breaks_db = Breaks::new()?;
268 breaks_db.insert(&break_record)?;
269
270 msg_success!(Message::BreakCreated {
271 start_time: chosen_option.start.format("%H:%M").to_string(),
272 end_time: chosen_option.end.format("%H:%M").to_string(),
273 duration_minutes: minutes,
274 });
275
276 show_updated_productivity(date).await?;
278
279 Ok(())
280}
281
282fn find_break_placement_options(
287 workday: &crate::db::workdays::Workday,
288 pauses: &[Pause],
289 duration_minutes: u64,
290 min_work_interval: u64,
291) -> Result<Vec<BreakOption>> {
292 let mut options = Vec::new();
293 let current_time = Local::now().naive_local();
294 let workday_end = workday.end.unwrap_or(current_time);
295
296 let intervals = report::calculate_work_intervals(workday, pauses);
298
299 let break_duration = Duration::minutes(duration_minutes as i64);
301
302 if intervals.is_empty() {
303 return Ok(options);
304 }
305
306 let longest_interval = intervals.iter().max_by_key(|interval| interval.duration.num_minutes());
308
309 if let Some(interval) = longest_interval {
310 let required_time = break_duration + Duration::minutes(min_work_interval as i64 * 2);
312 if interval.duration >= required_time && interval.end <= current_time {
313 let interval_mid = interval.start + (interval.duration / 2);
314 let break_start = interval_mid - (break_duration / 2);
315 let break_end = break_start + break_duration;
316
317 options.push(BreakOption {
318 start: break_start,
319 end: break_end,
320 duration: break_duration,
321 description: "Middle of longest work period".to_string(),
322 });
323 }
324 }
325
326 for (i, pause) in pauses.iter().enumerate() {
328 if let Some(pause_end) = pause.end {
329 let next_pause_start = pauses.get(i + 1).map(|p| p.start).unwrap_or(workday_end.min(current_time));
331
332 let available_time = next_pause_start - pause_end;
333 let required_time = break_duration + Duration::minutes(min_work_interval as i64);
334
335 if available_time >= required_time && pause_end + break_duration <= current_time {
336 options.push(BreakOption {
337 start: pause_end,
338 end: pause_end + break_duration,
339 duration: break_duration,
340 description: format!("After {} pause", format_duration(&pause.duration.unwrap_or_default())),
341 });
342 }
343 }
344 }
345
346 for pause in pauses.iter() {
348 let work_start = workday.start;
349 let available_time = pause.start - work_start;
350 let required_time = break_duration + Duration::minutes(min_work_interval as i64);
351
352 if available_time >= required_time {
353 let break_end = pause.start - Duration::minutes(min_work_interval as i64);
354 let break_start = break_end - break_duration;
355
356 if break_start >= work_start && break_end <= current_time {
357 options.push(BreakOption {
358 start: break_start,
359 end: break_end,
360 duration: break_duration,
361 description: format!("Before {} pause", format_duration(&pause.duration.unwrap_or_default())),
362 });
363 }
364 }
365 }
366
367 options.sort_by_key(|opt| opt.start);
369 options.dedup_by(|a, b| {
370 (a.start - b.start).num_minutes().abs() < 5 });
372
373 options.truncate(3);
375
376 Ok(options)
377}
378
379async fn show_updated_productivity(date: NaiveDate) -> Result<()> {
398 let workday = Workdays::new()?.fetch(date)?.expect("Workday should exist");
400
401 let productivity = Productivity::new(&workday)?.calculate_productivity();
403
404 msg_info!(Message::ProductivityRecalculated(productivity));
405 Ok(())
406}