tempo_cli/ui/
interactive.rs

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            // Handle input
49            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        // Project info
111        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                // Sessions for this project
119                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        // This would use IPC to load actual data
205        // For now, create placeholder data
206        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}