work_tuimer/ui/
app_state.rs

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            // Check if there are any existing tasks to pick from
240            let task_names = self.get_unique_task_names();
241            if !task_names.is_empty() {
242                // Open task picker if tasks exist
243                self.input_buffer.clear();
244                self.task_picker_selected = 0;
245                self.mode = AppMode::TaskPicker;
246            } else {
247                // Go directly to edit mode if no tasks exist
248                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        // Adjust selected date if it's in a different month
733        if self.calendar_selected_date.month() != new_month
734            || self.calendar_selected_date.year() != new_year
735        {
736            // Try to keep same day of month, or use last valid day
737            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        // Adjust selected date if it's in a different month
768        if self.calendar_selected_date.month() != new_month
769            || self.calendar_selected_date.year() != new_year
770        {
771            // Try to keep same day of month, or use last valid day
772            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                            // Open browser using platform-specific command
796                            let result = if cfg!(target_os = "macos") {
797                                std::process::Command::new("open").arg(&url).spawn()
798                            } else if cfg!(target_os = "windows") {
799                                std::process::Command::new("cmd")
800                                    .args(["/C", "start", &url])
801                                    .spawn()
802                            } else {
803                                // Linux/Unix
804                                std::process::Command::new("xdg-open").arg(&url).spawn()
805                            };
806
807                            if let Err(e) = result {
808                                self.last_error_message =
809                                    Some(format!("Failed to open browser: {}", e));
810                            }
811                        }
812                        Err(e) => {
813                            self.last_error_message = Some(format!("Failed to build URL: {}", e));
814                        }
815                    }
816                } else {
817                    self.last_error_message =
818                        Some("Could not detect tracker for ticket".to_string());
819                }
820            } else {
821                self.last_error_message = Some("No ticket found in task name".to_string());
822            }
823        }
824    }
825
826    pub fn open_worklog_in_browser(&mut self) {
827        use crate::integrations::{build_url, detect_tracker, extract_ticket_from_name};
828
829        if let Some(record) = self.get_selected_record() {
830            if let Some(ticket_id) = extract_ticket_from_name(&record.name) {
831                if let Some(tracker_name) = detect_tracker(&ticket_id, &self.config) {
832                    match build_url(&ticket_id, &tracker_name, &self.config, true) {
833                        Ok(url) => {
834                            // Open browser using platform-specific command
835                            let result = if cfg!(target_os = "macos") {
836                                std::process::Command::new("open").arg(&url).spawn()
837                            } else if cfg!(target_os = "windows") {
838                                std::process::Command::new("cmd")
839                                    .args(["/C", "start", &url])
840                                    .spawn()
841                            } else {
842                                // Linux/Unix
843                                std::process::Command::new("xdg-open").arg(&url).spawn()
844                            };
845
846                            if let Err(e) = result {
847                                self.last_error_message =
848                                    Some(format!("Failed to open browser: {}", e));
849                            }
850                        }
851                        Err(e) => {
852                            self.last_error_message = Some(format!("Failed to build URL: {}", e));
853                        }
854                    }
855                } else {
856                    self.last_error_message =
857                        Some("Could not detect tracker for ticket".to_string());
858                }
859            } else {
860                self.last_error_message = Some("No ticket found in task name".to_string());
861            }
862        }
863    }
864
865    pub fn clear_error(&mut self) {
866        self.last_error_message = None;
867    }
868
869    pub fn close_task_picker(&mut self) {
870        // Cancel and return to Browse mode
871        self.input_buffer.clear();
872        self.mode = AppMode::Browse;
873    }
874
875    pub fn get_unique_task_names(&self) -> Vec<String> {
876        use std::collections::HashSet;
877
878        let mut seen = HashSet::new();
879        let mut task_names = Vec::new();
880
881        for record in self.day_data.work_records.values() {
882            let name = record.name.trim().to_string();
883            if !name.is_empty() && seen.insert(name.clone()) {
884                task_names.push(name);
885            }
886        }
887
888        task_names.sort();
889        task_names
890    }
891
892    pub fn get_filtered_task_names(&self) -> Vec<String> {
893        let all_tasks = self.get_unique_task_names();
894        let filter = self.input_buffer.to_lowercase();
895
896        if filter.is_empty() {
897            return all_tasks;
898        }
899
900        all_tasks
901            .into_iter()
902            .filter(|task| task.to_lowercase().contains(&filter))
903            .collect()
904    }
905
906    pub fn move_task_picker_up(&mut self) {
907        if self.task_picker_selected > 0 {
908            self.task_picker_selected -= 1;
909        }
910    }
911
912    pub fn move_task_picker_down(&mut self, task_count: usize) {
913        if self.task_picker_selected < task_count.saturating_sub(1) {
914            self.task_picker_selected += 1;
915        }
916    }
917
918    pub fn select_task_from_picker(&mut self) {
919        let filtered_tasks = self.get_filtered_task_names();
920
921        if filtered_tasks.is_empty() {
922            // No matches - use the typed input as-is (creating new task)
923            // input_buffer already contains the typed text
924        } else if let Some(selected_name) = filtered_tasks.get(self.task_picker_selected) {
925            // Select from filtered list
926            self.input_buffer = selected_name.clone();
927        }
928
929        // Save the task name and return to Browse mode
930        if let Some(record) = self.get_selected_record() {
931            let record_id = record.id;
932            let new_name = self.input_buffer.trim().to_string();
933
934            self.save_snapshot();
935            if let Some(work_record) = self.day_data.work_records.get_mut(&record_id) {
936                work_record.name = new_name;
937            }
938        }
939
940        self.input_buffer.clear();
941        self.mode = AppMode::Browse;
942    }
943
944    pub fn handle_task_picker_char(&mut self, c: char) {
945        self.input_buffer.push(c);
946        // Reset selection when typing
947        self.task_picker_selected = 0;
948    }
949
950    pub fn handle_task_picker_backspace(&mut self) {
951        self.input_buffer.pop();
952        // Reset selection when deleting
953        self.task_picker_selected = 0;
954    }
955
956    /// Start a new timer with the current selected task
957    pub fn start_timer_for_selected(
958        &mut self,
959        storage: &crate::storage::StorageManager,
960    ) -> Result<(), String> {
961        if let Some(record) = self.get_selected_record() {
962            match storage.start_timer(
963                record.name.clone(),
964                Some(record.description.clone()),
965                Some(record.id),
966                Some(self.current_date),
967            ) {
968                Ok(timer) => {
969                    self.active_timer = Some(timer);
970                    Ok(())
971                }
972                Err(e) => Err(e.to_string()),
973            }
974        } else {
975            Err("No record selected".to_string())
976        }
977    }
978
979    /// Stop the active timer and convert to work record
980    pub fn stop_active_timer(
981        &mut self,
982        storage: &mut crate::storage::StorageManager,
983    ) -> Result<(), String> {
984        if self.active_timer.is_some() {
985            match storage.stop_timer() {
986                Ok(_work_record) => {
987                    self.active_timer = None;
988                    // Reload day data to reflect the new work record
989                    match storage.load_with_tracking(self.current_date) {
990                        Ok(new_day_data) => {
991                            self.day_data = new_day_data;
992                            self.selected_index = 0;
993                            Ok(())
994                        }
995                        Err(e) => Err(format!("Failed to reload day data: {}", e)),
996                    }
997                }
998                Err(e) => Err(e.to_string()),
999            }
1000        } else {
1001            Err("No active timer".to_string())
1002        }
1003    }
1004
1005    /// Pause the active timer
1006    pub fn pause_active_timer(
1007        &mut self,
1008        storage: &crate::storage::StorageManager,
1009    ) -> Result<(), String> {
1010        if self.active_timer.is_some() {
1011            match storage.pause_timer() {
1012                Ok(paused_timer) => {
1013                    self.active_timer = Some(paused_timer);
1014                    Ok(())
1015                }
1016                Err(e) => Err(e.to_string()),
1017            }
1018        } else {
1019            Err("No active timer".to_string())
1020        }
1021    }
1022
1023    /// Resume a paused timer
1024    pub fn resume_active_timer(
1025        &mut self,
1026        storage: &crate::storage::StorageManager,
1027    ) -> Result<(), String> {
1028        if self.active_timer.is_some() {
1029            match storage.resume_timer() {
1030                Ok(resumed_timer) => {
1031                    self.active_timer = Some(resumed_timer);
1032                    Ok(())
1033                }
1034                Err(e) => Err(e.to_string()),
1035            }
1036        } else {
1037            Err("No active timer".to_string())
1038        }
1039    }
1040
1041    /// Get current status of active timer or None if no timer running
1042    pub fn get_timer_status(&self) -> Option<&TimerState> {
1043        self.active_timer.as_ref()
1044    }
1045
1046    /// Check if the data file has been modified externally and reload if needed
1047    /// Returns true if the file was reloaded
1048    pub fn check_and_reload_if_modified(
1049        &mut self,
1050        storage: &mut crate::storage::StorageManager,
1051    ) -> bool {
1052        let mut changed = false;
1053
1054        // Check if day data file has been modified
1055        if let Ok(Some(new_data)) = storage.check_and_reload(self.current_date) {
1056            self.day_data = new_data;
1057            self.last_file_modified = storage.get_last_modified(&self.current_date);
1058
1059            // Adjust selected_index if it's now out of bounds
1060            let record_count = self.day_data.work_records.len();
1061            if self.selected_index >= record_count && record_count > 0 {
1062                self.selected_index = record_count - 1;
1063            }
1064
1065            changed = true;
1066        }
1067
1068        // Check if active timer has been modified externally (e.g., started/stopped from CLI)
1069        if let Ok(Some(timer)) = storage.load_active_timer() {
1070            // Timer exists - update if different from current state
1071            if self.active_timer.is_none() || self.active_timer.as_ref() != Some(&timer) {
1072                self.active_timer = Some(timer);
1073                changed = true;
1074            }
1075        } else if self.active_timer.is_some() {
1076            // Timer was cleared externally
1077            self.active_timer = None;
1078            changed = true;
1079        }
1080
1081        changed
1082    }
1083}
1084
1085fn days_in_month(month: time::Month, year: i32) -> u8 {
1086    use time::Month;
1087    match month {
1088        Month::January
1089        | Month::March
1090        | Month::May
1091        | Month::July
1092        | Month::August
1093        | Month::October
1094        | Month::December => 31,
1095        Month::April | Month::June | Month::September | Month::November => 30,
1096        Month::February => {
1097            if is_leap_year(year) {
1098                29
1099            } else {
1100                28
1101            }
1102        }
1103    }
1104}
1105
1106fn is_leap_year(year: i32) -> bool {
1107    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
1108}