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