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