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