1use super::history::History;
2use crate::config::{Config, Theme};
3use crate::models::{DayData, WorkRecord};
4use crate::timer::TimerState;
5use time::Date;
6
7pub enum AppMode {
8 Browse,
9 Edit,
10 Visual,
11 CommandPalette,
12 Calendar,
13 TaskPicker,
14}
15
16pub enum EditField {
17 Name,
18 Start,
19 End,
20 Description,
21}
22
23pub struct Command {
24 pub key: &'static str,
25 pub description: &'static str,
26 pub action: CommandAction,
27}
28
29#[derive(Debug, Clone, Copy)]
30pub enum CommandAction {
31 MoveUp,
32 MoveDown,
33 MoveLeft,
34 MoveRight,
35 Edit,
36 Change,
37 New,
38 Break,
39 Delete,
40 Visual,
41 SetNow,
42 Undo,
43 Redo,
44 Save,
45 StartTimer,
46 PauseTimer,
47 Quit,
48}
49
50pub struct AppState {
51 pub day_data: DayData,
52 pub current_date: Date,
53 pub mode: AppMode,
54 pub selected_index: usize,
55 pub edit_field: EditField,
56 pub input_buffer: String,
57 pub time_cursor: usize,
58 pub should_quit: bool,
59 pub visual_start: usize,
60 pub visual_end: usize,
61 pub command_palette_input: String,
62 pub command_palette_selected: usize,
63 pub available_commands: Vec<Command>,
64 pub date_changed: bool,
65 pub calendar_selected_date: Date,
66 pub calendar_view_month: time::Month,
67 pub calendar_view_year: i32,
68 pub config: Config,
69 pub theme: Theme,
70 pub last_error_message: Option<String>,
71 pub task_picker_selected: usize,
72 pub active_timer: Option<TimerState>,
73 pub last_file_modified: Option<std::time::SystemTime>,
74 history: History,
75}
76
77impl AppState {
78 pub fn new(day_data: DayData) -> Self {
79 let current_date = day_data.date;
80 let available_commands = vec![
81 Command {
82 key: "↑/k",
83 description: "Move selection up",
84 action: CommandAction::MoveUp,
85 },
86 Command {
87 key: "↓/j",
88 description: "Move selection down",
89 action: CommandAction::MoveDown,
90 },
91 Command {
92 key: "←/h",
93 description: "Move field left",
94 action: CommandAction::MoveLeft,
95 },
96 Command {
97 key: "→/l",
98 description: "Move field right",
99 action: CommandAction::MoveRight,
100 },
101 Command {
102 key: "Enter/i",
103 description: "Enter edit mode",
104 action: CommandAction::Edit,
105 },
106 Command {
107 key: "c",
108 description: "Change task name",
109 action: CommandAction::Change,
110 },
111 Command {
112 key: "n",
113 description: "Add new task",
114 action: CommandAction::New,
115 },
116 Command {
117 key: "b",
118 description: "Add break",
119 action: CommandAction::Break,
120 },
121 Command {
122 key: "d",
123 description: "Delete selected record",
124 action: CommandAction::Delete,
125 },
126 Command {
127 key: "v",
128 description: "Enter visual mode",
129 action: CommandAction::Visual,
130 },
131 Command {
132 key: "t",
133 description: "Set current time on field",
134 action: CommandAction::SetNow,
135 },
136 Command {
137 key: "u",
138 description: "Undo last change",
139 action: CommandAction::Undo,
140 },
141 Command {
142 key: "r",
143 description: "Redo last change",
144 action: CommandAction::Redo,
145 },
146 Command {
147 key: "s",
148 description: "Save to file",
149 action: CommandAction::Save,
150 },
151 Command {
152 key: "S",
153 description: "Start/Stop session (toggle)",
154 action: CommandAction::StartTimer,
155 },
156 Command {
157 key: "P",
158 description: "Pause/Resume active session",
159 action: CommandAction::PauseTimer,
160 },
161 Command {
162 key: "q",
163 description: "Quit application",
164 action: CommandAction::Quit,
165 },
166 ];
167
168 let config = Config::load().unwrap_or_default();
169 let theme = config.get_theme();
170
171 AppState {
172 calendar_selected_date: current_date,
173 calendar_view_month: current_date.month(),
174 calendar_view_year: current_date.year(),
175 day_data,
176 current_date,
177 mode: AppMode::Browse,
178 selected_index: 0,
179 edit_field: EditField::Name,
180 input_buffer: String::new(),
181 time_cursor: 0,
182 should_quit: false,
183 visual_start: 0,
184 visual_end: 0,
185 command_palette_input: String::new(),
186 command_palette_selected: 0,
187 available_commands,
188 date_changed: false,
189 config,
190 theme,
191 last_error_message: None,
192 task_picker_selected: 0,
193 active_timer: None,
194 last_file_modified: None,
195 history: History::new(),
196 }
197 }
198
199 pub fn get_selected_record(&self) -> Option<&WorkRecord> {
200 let records = self.day_data.get_sorted_records();
201 records.get(self.selected_index).copied()
202 }
203
204 pub fn move_selection_up(&mut self) {
205 if self.selected_index > 0 {
206 self.selected_index -= 1;
207 }
208 if matches!(self.mode, AppMode::Visual) {
209 self.visual_end = self.selected_index;
210 }
211 }
212
213 pub fn move_selection_down(&mut self) {
214 let record_count = self.day_data.work_records.len();
215 if self.selected_index < record_count.saturating_sub(1) {
216 self.selected_index += 1;
217 }
218 if matches!(self.mode, AppMode::Visual) {
219 self.visual_end = self.selected_index;
220 }
221 }
222
223 pub fn enter_edit_mode(&mut self) {
224 if let Some(record) = self.get_selected_record() {
225 let input_value = match self.edit_field {
226 EditField::Name => record.name.clone(),
227 EditField::Start => record.start.to_string(),
228 EditField::End => record.end.to_string(),
229 EditField::Description => record.description.clone(),
230 };
231 self.mode = AppMode::Edit;
232 self.input_buffer = input_value;
233 self.time_cursor = 0;
234 }
235 }
236
237 pub fn change_task_name(&mut self) {
238 if matches!(self.edit_field, EditField::Name) && self.get_selected_record().is_some() {
239 let task_names = self.get_unique_task_names();
241 if !task_names.is_empty() {
242 self.input_buffer.clear();
244 self.task_picker_selected = 0;
245 self.mode = AppMode::TaskPicker;
246 } else {
247 self.mode = AppMode::Edit;
249 self.input_buffer.clear();
250 self.time_cursor = 0;
251 }
252 }
253 }
254
255 pub fn exit_edit_mode(&mut self) {
256 self.mode = AppMode::Browse;
257 self.input_buffer.clear();
258 self.edit_field = EditField::Name;
259 self.time_cursor = 0;
260 }
261
262 pub fn next_field(&mut self) {
263 if let Some(record) = self.get_selected_record() {
264 self.edit_field = match self.edit_field {
265 EditField::Name => {
266 self.input_buffer = record.start.to_string();
267 self.time_cursor = 0;
268 EditField::Start
269 }
270 EditField::Start => {
271 self.input_buffer = record.end.to_string();
272 self.time_cursor = 0;
273 EditField::End
274 }
275 EditField::End => {
276 self.input_buffer = record.description.clone();
277 self.time_cursor = 0;
278 EditField::Description
279 }
280 EditField::Description => {
281 self.input_buffer = record.name.clone();
282 self.time_cursor = 0;
283 EditField::Name
284 }
285 };
286 }
287 }
288
289 pub fn handle_char_input(&mut self, c: char) {
290 match self.edit_field {
291 EditField::Name | EditField::Description => {
292 self.input_buffer.push(c);
293 }
294 EditField::Start | EditField::End => {
295 if !c.is_ascii_digit() {
296 return;
297 }
298
299 if self.input_buffer.len() != 5 {
300 return;
301 }
302
303 let positions = [0, 1, 3, 4];
304 if self.time_cursor >= positions.len() {
305 return;
306 }
307
308 let pos = positions[self.time_cursor];
309 let mut chars: Vec<char> = self.input_buffer.chars().collect();
310 chars[pos] = c;
311 self.input_buffer = chars.into_iter().collect();
312
313 self.time_cursor += 1;
314
315 if self.time_cursor >= positions.len() && self.save_current_field().is_ok() {
316 self.exit_edit_mode();
317 }
318 }
319 }
320 }
321
322 pub fn handle_backspace(&mut self) {
323 match self.edit_field {
324 EditField::Name | EditField::Description => {
325 self.input_buffer.pop();
326 }
327 EditField::Start | EditField::End => {
328 if self.time_cursor > 0 {
329 self.time_cursor -= 1;
330 }
331 }
332 }
333 }
334
335 fn save_current_field(&mut self) -> Result<(), String> {
336 let records = self.day_data.get_sorted_records();
337 if let Some(&record) = records.get(self.selected_index) {
338 let id = record.id;
339
340 if let Some(record_mut) = self.day_data.work_records.get_mut(&id) {
341 match self.edit_field {
342 EditField::Name => {
343 if self.input_buffer.trim().is_empty() {
344 return Err("Name cannot be empty".to_string());
345 }
346 record_mut.name = self.input_buffer.trim().to_string();
347 }
348 EditField::Start => {
349 record_mut.start = self
350 .input_buffer
351 .parse()
352 .map_err(|_| "Invalid start time format (use HH:MM)".to_string())?;
353 record_mut.update_duration();
354 }
355 EditField::End => {
356 record_mut.end = self
357 .input_buffer
358 .parse()
359 .map_err(|_| "Invalid end time format (use HH:MM)".to_string())?;
360 record_mut.update_duration();
361 }
362 EditField::Description => {
363 record_mut.description = self.input_buffer.trim().to_string();
364 }
365 }
366 }
367 }
368 Ok(())
369 }
370
371 pub fn save_edit(&mut self) -> Result<(), String> {
372 self.save_snapshot();
373 self.save_current_field()?;
374 self.exit_edit_mode();
375 Ok(())
376 }
377
378 pub fn add_new_record(&mut self) {
379 use crate::models::{TimePoint, WorkRecord};
380
381 self.save_snapshot();
382
383 let id = self.day_data.next_id();
384
385 let (default_start, default_end) = if let Some(current_record) = self.get_selected_record()
386 {
387 let start_minutes = current_record.end.to_minutes_since_midnight();
388 let end_minutes = (start_minutes + 60).min(24 * 60 - 1);
389 (
390 current_record.end,
391 TimePoint::from_minutes_since_midnight(end_minutes).unwrap(),
392 )
393 } else {
394 (
395 TimePoint::new(9, 0).unwrap(),
396 TimePoint::new(10, 0).unwrap(),
397 )
398 };
399
400 let record = WorkRecord::new(id, "New Task".to_string(), default_start, default_end);
401
402 self.day_data.add_record(record);
403
404 let records = self.day_data.get_sorted_records();
405 self.selected_index = records.iter().position(|r| r.id == id).unwrap_or(0);
406 }
407
408 pub fn add_break(&mut self) {
409 use crate::models::{TimePoint, WorkRecord};
410
411 self.save_snapshot();
412
413 let id = self.day_data.next_id();
414
415 let (default_start, default_end) = if let Some(current_record) = self.get_selected_record()
416 {
417 let start_minutes = current_record.end.to_minutes_since_midnight();
418 let end_minutes = (start_minutes + 15).min(24 * 60 - 1);
419 (
420 current_record.end,
421 TimePoint::from_minutes_since_midnight(end_minutes).unwrap(),
422 )
423 } else {
424 (
425 TimePoint::new(12, 0).unwrap(),
426 TimePoint::new(12, 15).unwrap(),
427 )
428 };
429
430 let record = WorkRecord::new(id, "Break".to_string(), default_start, default_end);
431
432 self.day_data.add_record(record);
433
434 let records = self.day_data.get_sorted_records();
435 self.selected_index = records.iter().position(|r| r.id == id).unwrap_or(0);
436 }
437
438 pub fn delete_selected_record(&mut self) {
439 self.save_snapshot();
440
441 let records = self.day_data.get_sorted_records();
442 if let Some(&record) = records.get(self.selected_index) {
443 self.day_data.remove_record(record.id);
444
445 if self.selected_index >= self.day_data.work_records.len() {
446 self.selected_index = self.day_data.work_records.len().saturating_sub(1);
447 }
448 }
449 }
450
451 pub fn move_field_left(&mut self) {
452 self.edit_field = match self.edit_field {
453 EditField::Name => EditField::Description,
454 EditField::Start => EditField::Name,
455 EditField::End => EditField::Start,
456 EditField::Description => EditField::End,
457 };
458 }
459
460 pub fn move_field_right(&mut self) {
461 self.edit_field = match self.edit_field {
462 EditField::Name => EditField::Start,
463 EditField::Start => EditField::End,
464 EditField::End => EditField::Description,
465 EditField::Description => EditField::Name,
466 };
467 }
468
469 pub fn set_current_time_on_field(&mut self) {
470 use time::{OffsetDateTime, UtcOffset};
471
472 self.save_snapshot();
473
474 let local_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
475 let now = OffsetDateTime::now_utc().to_offset(local_offset);
476 let current_time = format!("{:02}:{:02}", now.hour(), now.minute());
477
478 let records = self.day_data.get_sorted_records();
479 if let Some(&record) = records.get(self.selected_index) {
480 let id = record.id;
481
482 if let Some(record_mut) = self.day_data.work_records.get_mut(&id) {
483 match self.edit_field {
484 EditField::Start => {
485 if let Ok(time_point) = current_time.parse() {
486 record_mut.start = time_point;
487 record_mut.update_duration();
488 }
489 }
490 EditField::End => {
491 if let Ok(time_point) = current_time.parse() {
492 record_mut.end = time_point;
493 record_mut.update_duration();
494 }
495 }
496 _ => {}
497 }
498 }
499 }
500 }
501
502 pub fn enter_visual_mode(&mut self) {
503 self.mode = AppMode::Visual;
504 self.visual_start = self.selected_index;
505 self.visual_end = self.selected_index;
506 }
507
508 pub fn exit_visual_mode(&mut self) {
509 self.mode = AppMode::Browse;
510 }
511
512 pub fn is_in_visual_selection(&self, index: usize) -> bool {
513 let start = self.visual_start.min(self.visual_end);
514 let end = self.visual_start.max(self.visual_end);
515
516 index >= start && index <= end
517 }
518
519 pub fn delete_visual_selection(&mut self) {
520 self.save_snapshot();
521
522 let records = self.day_data.get_sorted_records();
523 let start = self.visual_start.min(self.visual_end);
524 let end = self.visual_start.max(self.visual_end);
525
526 let ids_to_delete: Vec<u32> = records
527 .iter()
528 .enumerate()
529 .filter(|(i, _)| *i >= start && *i <= end)
530 .map(|(_, record)| record.id)
531 .collect();
532
533 for id in ids_to_delete {
534 self.day_data.remove_record(id);
535 }
536
537 if self.selected_index >= self.day_data.work_records.len() {
538 self.selected_index = self.day_data.work_records.len().saturating_sub(1);
539 }
540
541 self.exit_visual_mode();
542 }
543
544 fn save_snapshot(&mut self) {
545 self.history.push(self.day_data.clone());
546 }
547
548 pub fn undo(&mut self) {
549 if let Some(previous_state) = self.history.undo(self.day_data.clone()) {
550 self.day_data = previous_state;
551
552 if self.selected_index >= self.day_data.work_records.len() {
553 self.selected_index = self.day_data.work_records.len().saturating_sub(1);
554 }
555 }
556 }
557
558 pub fn redo(&mut self) {
559 if let Some(next_state) = self.history.redo(self.day_data.clone()) {
560 self.day_data = next_state;
561
562 if self.selected_index >= self.day_data.work_records.len() {
563 self.selected_index = self.day_data.work_records.len().saturating_sub(1);
564 }
565 }
566 }
567
568 pub fn open_command_palette(&mut self) {
569 self.mode = AppMode::CommandPalette;
570 self.command_palette_input.clear();
571 self.command_palette_selected = 0;
572 }
573
574 pub fn close_command_palette(&mut self) {
575 self.mode = AppMode::Browse;
576 self.command_palette_input.clear();
577 self.command_palette_selected = 0;
578 }
579
580 pub fn handle_command_palette_char(&mut self, c: char) {
581 self.command_palette_input.push(c);
582 self.command_palette_selected = 0;
583 }
584
585 pub fn handle_command_palette_backspace(&mut self) {
586 self.command_palette_input.pop();
587 self.command_palette_selected = 0;
588 }
589
590 pub fn move_command_palette_up(&mut self) {
591 if self.command_palette_selected > 0 {
592 self.command_palette_selected -= 1;
593 }
594 }
595
596 pub fn move_command_palette_down(&mut self, filtered_count: usize) {
597 if self.command_palette_selected < filtered_count.saturating_sub(1) {
598 self.command_palette_selected += 1;
599 }
600 }
601
602 pub fn get_filtered_commands(&self) -> Vec<(usize, i64, &Command)> {
603 use fuzzy_matcher::FuzzyMatcher;
604 use fuzzy_matcher::skim::SkimMatcherV2;
605
606 let matcher = SkimMatcherV2::default();
607 let query = self.command_palette_input.as_str();
608
609 if query.is_empty() {
610 return self
611 .available_commands
612 .iter()
613 .enumerate()
614 .map(|(i, cmd)| (i, 0, cmd))
615 .collect();
616 }
617
618 let mut results: Vec<(usize, i64, &Command)> = self
619 .available_commands
620 .iter()
621 .enumerate()
622 .filter_map(|(i, cmd)| {
623 let search_text = format!("{} {}", cmd.key, cmd.description);
624 matcher
625 .fuzzy_match(&search_text, query)
626 .map(|score| (i, score, cmd))
627 })
628 .collect();
629
630 results.sort_by(|a, b| b.1.cmp(&a.1));
631 results
632 }
633
634 pub fn execute_selected_command(&mut self) -> Option<CommandAction> {
635 let filtered = self.get_filtered_commands();
636 let action = filtered
637 .get(self.command_palette_selected)
638 .map(|(_, _, cmd)| cmd.action);
639 self.close_command_palette();
640 action
641 }
642
643 pub fn navigate_to_previous_day(&mut self) {
644 use time::Duration;
645
646 self.current_date = self.current_date.saturating_sub(Duration::days(1));
647 self.date_changed = true;
648 }
649
650 pub fn navigate_to_next_day(&mut self) {
651 use time::Duration;
652
653 self.current_date = self.current_date.saturating_add(Duration::days(1));
654 self.date_changed = true;
655 }
656
657 pub fn load_new_day_data(&mut self, new_day_data: DayData) {
658 self.day_data = new_day_data;
659 self.selected_index = 0;
660 self.history = History::new();
661 self.date_changed = false;
662 }
663
664 pub fn open_calendar(&mut self) {
665 self.mode = AppMode::Calendar;
666 self.calendar_selected_date = self.current_date;
667 self.calendar_view_month = self.current_date.month();
668 self.calendar_view_year = self.current_date.year();
669 }
670
671 pub fn close_calendar(&mut self) {
672 self.mode = AppMode::Browse;
673 }
674
675 pub fn calendar_navigate_left(&mut self) {
676 use time::Duration;
677 self.calendar_selected_date = self
678 .calendar_selected_date
679 .saturating_sub(Duration::days(1));
680 self.calendar_view_month = self.calendar_selected_date.month();
681 self.calendar_view_year = self.calendar_selected_date.year();
682 }
683
684 pub fn calendar_navigate_right(&mut self) {
685 use time::Duration;
686 self.calendar_selected_date = self
687 .calendar_selected_date
688 .saturating_add(Duration::days(1));
689 self.calendar_view_month = self.calendar_selected_date.month();
690 self.calendar_view_year = self.calendar_selected_date.year();
691 }
692
693 pub fn calendar_navigate_up(&mut self) {
694 use time::Duration;
695 self.calendar_selected_date = self
696 .calendar_selected_date
697 .saturating_sub(Duration::days(7));
698 self.calendar_view_month = self.calendar_selected_date.month();
699 self.calendar_view_year = self.calendar_selected_date.year();
700 }
701
702 pub fn calendar_navigate_down(&mut self) {
703 use time::Duration;
704 self.calendar_selected_date = self
705 .calendar_selected_date
706 .saturating_add(Duration::days(7));
707 self.calendar_view_month = self.calendar_selected_date.month();
708 self.calendar_view_year = self.calendar_selected_date.year();
709 }
710
711 pub fn calendar_previous_month(&mut self) {
712 use time::Month;
713
714 let (new_month, new_year) = match self.calendar_view_month {
715 Month::January => (Month::December, self.calendar_view_year - 1),
716 Month::February => (Month::January, self.calendar_view_year),
717 Month::March => (Month::February, self.calendar_view_year),
718 Month::April => (Month::March, self.calendar_view_year),
719 Month::May => (Month::April, self.calendar_view_year),
720 Month::June => (Month::May, self.calendar_view_year),
721 Month::July => (Month::June, self.calendar_view_year),
722 Month::August => (Month::July, self.calendar_view_year),
723 Month::September => (Month::August, self.calendar_view_year),
724 Month::October => (Month::September, self.calendar_view_year),
725 Month::November => (Month::October, self.calendar_view_year),
726 Month::December => (Month::November, self.calendar_view_year),
727 };
728
729 self.calendar_view_month = new_month;
730 self.calendar_view_year = new_year;
731
732 if self.calendar_selected_date.month() != new_month
734 || self.calendar_selected_date.year() != new_year
735 {
736 let day = self
738 .calendar_selected_date
739 .day()
740 .min(days_in_month(new_month, new_year));
741 self.calendar_selected_date =
742 time::Date::from_calendar_date(new_year, new_month, day).unwrap();
743 }
744 }
745
746 pub fn calendar_next_month(&mut self) {
747 use time::Month;
748
749 let (new_month, new_year) = match self.calendar_view_month {
750 Month::January => (Month::February, self.calendar_view_year),
751 Month::February => (Month::March, self.calendar_view_year),
752 Month::March => (Month::April, self.calendar_view_year),
753 Month::April => (Month::May, self.calendar_view_year),
754 Month::May => (Month::June, self.calendar_view_year),
755 Month::June => (Month::July, self.calendar_view_year),
756 Month::July => (Month::August, self.calendar_view_year),
757 Month::August => (Month::September, self.calendar_view_year),
758 Month::September => (Month::October, self.calendar_view_year),
759 Month::October => (Month::November, self.calendar_view_year),
760 Month::November => (Month::December, self.calendar_view_year),
761 Month::December => (Month::January, self.calendar_view_year + 1),
762 };
763
764 self.calendar_view_month = new_month;
765 self.calendar_view_year = new_year;
766
767 if self.calendar_selected_date.month() != new_month
769 || self.calendar_selected_date.year() != new_year
770 {
771 let day = self
773 .calendar_selected_date
774 .day()
775 .min(days_in_month(new_month, new_year));
776 self.calendar_selected_date =
777 time::Date::from_calendar_date(new_year, new_month, day).unwrap();
778 }
779 }
780
781 pub fn calendar_select_date(&mut self) {
782 self.current_date = self.calendar_selected_date;
783 self.date_changed = true;
784 self.close_calendar();
785 }
786
787 pub fn open_ticket_in_browser(&mut self) {
788 use crate::integrations::{build_url, detect_tracker, extract_ticket_from_name};
789
790 if let Some(record) = self.get_selected_record() {
791 if let Some(ticket_id) = extract_ticket_from_name(&record.name) {
792 if let Some(tracker_name) = detect_tracker(&ticket_id, &self.config) {
793 match build_url(&ticket_id, &tracker_name, &self.config, false) {
794 Ok(url) => {
795 if let Err(e) = open_url_in_browser(&url) {
796 self.last_error_message =
797 Some(format!("Failed to open browser: {}", e));
798 }
799 }
800 Err(e) => {
801 self.last_error_message = Some(format!("Failed to build URL: {}", e));
802 }
803 }
804 } else {
805 self.last_error_message =
806 Some("Could not detect tracker for ticket".to_string());
807 }
808 } else {
809 self.last_error_message = Some("No ticket found in task name".to_string());
810 }
811 }
812 }
813
814 pub fn open_worklog_in_browser(&mut self) {
815 use crate::integrations::{build_url, detect_tracker, extract_ticket_from_name};
816
817 if let Some(record) = self.get_selected_record() {
818 if let Some(ticket_id) = extract_ticket_from_name(&record.name) {
819 if let Some(tracker_name) = detect_tracker(&ticket_id, &self.config) {
820 match build_url(&ticket_id, &tracker_name, &self.config, true) {
821 Ok(url) => {
822 if let Err(e) = open_url_in_browser(&url) {
823 self.last_error_message =
824 Some(format!("Failed to open browser: {}", e));
825 }
826 }
827 Err(e) => {
828 self.last_error_message = Some(format!("Failed to build URL: {}", e));
829 }
830 }
831 } else {
832 self.last_error_message =
833 Some("Could not detect tracker for ticket".to_string());
834 }
835 } else {
836 self.last_error_message = Some("No ticket found in task name".to_string());
837 }
838 }
839 }
840
841 pub fn clear_error(&mut self) {
842 self.last_error_message = None;
843 }
844
845 pub fn close_task_picker(&mut self) {
846 self.input_buffer.clear();
848 self.mode = AppMode::Browse;
849 }
850
851 pub fn get_unique_task_names(&self) -> Vec<String> {
852 use std::collections::HashSet;
853
854 let mut seen = HashSet::new();
855 let mut task_names = Vec::new();
856
857 for record in self.day_data.work_records.values() {
858 let name = record.name.trim().to_string();
859 if !name.is_empty() && seen.insert(name.clone()) {
860 task_names.push(name);
861 }
862 }
863
864 task_names.sort();
865 task_names
866 }
867
868 pub fn get_filtered_task_names(&self) -> Vec<String> {
869 let all_tasks = self.get_unique_task_names();
870 let filter = self.input_buffer.to_lowercase();
871
872 if filter.is_empty() {
873 return all_tasks;
874 }
875
876 all_tasks
877 .into_iter()
878 .filter(|task| task.to_lowercase().contains(&filter))
879 .collect()
880 }
881
882 pub fn move_task_picker_up(&mut self) {
883 if self.task_picker_selected > 0 {
884 self.task_picker_selected -= 1;
885 }
886 }
887
888 pub fn move_task_picker_down(&mut self, task_count: usize) {
889 if self.task_picker_selected < task_count.saturating_sub(1) {
890 self.task_picker_selected += 1;
891 }
892 }
893
894 pub fn select_task_from_picker(&mut self) {
895 let filtered_tasks = self.get_filtered_task_names();
896
897 if filtered_tasks.is_empty() {
898 } else if let Some(selected_name) = filtered_tasks.get(self.task_picker_selected) {
901 self.input_buffer = selected_name.clone();
903 }
904
905 if let Some(record) = self.get_selected_record() {
907 let record_id = record.id;
908 let new_name = self.input_buffer.trim().to_string();
909
910 self.save_snapshot();
911 if let Some(work_record) = self.day_data.work_records.get_mut(&record_id) {
912 work_record.name = new_name;
913 }
914 }
915
916 self.input_buffer.clear();
917 self.mode = AppMode::Browse;
918 }
919
920 pub fn handle_task_picker_char(&mut self, c: char) {
921 self.input_buffer.push(c);
922 self.task_picker_selected = 0;
924 }
925
926 pub fn handle_task_picker_backspace(&mut self) {
927 self.input_buffer.pop();
928 self.task_picker_selected = 0;
930 }
931
932 pub fn start_timer_for_selected(
934 &mut self,
935 storage: &crate::storage::StorageManager,
936 ) -> Result<(), String> {
937 if let Some(record) = self.get_selected_record() {
938 match storage.start_timer(
939 record.name.clone(),
940 Some(record.description.clone()),
941 Some(record.id),
942 Some(self.current_date),
943 ) {
944 Ok(timer) => {
945 self.active_timer = Some(timer);
946 Ok(())
947 }
948 Err(e) => Err(e.to_string()),
949 }
950 } else {
951 Err("No record selected".to_string())
952 }
953 }
954
955 pub fn stop_active_timer(
957 &mut self,
958 storage: &mut crate::storage::StorageManager,
959 ) -> Result<(), String> {
960 if self.active_timer.is_some() {
961 match storage.stop_timer() {
962 Ok(_work_record) => {
963 self.active_timer = None;
964 match storage.load_with_tracking(self.current_date) {
966 Ok(new_day_data) => {
967 self.day_data = new_day_data;
968 self.selected_index = 0;
969 Ok(())
970 }
971 Err(e) => Err(format!("Failed to reload day data: {}", e)),
972 }
973 }
974 Err(e) => Err(e.to_string()),
975 }
976 } else {
977 Err("No active timer".to_string())
978 }
979 }
980
981 pub fn pause_active_timer(
983 &mut self,
984 storage: &crate::storage::StorageManager,
985 ) -> Result<(), String> {
986 if self.active_timer.is_some() {
987 match storage.pause_timer() {
988 Ok(paused_timer) => {
989 self.active_timer = Some(paused_timer);
990 Ok(())
991 }
992 Err(e) => Err(e.to_string()),
993 }
994 } else {
995 Err("No active timer".to_string())
996 }
997 }
998
999 pub fn resume_active_timer(
1001 &mut self,
1002 storage: &crate::storage::StorageManager,
1003 ) -> Result<(), String> {
1004 if self.active_timer.is_some() {
1005 match storage.resume_timer() {
1006 Ok(resumed_timer) => {
1007 self.active_timer = Some(resumed_timer);
1008 Ok(())
1009 }
1010 Err(e) => Err(e.to_string()),
1011 }
1012 } else {
1013 Err("No active timer".to_string())
1014 }
1015 }
1016
1017 pub fn get_timer_status(&self) -> Option<&TimerState> {
1019 self.active_timer.as_ref()
1020 }
1021
1022 pub fn check_and_reload_if_modified(
1025 &mut self,
1026 storage: &mut crate::storage::StorageManager,
1027 ) -> bool {
1028 let mut changed = false;
1029
1030 if let Ok(Some(new_data)) = storage.check_and_reload(self.current_date) {
1032 self.day_data = new_data;
1033 self.last_file_modified = storage.get_last_modified(&self.current_date);
1034
1035 let record_count = self.day_data.work_records.len();
1037 if self.selected_index >= record_count && record_count > 0 {
1038 self.selected_index = record_count - 1;
1039 }
1040
1041 changed = true;
1042 }
1043
1044 if let Ok(Some(timer)) = storage.load_active_timer() {
1046 if self.active_timer.is_none() || self.active_timer.as_ref() != Some(&timer) {
1048 self.active_timer = Some(timer);
1049 changed = true;
1050 }
1051 } else if self.active_timer.is_some() {
1052 self.active_timer = None;
1054 changed = true;
1055 }
1056
1057 changed
1058 }
1059}
1060
1061fn open_url_in_browser(url: &str) -> std::io::Result<()> {
1068 #[cfg(target_os = "macos")]
1069 {
1070 std::process::Command::new("open").arg(url).spawn()?;
1071 }
1072
1073 #[cfg(target_os = "windows")]
1074 {
1075 use std::os::windows::process::CommandExt;
1079 std::process::Command::new("cmd")
1080 .raw_arg(format!("/C start \"\" \"{}\"", url))
1081 .spawn()?;
1082 }
1083
1084 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
1085 {
1086 std::process::Command::new("xdg-open").arg(url).spawn()?;
1088 }
1089
1090 Ok(())
1091}
1092
1093fn days_in_month(month: time::Month, year: i32) -> u8 {
1094 use time::Month;
1095 match month {
1096 Month::January
1097 | Month::March
1098 | Month::May
1099 | Month::July
1100 | Month::August
1101 | Month::October
1102 | Month::December => 31,
1103 Month::April | Month::June | Month::September | Month::November => 30,
1104 Month::February => {
1105 if is_leap_year(year) {
1106 29
1107 } else {
1108 28
1109 }
1110 }
1111 }
1112}
1113
1114fn is_leap_year(year: i32) -> bool {
1115 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
1116}