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 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 }
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), Constraint::Min(0), Constraint::Length(3), ])
93 .split(f.size());
94
95 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 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 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}