tempo_cli/ui/
history.rs

1use anyhow::Result;
2use chrono::{Local, Utc};
3use crossterm::event::{self, Event, KeyCode, KeyEventKind};
4use ratatui::{
5    backend::Backend,
6    layout::{Alignment, Constraint, Direction, Layout},
7    style::{Color, Modifier, Style},
8    text::{Line, Span},
9    widgets::{Block, Borders, List, ListItem, Paragraph},
10    Frame, Terminal,
11};
12use std::time::Duration;
13
14use crate::{
15    db::queries::SessionQueries,
16    db::{get_database_path, Database},
17    models::Session,
18    ui::formatter::Formatter,
19};
20
21pub struct SessionHistoryBrowser {
22    sessions: Vec<Session>,
23    selected_index: usize,
24    filter_project: Option<String>,
25    filter_date_from: Option<chrono::NaiveDate>,
26    filter_date_to: Option<chrono::NaiveDate>,
27    show_filters: bool,
28}
29
30impl SessionHistoryBrowser {
31    pub async fn new() -> Result<Self> {
32        let db_path = get_database_path()?;
33        let db = Database::new(&db_path)?;
34
35        // Load recent sessions (last 100)
36        let sessions =
37            SessionQueries::list_with_filter(&db.connection, None, None, None, Some(100))?;
38
39        Ok(Self {
40            sessions,
41            selected_index: 0,
42            filter_project: None,
43            filter_date_from: None,
44            filter_date_to: None,
45            show_filters: false,
46        })
47    }
48
49    pub async fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
50        loop {
51            terminal.draw(|f| self.render(f))?;
52
53            if event::poll(Duration::from_millis(100))? {
54                match event::read()? {
55                    Event::Key(key) if key.kind == KeyEventKind::Press => {
56                        match key.code {
57                            KeyCode::Char('q') | KeyCode::Esc => break,
58                            KeyCode::Up => {
59                                if self.selected_index > 0 {
60                                    self.selected_index -= 1;
61                                }
62                            }
63                            KeyCode::Down => {
64                                if self.selected_index < self.sessions.len().saturating_sub(1) {
65                                    self.selected_index += 1;
66                                }
67                            }
68                            KeyCode::Char('f') => {
69                                self.show_filters = !self.show_filters;
70                            }
71                            KeyCode::Enter => {
72                                // Could show session details here
73                            }
74                            _ => {}
75                        }
76                    }
77                    _ => {}
78                }
79            }
80        }
81
82        Ok(())
83    }
84
85    fn render(&self, f: &mut Frame) {
86        let chunks = Layout::default()
87            .direction(Direction::Vertical)
88            .constraints([
89                Constraint::Length(3), // Title
90                Constraint::Min(0),    // Session list
91                Constraint::Length(3), // Help
92            ])
93            .split(f.size());
94
95        // Title
96        let title = Paragraph::new("Session History Browser")
97            .style(
98                Style::default()
99                    .fg(Color::Cyan)
100                    .add_modifier(Modifier::BOLD),
101            )
102            .alignment(Alignment::Center)
103            .block(Block::default().borders(Borders::ALL));
104        f.render_widget(title, chunks[0]);
105
106        // Session list
107        if self.sessions.is_empty() {
108            let no_sessions = Paragraph::new("No sessions found")
109                .style(Style::default().fg(Color::Yellow))
110                .alignment(Alignment::Center)
111                .block(Block::default().borders(Borders::ALL).title("Sessions"));
112            f.render_widget(no_sessions, chunks[1]);
113        } else {
114            let session_items: Vec<ListItem> = self
115                .sessions
116                .iter()
117                .enumerate()
118                .map(|(i, session)| {
119                    let style = if i == self.selected_index {
120                        Style::default()
121                            .fg(Color::Black)
122                            .bg(Color::Cyan)
123                            .add_modifier(Modifier::BOLD)
124                    } else {
125                        Style::default().fg(Color::White)
126                    };
127
128                    let duration = if let Some(end) = session.end_time {
129                        (end - session.start_time).num_seconds()
130                            - session.paused_duration.num_seconds()
131                    } else {
132                        (Utc::now() - session.start_time).num_seconds()
133                            - session.paused_duration.num_seconds()
134                    };
135
136                    let context_color = match session.context.to_string().as_str() {
137                        "terminal" => Color::Cyan,
138                        "ide" => Color::Magenta,
139                        "linked" => Color::Yellow,
140                        "manual" => Color::Blue,
141                        _ => Color::White,
142                    };
143
144                    let start_time = session.start_time.with_timezone(&Local);
145
146                    let content = vec![
147                        Line::from(vec![
148                            Span::styled(format!("Session #{}", session.id.unwrap_or(0)), style),
149                            Span::raw("  "),
150                            Span::styled(
151                                Formatter::format_duration(duration),
152                                Style::default().fg(Color::Green),
153                            ),
154                        ]),
155                        Line::from(vec![
156                            Span::styled(
157                                format!("{}", start_time.format("%Y-%m-%d %H:%M:%S")),
158                                Style::default().fg(Color::Gray),
159                            ),
160                            Span::raw("  "),
161                            Span::styled(
162                                session.context.to_string(),
163                                Style::default().fg(context_color),
164                            ),
165                        ]),
166                    ];
167
168                    ListItem::new(content).style(style)
169                })
170                .collect();
171
172            let sessions_list = List::new(session_items)
173                .block(Block::default().borders(Borders::ALL).title("Sessions"))
174                .style(Style::default().fg(Color::White));
175            f.render_widget(sessions_list, chunks[1]);
176        }
177
178        // Help
179        let help_text = if self.show_filters {
180            "Filters: [f] Toggle | [q/Esc] Quit"
181        } else {
182            "[Up/Down] Navigate | [Enter] View Details | [f] Filters | [q/Esc] Quit"
183        };
184
185        let help = Paragraph::new(help_text)
186            .style(Style::default().fg(Color::Gray))
187            .alignment(Alignment::Center)
188            .block(Block::default().borders(Borders::ALL));
189        f.render_widget(help, chunks[2]);
190    }
191}