tempo_cli/ui/
dashboard.rs

1use anyhow::Result;
2use chrono::Local;
3use crossterm::event::{self, Event, KeyCode, KeyEventKind};
4use log::debug;
5use ratatui::{
6    backend::Backend,
7    layout::{Alignment, Constraint, Direction, Layout, Rect},
8    style::{Color, Modifier, Style},
9    text::{Line, Span},
10    widgets::{Gauge, List, ListItem, Paragraph, Wrap},
11    Frame, Terminal,
12};
13use std::time::Duration;
14
15use crate::{
16    db::queries::ProjectQueries,
17    db::{get_database_path, Database},
18    models::{Project, Session},
19    ui::formatter::Formatter,
20    ui::widgets::{ColorScheme, Spinner},
21    utils::ipc::{get_socket_path, is_daemon_running, IpcClient, IpcMessage, IpcResponse},
22};
23
24pub struct Dashboard {
25    client: IpcClient,
26    show_project_switcher: bool,
27    available_projects: Vec<Project>,
28    selected_project_index: usize,
29    spinner: Spinner,
30}
31
32impl Dashboard {
33    pub async fn new() -> Result<Self> {
34        let socket_path = get_socket_path()?;
35        let client = if socket_path.exists() {
36            match IpcClient::connect(&socket_path).await {
37                Ok(client) => client,
38                Err(_) => IpcClient::new()?,
39            }
40        } else {
41            IpcClient::new()?
42        };
43
44        Ok(Self {
45            client,
46            show_project_switcher: false,
47            available_projects: Vec::new(),
48            selected_project_index: 0,
49            spinner: Spinner::new(),
50        })
51    }
52
53    pub async fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
54        let mut heartbeat_counter = 0;
55
56        loop {
57            // Send activity heartbeat every 30 iterations (3 seconds at 100ms intervals)
58            if heartbeat_counter >= 30 {
59                if let Err(e) = self.send_activity_heartbeat().await {
60                    // Ignore heartbeat errors to avoid interrupting the dashboard
61                    debug!("Heartbeat error: {}", e);
62                }
63                heartbeat_counter = 0;
64            }
65            heartbeat_counter += 1;
66
67            // Tick spinner animation
68            self.spinner.next();
69
70            // Get current status
71            let current_session = self.get_current_session().await?;
72            let current_project = if let Some(ref session) = current_session {
73                self.get_project_by_session(session).await?
74            } else {
75                None
76            };
77            let daily_stats = self.get_today_stats().await.unwrap_or((0, 0, 0));
78            let session_metrics = self.get_session_metrics().await.unwrap_or(None);
79
80            terminal.draw(|f| {
81                self.render_dashboard_sync(
82                    f,
83                    &current_session,
84                    &current_project,
85                    &daily_stats,
86                    &session_metrics,
87                );
88            })?;
89
90            // Handle input
91            if event::poll(Duration::from_millis(100))? {
92                match event::read()? {
93                    Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
94                        KeyCode::Char('q') | KeyCode::Esc => {
95                            if self.show_project_switcher {
96                                self.show_project_switcher = false;
97                            } else {
98                                break;
99                            }
100                        }
101                        KeyCode::Char('p') => {
102                            self.toggle_project_switcher().await?;
103                        }
104                        KeyCode::Up => {
105                            if self.show_project_switcher {
106                                self.navigate_projects(-1);
107                            }
108                        }
109                        KeyCode::Down => {
110                            if self.show_project_switcher {
111                                self.navigate_projects(1);
112                            }
113                        }
114                        KeyCode::Enter => {
115                            if self.show_project_switcher {
116                                self.switch_to_selected_project().await?;
117                            }
118                        }
119                        _ => {}
120                    },
121                    _ => {}
122                }
123            }
124        }
125
126        Ok(())
127    }
128
129    fn render_dashboard_sync(
130        &self,
131        f: &mut Frame,
132        current_session: &Option<Session>,
133        current_project: &Option<Project>,
134        daily_stats: &(i64, i64, i64),
135        session_metrics: &Option<crate::utils::ipc::SessionMetrics>,
136    ) {
137        let chunks = Layout::default()
138            .direction(Direction::Vertical)
139            .constraints([
140                Constraint::Length(3),  // Title
141                Constraint::Length(10), // Enhanced session info with progress
142                Constraint::Length(6),  // Project info
143                Constraint::Length(8),  // Real-time metrics with visuals
144                Constraint::Min(0),     // Statistics with charts
145                Constraint::Length(3),  // Help
146            ])
147            .split(f.size());
148
149        // Title
150        let spinner_char = self.spinner.current();
151        let title_text = format!(" {} Tempo - Time Tracking Dashboard ", spinner_char);
152        let title = Paragraph::new(title_text)
153            .style(
154                Style::default()
155                    .fg(ColorScheme::NEON_PINK)
156                    .add_modifier(Modifier::BOLD),
157            )
158            .alignment(Alignment::Center)
159            .block(ColorScheme::base_block());
160        f.render_widget(title, chunks[0]);
161
162        // Current session info
163        self.render_session_info(f, chunks[1], current_session);
164
165        // Project info
166        self.render_project_info(f, chunks[2], current_project);
167
168        // Real-time metrics
169        self.render_session_metrics(f, chunks[3], session_metrics);
170
171        // Statistics
172        self.render_statistics_sync(f, chunks[4], daily_stats);
173
174        // Help
175        self.render_help(f, chunks[5]);
176
177        // Project switcher overlay
178        if self.show_project_switcher {
179            self.render_project_switcher(f, f.size());
180        }
181    }
182
183    fn render_session_info(&self, f: &mut Frame, area: Rect, session: &Option<Session>) {
184        let block = ColorScheme::base_block().title(Span::styled(
185            " Current Session ",
186            Style::default().fg(ColorScheme::title()),
187        ));
188
189        if let Some(session) = session {
190            let now = Local::now();
191            let elapsed_seconds = (now.timestamp() - session.start_time.timestamp())
192                - session.paused_duration.num_seconds();
193
194            // Split the area for text and progress bar
195            let session_chunks = Layout::default()
196                .direction(Direction::Vertical)
197                .constraints([
198                    Constraint::Length(6), // Session info
199                    Constraint::Length(2), // Progress bar
200                ])
201                .split(area);
202
203            // Session information
204            let status_text = vec![
205                Line::from(vec![
206                    Span::raw("Status: "),
207                    Span::styled(
208                        "● ACTIVE",
209                        Style::default()
210                            .fg(ColorScheme::NEON_GREEN)
211                            .add_modifier(Modifier::BOLD),
212                    ),
213                ]),
214                Line::from(vec![
215                    Span::raw("Started: "),
216                    Span::styled(
217                        Formatter::format_timestamp(&session.start_time.with_timezone(&Local)),
218                        Style::default().fg(ColorScheme::WHITE_TEXT),
219                    ),
220                ]),
221                Line::from(vec![
222                    Span::raw("Elapsed: "),
223                    Span::styled(
224                        Formatter::format_duration(elapsed_seconds),
225                        Style::default()
226                            .fg(ColorScheme::NEON_CYAN)
227                            .add_modifier(Modifier::BOLD),
228                    ),
229                ]),
230                Line::from(vec![
231                    Span::raw("Context: "),
232                    Span::styled(
233                        session.context.to_string(),
234                        Style::default().fg(ColorScheme::NEON_YELLOW),
235                    ),
236                ]),
237            ];
238
239            let session_block = ColorScheme::base_block().title(Span::styled(
240                " Current Session ",
241                Style::default().fg(ColorScheme::title()),
242            ));
243
244            let paragraph = Paragraph::new(status_text)
245                .block(session_block)
246                .wrap(Wrap { trim: true });
247            f.render_widget(paragraph, session_chunks[0]);
248
249            // Visual progress bar for session duration
250            let progress_ratio = self.calculate_session_progress(elapsed_seconds);
251            let progress_bar = Gauge::default()
252                .block(ColorScheme::base_block().title(Span::styled(
253                    " Session Progress ",
254                    Style::default().fg(ColorScheme::title()),
255                )))
256                .gauge_style(
257                    Style::default()
258                        .fg(ColorScheme::NEON_GREEN)
259                        .bg(Color::Black),
260                )
261                .percent((progress_ratio * 100.0) as u16)
262                .label(format!(
263                    "{} / target: 2h",
264                    Formatter::format_duration(elapsed_seconds)
265                ));
266            f.render_widget(progress_bar, session_chunks[1]);
267        } else {
268            let no_session_text = vec![
269                Line::from(Span::styled(
270                    "No active session",
271                    Style::default().fg(ColorScheme::GRAY_TEXT),
272                )),
273                Line::from(Span::raw("")),
274                Line::from(Span::raw("Use 'tempo start' to begin tracking time")),
275                Line::from(Span::raw("")),
276                Line::from(Span::styled(
277                    "Set your focus and track your productivity",
278                    Style::default().fg(ColorScheme::NEON_CYAN),
279                )),
280            ];
281
282            let paragraph = Paragraph::new(no_session_text)
283                .block(block)
284                .wrap(Wrap { trim: true });
285            f.render_widget(paragraph, area);
286        }
287    }
288
289    fn render_project_info(&self, f: &mut Frame, area: Rect, project: &Option<Project>) {
290        let block = ColorScheme::base_block().title(Span::styled(
291            " Current Project ",
292            Style::default().fg(ColorScheme::title()),
293        ));
294
295        if let Some(project) = project {
296            let project_text = vec![
297                Line::from(vec![
298                    Span::raw("Name: "),
299                    Span::styled(
300                        &project.name,
301                        Style::default()
302                            .fg(ColorScheme::NEON_YELLOW)
303                            .add_modifier(Modifier::BOLD),
304                    ),
305                ]),
306                Line::from(vec![
307                    Span::raw("Path: "),
308                    Span::styled(
309                        project.path.to_string_lossy().to_string(),
310                        Style::default().fg(ColorScheme::GRAY_TEXT),
311                    ),
312                ]),
313            ];
314
315            let paragraph = Paragraph::new(project_text)
316                .block(block)
317                .wrap(Wrap { trim: true });
318            f.render_widget(paragraph, area);
319        } else {
320            let no_project_text = vec![Line::from(Span::styled(
321                "No active project",
322                Style::default().fg(ColorScheme::GRAY_TEXT),
323            ))];
324
325            let paragraph = Paragraph::new(no_project_text)
326                .block(block)
327                .wrap(Wrap { trim: true });
328            f.render_widget(paragraph, area);
329        }
330    }
331
332    fn render_statistics_sync(&self, f: &mut Frame, area: Rect, daily_stats: &(i64, i64, i64)) {
333        let (sessions_count, total_seconds, avg_seconds) = *daily_stats;
334
335        if sessions_count > 0 {
336            // Split area for text and visual chart
337            let stats_chunks = Layout::default()
338                .direction(Direction::Horizontal)
339                .constraints([
340                    Constraint::Percentage(50), // Stats text
341                    Constraint::Percentage(50), // Visual chart
342                ])
343                .split(area);
344
345            // Statistics text
346            let stats_text = vec![
347                Line::from(vec![
348                    Span::raw("Sessions: "),
349                    Span::styled(
350                        sessions_count.to_string(),
351                        Style::default()
352                            .fg(ColorScheme::NEON_PURPLE)
353                            .add_modifier(Modifier::BOLD),
354                    ),
355                ]),
356                Line::from(vec![
357                    Span::raw("Total time: "),
358                    Span::styled(
359                        Formatter::format_duration(total_seconds),
360                        Style::default().fg(ColorScheme::NEON_GREEN),
361                    ),
362                ]),
363                Line::from(vec![
364                    Span::raw("Avg session: "),
365                    Span::styled(
366                        Formatter::format_duration(avg_seconds),
367                        Style::default().fg(ColorScheme::NEON_CYAN),
368                    ),
369                ]),
370                Line::from(vec![
371                    Span::raw("Target: "),
372                    Span::styled(
373                        format!(
374                            "{:.0}% complete",
375                            (total_seconds as f64 / (8.0 * 3600.0)) * 100.0
376                        ),
377                        if total_seconds > 4 * 3600 {
378                            Style::default().fg(ColorScheme::NEON_GREEN)
379                        } else {
380                            Style::default().fg(ColorScheme::NEON_YELLOW)
381                        },
382                    ),
383                ]),
384            ];
385
386            let text_block = ColorScheme::base_block().title(Span::styled(
387                " Today's Summary ",
388                Style::default().fg(ColorScheme::title()),
389            ));
390
391            let paragraph = Paragraph::new(stats_text)
392                .block(text_block)
393                .wrap(Wrap { trim: true });
394            f.render_widget(paragraph, stats_chunks[0]);
395
396            // Visual progress bar for daily goal (8 hours)
397            let daily_goal_seconds = 8 * 3600; // 8 hours
398            let progress_percentage =
399                ((total_seconds as f64 / daily_goal_seconds as f64) * 100.0).min(100.0);
400
401            let goal_chunks = Layout::default()
402                .direction(Direction::Vertical)
403                .constraints([
404                    Constraint::Length(3), // Daily goal progress
405                    Constraint::Min(0),    // Activity sparkline (placeholder)
406                ])
407                .split(stats_chunks[1]);
408
409            let daily_progress = Gauge::default()
410                .block(ColorScheme::base_block().title(Span::styled(
411                    " Daily Goal (8h) ",
412                    Style::default().fg(ColorScheme::title()),
413                )))
414                .gauge_style(Style::default().fg(if progress_percentage >= 100.0 {
415                    ColorScheme::NEON_GREEN
416                } else if progress_percentage >= 50.0 {
417                    ColorScheme::NEON_YELLOW
418                } else {
419                    ColorScheme::NEON_PINK
420                }))
421                .percent(progress_percentage as u16)
422                .label(format!("{:.1}%", progress_percentage));
423            f.render_widget(daily_progress, goal_chunks[0]);
424
425            // Placeholder for activity sparkline or mini-chart
426            let activity_placeholder = Paragraph::new(vec![
427                Line::from(Span::styled(
428                    "Activity Timeline",
429                    Style::default().fg(ColorScheme::NEON_CYAN),
430                )),
431                Line::from(Span::raw(" ▂▃▅▇█▇▅▃▂  (simulated)")),
432            ])
433            .block(ColorScheme::base_block().title(Span::styled(
434                " Activity Pattern ",
435                Style::default().fg(ColorScheme::title()),
436            )))
437            .alignment(Alignment::Center);
438            f.render_widget(activity_placeholder, goal_chunks[1]);
439        } else {
440            let no_stats_text = vec![
441                Line::from(Span::styled(
442                    "No sessions today",
443                    Style::default().fg(ColorScheme::GRAY_TEXT),
444                )),
445                Line::from(Span::raw("")),
446                Line::from(Span::raw("Start your first session to see:")),
447                Line::from(Span::raw("  • Session count and timing")),
448                Line::from(Span::raw("  • Daily goal progress")),
449                Line::from(Span::raw("  • Activity patterns")),
450                Line::from(Span::raw("  • Productivity insights")),
451            ];
452
453            let block = ColorScheme::base_block().title(Span::styled(
454                " Today's Summary ",
455                Style::default().fg(ColorScheme::title()),
456            ));
457
458            let paragraph = Paragraph::new(no_stats_text)
459                .block(block)
460                .wrap(Wrap { trim: true });
461            f.render_widget(paragraph, area);
462        }
463    }
464
465    fn render_session_metrics(
466        &self,
467        f: &mut Frame,
468        area: Rect,
469        metrics: &Option<crate::utils::ipc::SessionMetrics>,
470    ) {
471        if let Some(metrics) = metrics {
472            // Split area for metrics text and visual indicators
473            let metrics_chunks = Layout::default()
474                .direction(Direction::Horizontal)
475                .constraints([
476                    Constraint::Percentage(60), // Metrics text
477                    Constraint::Percentage(40), // Visual indicators
478                ])
479                .split(area);
480
481            // Metrics text
482            let activity_color = match metrics.activity_score {
483                s if s > 0.7 => ColorScheme::NEON_GREEN,
484                s if s > 0.3 => ColorScheme::NEON_YELLOW,
485                _ => ColorScheme::NEON_PINK,
486            };
487
488            let activity_indicator = match metrics.activity_score {
489                s if s > 0.8 => "Very Active",
490                s if s > 0.6 => "Active",
491                s if s > 0.3 => "Moderate",
492                _ => "Low Activity",
493            };
494
495            let metrics_text = vec![
496                Line::from(vec![
497                    Span::raw("Activity: "),
498                    Span::styled(activity_indicator, Style::default().fg(activity_color)),
499                ]),
500                Line::from(vec![
501                    Span::raw("Score: "),
502                    Span::styled(
503                        format!("{:.1}%", metrics.activity_score * 100.0),
504                        Style::default().fg(activity_color),
505                    ),
506                ]),
507                Line::from(vec![
508                    Span::raw("Active: "),
509                    Span::styled(
510                        Formatter::format_duration(metrics.active_duration),
511                        Style::default().fg(ColorScheme::NEON_CYAN),
512                    ),
513                ]),
514                Line::from(vec![
515                    Span::raw("Paused: "),
516                    Span::styled(
517                        Formatter::format_duration(metrics.paused_duration),
518                        Style::default().fg(ColorScheme::GRAY_TEXT),
519                    ),
520                ]),
521                Line::from(vec![
522                    Span::raw("Efficiency: "),
523                    Span::styled(
524                        format!("{:.0}%", self.calculate_efficiency_percentage(metrics)),
525                        Style::default().fg(
526                            if self.calculate_efficiency_percentage(metrics) > 70.0 {
527                                ColorScheme::NEON_GREEN
528                            } else {
529                                ColorScheme::NEON_YELLOW
530                            },
531                        ),
532                    ),
533                ]),
534            ];
535
536            let text_block = ColorScheme::base_block().title(Span::styled(
537                " Real-time Metrics ",
538                Style::default().fg(ColorScheme::title()),
539            ));
540
541            let paragraph = Paragraph::new(metrics_text)
542                .block(text_block)
543                .wrap(Wrap { trim: true });
544            f.render_widget(paragraph, metrics_chunks[0]);
545
546            // Visual activity indicator
547            let activity_chunks = Layout::default()
548                .direction(Direction::Vertical)
549                .constraints([
550                    Constraint::Length(3), // Activity gauge
551                    Constraint::Length(3), // Efficiency gauge
552                ])
553                .split(metrics_chunks[1]);
554
555            // Activity level gauge
556            let activity_gauge = Gauge::default()
557                .block(ColorScheme::base_block().title(Span::styled(
558                    " Activity ",
559                    Style::default().fg(ColorScheme::title()),
560                )))
561                .gauge_style(Style::default().fg(activity_color))
562                .percent((metrics.activity_score * 100.0) as u16)
563                .label(format!("{:.0}%", metrics.activity_score * 100.0));
564            f.render_widget(activity_gauge, activity_chunks[0]);
565
566            // Efficiency gauge
567            let efficiency = self.calculate_efficiency_percentage(metrics);
568            let efficiency_color = if efficiency > 80.0 {
569                ColorScheme::NEON_GREEN
570            } else if efficiency > 60.0 {
571                ColorScheme::NEON_YELLOW
572            } else {
573                ColorScheme::NEON_PINK
574            };
575
576            let efficiency_gauge = Gauge::default()
577                .block(ColorScheme::base_block().title(Span::styled(
578                    " Efficiency ",
579                    Style::default().fg(ColorScheme::title()),
580                )))
581                .gauge_style(Style::default().fg(efficiency_color))
582                .percent(efficiency as u16)
583                .label(format!("{:.0}%", efficiency));
584            f.render_widget(efficiency_gauge, activity_chunks[1]);
585        } else {
586            let no_metrics_block = ColorScheme::base_block().title(Span::styled(
587                " Real-time Metrics ",
588                Style::default().fg(ColorScheme::title()),
589            ));
590
591            let no_metrics_text = vec![
592                Line::from(Span::styled(
593                    "No active session",
594                    Style::default().fg(ColorScheme::GRAY_TEXT),
595                )),
596                Line::from(Span::raw("")),
597                Line::from(Span::raw("Start tracking to see:")),
598                Line::from(Span::raw("• Activity indicators")),
599                Line::from(Span::raw("• Efficiency metrics")),
600                Line::from(Span::raw("• Visual progress")),
601            ];
602
603            let paragraph = Paragraph::new(no_metrics_text)
604                .block(no_metrics_block)
605                .wrap(Wrap { trim: true });
606            f.render_widget(paragraph, area);
607        }
608    }
609
610    fn render_help(&self, f: &mut Frame, area: Rect) {
611        let help_text = if self.show_project_switcher {
612            "Project Switcher: ↑/↓ Navigate | Enter - Select | P/Esc - Close"
613        } else {
614            "Press 'q' or 'Esc' to quit | 'p' for project switcher | Updates every 100ms"
615        };
616
617        let help_paragraph = Paragraph::new(help_text)
618            .style(Style::default().fg(ColorScheme::GRAY_TEXT))
619            .alignment(Alignment::Center)
620            .block(ColorScheme::base_block());
621        f.render_widget(help_paragraph, area);
622    }
623
624    fn render_project_switcher(&self, f: &mut Frame, area: Rect) {
625        // Create a centered popup
626        let popup_area = self.centered_rect(60, 70, area);
627
628        // Clear the background
629        let background = ColorScheme::base_block()
630            .title(Span::styled(
631                " Project Switcher ",
632                Style::default().fg(ColorScheme::title()),
633            ))
634            .title_alignment(Alignment::Center);
635        f.render_widget(background, popup_area);
636
637        // Create the project list
638        let projects_area = Layout::default()
639            .direction(Direction::Vertical)
640            .margin(1)
641            .split(popup_area)[0];
642
643        if self.available_projects.is_empty() {
644            let no_projects = Paragraph::new(
645                "No projects found\n\nCreate a project first using:\ntempo init <project-name>",
646            )
647            .style(Style::default().fg(ColorScheme::NEON_YELLOW))
648            .alignment(Alignment::Center)
649            .wrap(Wrap { trim: true });
650            f.render_widget(no_projects, projects_area);
651        } else {
652            let project_items: Vec<ListItem> = self
653                .available_projects
654                .iter()
655                .enumerate()
656                .map(|(i, project)| {
657                    let style = if i == self.selected_project_index {
658                        Style::default()
659                            .fg(Color::Black)
660                            .bg(ColorScheme::NEON_CYAN)
661                            .add_modifier(Modifier::BOLD)
662                    } else {
663                        Style::default().fg(ColorScheme::WHITE_TEXT)
664                    };
665
666                    let content = vec![
667                        Line::from(vec![Span::styled(format!("{}", project.name), style)]),
668                        Line::from(vec![Span::styled(
669                            format!("  [P] {}", project.path.to_string_lossy()),
670                            Style::default().fg(if i == self.selected_project_index {
671                                Color::Black
672                            } else {
673                                ColorScheme::GRAY_TEXT
674                            }),
675                        )]),
676                    ];
677
678                    ListItem::new(content).style(style)
679                })
680                .collect();
681
682            let projects_list =
683                List::new(project_items).style(Style::default().fg(ColorScheme::WHITE_TEXT));
684            f.render_widget(projects_list, projects_area);
685        }
686    }
687
688    fn centered_rect(&self, percent_x: u16, percent_y: u16, r: Rect) -> Rect {
689        let popup_layout = Layout::default()
690            .direction(Direction::Vertical)
691            .constraints([
692                Constraint::Percentage((100 - percent_y) / 2),
693                Constraint::Percentage(percent_y),
694                Constraint::Percentage((100 - percent_y) / 2),
695            ])
696            .split(r);
697
698        Layout::default()
699            .direction(Direction::Horizontal)
700            .constraints([
701                Constraint::Percentage((100 - percent_x) / 2),
702                Constraint::Percentage(percent_x),
703                Constraint::Percentage((100 - percent_x) / 2),
704            ])
705            .split(popup_layout[1])[1]
706    }
707
708    async fn get_current_session(&mut self) -> Result<Option<Session>> {
709        if !is_daemon_running() {
710            return Ok(None);
711        }
712
713        let response = self
714            .client
715            .send_message(&IpcMessage::GetActiveSession)
716            .await?;
717        match response {
718            IpcResponse::ActiveSession(session) => Ok(session),
719            IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get active session: {}", e)),
720            _ => Ok(None),
721        }
722    }
723
724    async fn get_project_by_session(&mut self, session: &Session) -> Result<Option<Project>> {
725        if !is_daemon_running() {
726            return Ok(None);
727        }
728
729        let response = self
730            .client
731            .send_message(&IpcMessage::GetProject(session.project_id))
732            .await?;
733        match response {
734            IpcResponse::Project(project) => Ok(project),
735            IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get project: {}", e)),
736            _ => Ok(None),
737        }
738    }
739
740    async fn get_today_stats(&mut self) -> Result<(i64, i64, i64)> {
741        // (sessions_count, total_seconds, avg_seconds)
742        if !is_daemon_running() {
743            return Ok((0, 0, 0));
744        }
745
746        let today = chrono::Local::now().date_naive();
747        let response = self
748            .client
749            .send_message(&IpcMessage::GetDailyStats(today))
750            .await?;
751        match response {
752            IpcResponse::DailyStats {
753                sessions_count,
754                total_seconds,
755                avg_seconds,
756            } => Ok((sessions_count, total_seconds, avg_seconds)),
757            IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get daily stats: {}", e)),
758            _ => Ok((0, 0, 0)),
759        }
760    }
761
762    async fn get_session_metrics(&mut self) -> Result<Option<crate::utils::ipc::SessionMetrics>> {
763        if !is_daemon_running() {
764            return Ok(None);
765        }
766
767        let response = self
768            .client
769            .send_message(&IpcMessage::GetSessionMetrics(0))
770            .await?;
771        match response {
772            IpcResponse::SessionMetrics(metrics) => Ok(Some(metrics)),
773            IpcResponse::Error(_) => Ok(None), // No active session
774            _ => Ok(None),
775        }
776    }
777
778    async fn send_activity_heartbeat(&mut self) -> Result<()> {
779        if !is_daemon_running() {
780            return Ok(());
781        }
782
783        let _response = self
784            .client
785            .send_message(&IpcMessage::ActivityHeartbeat)
786            .await?;
787        Ok(())
788    }
789
790    fn calculate_session_progress(&self, elapsed_seconds: i64) -> f64 {
791        // Progress towards 2-hour session target
792        let target_seconds = 2 * 3600; // 2 hours
793        (elapsed_seconds as f64 / target_seconds as f64).min(1.0)
794    }
795
796    fn calculate_efficiency_percentage(&self, metrics: &crate::utils::ipc::SessionMetrics) -> f64 {
797        if metrics.total_duration == 0 {
798            return 0.0;
799        }
800
801        let efficiency = (metrics.active_duration as f64 / metrics.total_duration as f64) * 100.0;
802        efficiency.min(100.0)
803    }
804
805    async fn toggle_project_switcher(&mut self) -> Result<()> {
806        if self.show_project_switcher {
807            self.show_project_switcher = false;
808        } else {
809            // Load available projects
810            self.available_projects = self.load_projects().await?;
811            self.selected_project_index = 0;
812            self.show_project_switcher = true;
813        }
814        Ok(())
815    }
816
817    fn navigate_projects(&mut self, direction: i32) {
818        if !self.available_projects.is_empty() {
819            let current = self.selected_project_index as i32;
820            let new_index = (current + direction)
821                .max(0)
822                .min(self.available_projects.len() as i32 - 1);
823            self.selected_project_index = new_index as usize;
824        }
825    }
826
827    async fn switch_to_selected_project(&mut self) -> Result<()> {
828        if let Some(selected_project) = self.available_projects.get(self.selected_project_index) {
829            // Switch to the selected project
830            let project_id = selected_project.id.unwrap_or(0);
831            let response = self
832                .client
833                .send_message(&IpcMessage::SwitchProject(project_id))
834                .await?;
835            match response {
836                IpcResponse::Success => {
837                    self.show_project_switcher = false;
838                }
839                IpcResponse::Error(e) => {
840                    return Err(anyhow::anyhow!("Failed to switch project: {}", e))
841                }
842                _ => return Err(anyhow::anyhow!("Unexpected response")),
843            }
844        }
845        Ok(())
846    }
847
848    async fn load_projects(&mut self) -> Result<Vec<Project>> {
849        let db_path = get_database_path()?;
850        let db = Database::new(&db_path)?;
851
852        let projects = ProjectQueries::list_all(&db.connection, false)?; // Don't include archived
853        Ok(projects)
854    }
855}