1use anyhow::Result;
2use crossterm::event::{self, Event, KeyCode};
3use ratatui::{
4 backend::Backend,
5 layout::{Constraint, Direction, Layout, Rect},
6 style::{Color, Modifier, Style},
7 text::{Line, Span},
8 widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
9 Frame, Terminal,
10};
11use std::time::Duration;
12
13use crate::{
14 models::{Project, Session},
15 ui::{formatter::Formatter, should_quit},
16 utils::ipc::IpcClient,
17};
18
19pub struct InteractiveViewer {
20 client: IpcClient,
21 projects: Vec<Project>,
22 sessions: Vec<Session>,
23 selected_project: Option<usize>,
24 project_list_state: ListState,
25}
26
27impl InteractiveViewer {
28 pub fn new() -> Result<Self> {
29 let client = IpcClient::new()?;
30 let mut viewer = Self {
31 client,
32 projects: Vec::new(),
33 sessions: Vec::new(),
34 selected_project: None,
35 project_list_state: ListState::default(),
36 };
37
38 viewer.load_data()?;
39 Ok(viewer)
40 }
41
42 pub fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
43 loop {
44 terminal.draw(|f| {
45 self.render(f);
46 })?;
47
48 if event::poll(Duration::from_millis(100))? {
50 if let Event::Key(key) = event::read()? {
51 match key.code {
52 KeyCode::Char('q') | KeyCode::Esc => break,
53 KeyCode::Up => self.previous_project(),
54 KeyCode::Down => self.next_project(),
55 KeyCode::Enter => self.select_project(),
56 KeyCode::Char('r') => self.load_data()?,
57 _ => {}
58 }
59 }
60 }
61 }
62
63 Ok(())
64 }
65
66 fn render(&mut self, f: &mut Frame) {
67 let chunks = Layout::default()
68 .direction(Direction::Horizontal)
69 .constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
70 .split(f.size());
71
72 self.render_project_list(f, chunks[0]);
73 self.render_session_details(f, chunks[1]);
74 }
75
76 fn render_project_list(&mut self, f: &mut Frame, area: Rect) {
77 let items: Vec<ListItem> = self
78 .projects
79 .iter()
80 .enumerate()
81 .map(|(i, project)| {
82 let style = if Some(i) == self.selected_project {
83 Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
84 } else {
85 Style::default().fg(Color::White)
86 };
87
88 ListItem::new(Line::from(Span::styled(project.name.clone(), style)))
89 })
90 .collect();
91
92 let list = List::new(items)
93 .block(
94 Block::default()
95 .title("Projects")
96 .borders(Borders::ALL)
97 .style(Style::default().fg(Color::Cyan)),
98 )
99 .highlight_style(Style::default().bg(Color::DarkGray));
100
101 f.render_stateful_widget(list, area, &mut self.project_list_state);
102 }
103
104 fn render_session_details(&self, f: &mut Frame, area: Rect) {
105 let chunks = Layout::default()
106 .direction(Direction::Vertical)
107 .constraints([Constraint::Length(8), Constraint::Min(0)])
108 .split(area);
109
110 if let Some(selected_idx) = self.selected_project {
112 if let Some(project) = self.projects.get(selected_idx) {
113 let project_info = Formatter::format_project_info(project);
114 let paragraph = Paragraph::new(project_info)
115 .block(Formatter::create_header_block("Project Details"));
116 f.render_widget(paragraph, chunks[0]);
117
118 let project_sessions: Vec<&Session> = self
120 .sessions
121 .iter()
122 .filter(|s| s.project_id == project.id.unwrap_or(-1))
123 .collect();
124
125 if !project_sessions.is_empty() {
126 let sessions_summary = Formatter::format_sessions_summary(&project_sessions.into_iter().cloned().collect::<Vec<_>>());
127 let sessions_widget = Paragraph::new(sessions_summary)
128 .block(Formatter::create_info_block());
129 f.render_widget(sessions_widget, chunks[1]);
130 } else {
131 let no_sessions = Paragraph::new("No sessions found for this project")
132 .style(Style::default().fg(Color::Gray))
133 .block(Formatter::create_info_block());
134 f.render_widget(no_sessions, chunks[1]);
135 }
136 }
137 } else {
138 let help_text = vec![
139 Line::from("Select a project to view details"),
140 Line::from(""),
141 Line::from("Controls:"),
142 Line::from(" ↑/↓ Navigate projects"),
143 Line::from(" Enter Select project"),
144 Line::from(" r Refresh data"),
145 Line::from(" q/Esc Quit"),
146 ];
147
148 let paragraph = Paragraph::new(help_text)
149 .block(
150 Block::default()
151 .title("Help")
152 .borders(Borders::ALL)
153 .style(Style::default().fg(Color::Cyan)),
154 );
155 f.render_widget(paragraph, area);
156 }
157 }
158
159 fn previous_project(&mut self) {
160 if self.projects.is_empty() {
161 return;
162 }
163
164 let i = match self.project_list_state.selected() {
165 Some(i) => {
166 if i == 0 {
167 self.projects.len() - 1
168 } else {
169 i - 1
170 }
171 }
172 None => 0,
173 };
174 self.project_list_state.select(Some(i));
175 self.selected_project = Some(i);
176 }
177
178 fn next_project(&mut self) {
179 if self.projects.is_empty() {
180 return;
181 }
182
183 let i = match self.project_list_state.selected() {
184 Some(i) => {
185 if i >= self.projects.len() - 1 {
186 0
187 } else {
188 i + 1
189 }
190 }
191 None => 0,
192 };
193 self.project_list_state.select(Some(i));
194 self.selected_project = Some(i);
195 }
196
197 fn select_project(&mut self) {
198 if let Some(i) = self.project_list_state.selected() {
199 self.selected_project = Some(i);
200 }
201 }
202
203 fn load_data(&mut self) -> Result<()> {
204 use chrono::Utc;
207 use std::path::PathBuf;
208
209 self.projects = vec![
210 Project {
211 id: Some(1),
212 name: "Sample Project".to_string(),
213 path: PathBuf::from("/Users/example/sample"),
214 git_hash: Some("abc123".to_string()),
215 created_at: chrono::Local::now().with_timezone(&Utc),
216 updated_at: chrono::Local::now().with_timezone(&Utc),
217 is_archived: false,
218 description: Some("A sample project for demo".to_string()),
219 },
220 ];
221
222 use crate::models::session::SessionContext;
223
224 self.sessions = vec![
225 Session {
226 id: Some(1),
227 project_id: 1,
228 start_time: (chrono::Local::now() - chrono::Duration::hours(2)).with_timezone(&Utc),
229 end_time: Some((chrono::Local::now() - chrono::Duration::hours(1)).with_timezone(&Utc)),
230 context: SessionContext::Terminal,
231 paused_duration: chrono::Duration::minutes(5),
232 notes: Some("Working on initial setup".to_string()),
233 created_at: chrono::Local::now().with_timezone(&Utc),
234 },
235 ];
236
237 Ok(())
238 }
239}