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 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")
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 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 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}