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::{Modifier, Style},
8    text::{Line, Span},
9    widgets::{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, widgets::ColorScheme},
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        // Title
97        let title = Paragraph::new("Session History Browser")
98            .style(
99                Style::default()
100                    .fg(ColorScheme::CLEAN_ACCENT)
101                    .add_modifier(Modifier::BOLD),
102            )
103            .alignment(Alignment::Center)
104            .block(ColorScheme::clean_block());
105        f.render_widget(title, chunks[0]);
106
107        // Session list
108        // Session list
109        if self.sessions.is_empty() {
110            let no_sessions = Paragraph::new("No sessions found")
111                .style(Style::default().fg(ColorScheme::CLEAN_GOLD))
112                .alignment(Alignment::Center)
113                .block(ColorScheme::clean_block().title("Sessions"));
114            f.render_widget(no_sessions, chunks[1]);
115        } else {
116            let session_items: Vec<ListItem> = self
117                .sessions
118                .iter()
119                .enumerate()
120                .map(|(i, session)| {
121                    let style = if i == self.selected_index {
122                        Style::default()
123                            .fg(ColorScheme::CLEAN_BG)
124                            .bg(ColorScheme::CLEAN_ACCENT)
125                            .add_modifier(Modifier::BOLD)
126                    } else {
127                        Style::default().fg(ColorScheme::WHITE_TEXT)
128                    };
129
130                    let duration = if let Some(end) = session.end_time {
131                        (end - session.start_time).num_seconds()
132                            - session.paused_duration.num_seconds()
133                    } else {
134                        (Utc::now() - session.start_time).num_seconds()
135                            - session.paused_duration.num_seconds()
136                    };
137
138                    let context_color = if i == self.selected_index {
139                        ColorScheme::CLEAN_BG
140                    } else {
141                        match session.context.to_string().as_str() {
142                            "terminal" => ColorScheme::CLEAN_ACCENT,
143                            "ide" => ColorScheme::CLEAN_BLUE,
144                            "linked" => ColorScheme::CLEAN_GOLD,
145                            "manual" => ColorScheme::CLEAN_GREEN,
146                            _ => ColorScheme::WHITE_TEXT,
147                        }
148                    };
149
150                    let start_time = session.start_time.with_timezone(&Local);
151                    let duration_color = if i == self.selected_index {
152                        ColorScheme::CLEAN_BG
153                    } else {
154                        ColorScheme::CLEAN_GREEN
155                    };
156                    let date_color = if i == self.selected_index {
157                        ColorScheme::CLEAN_BG
158                    } else {
159                        ColorScheme::GRAY_TEXT
160                    };
161
162                    let content = vec![
163                        Line::from(vec![
164                            Span::styled(format!("Session #{}", session.id.unwrap_or(0)), style),
165                            Span::raw("  "),
166                            Span::styled(
167                                Formatter::format_duration(duration),
168                                Style::default().fg(duration_color),
169                            ),
170                        ]),
171                        Line::from(vec![
172                            Span::styled(
173                                format!("{}", start_time.format("%Y-%m-%d %H:%M:%S")),
174                                Style::default().fg(date_color),
175                            ),
176                            Span::raw("  "),
177                            Span::styled(
178                                session.context.to_string(),
179                                Style::default().fg(context_color),
180                            ),
181                        ]),
182                    ];
183
184                    ListItem::new(content).style(style)
185                })
186                .collect();
187
188            let sessions_list = List::new(session_items)
189                .block(ColorScheme::clean_block().title("Sessions"))
190                .style(Style::default().fg(ColorScheme::WHITE_TEXT));
191            f.render_widget(sessions_list, chunks[1]);
192        }
193
194        // Help
195        let help_text = if self.show_filters {
196            "Filters: [f] Toggle | [q/Esc] Quit"
197        } else {
198            "[Up/Down] Navigate | [Enter] View Details | [f] Filters | [q/Esc] Quit"
199        };
200
201        let help = Paragraph::new(help_text)
202            .style(Style::default().fg(ColorScheme::GRAY_TEXT))
203            .alignment(Alignment::Center)
204            .block(ColorScheme::clean_block());
205        f.render_widget(help, chunks[2]);
206    }
207}