1use crate::chart::{engine::ChartEngine, renderers::LineRenderer, types::*};
2use anyhow::Result;
3use crossterm::{
4 event::{self, Event, KeyCode, KeyEventKind},
5 execute,
6 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
7};
8use ratatui::{
9 backend::CrosstermBackend,
10 layout::{Constraint, Direction, Layout, Margin},
11 style::{Color, Style},
12 text::{Line, Span},
13 widgets::{Block, Borders, Paragraph},
14 Terminal,
15};
16use std::io;
17
18pub struct ChartTui {
19 config: ChartConfig,
20 data: Option<DataSeries>,
21 renderer: Option<LineRenderer>,
22 should_exit: bool,
23}
24
25impl ChartTui {
26 pub fn new(config: ChartConfig) -> Self {
27 Self {
28 config,
29 data: None,
30 renderer: None,
31 should_exit: false,
32 }
33 }
34
35 pub fn run(&mut self, mut chart_engine: ChartEngine) -> Result<()> {
36 enable_raw_mode()?;
38 let mut stdout = io::stdout();
39 execute!(stdout, EnterAlternateScreen)?;
40 let backend = CrosstermBackend::new(stdout);
41 let mut terminal = Terminal::new(backend)?;
42
43 let data = chart_engine.execute_chart_query(&self.config)?;
45 let viewport = ChartViewport::new(data.x_range, data.y_range);
46 let mut renderer = LineRenderer::new(viewport);
47 renderer.viewport_mut().auto_scale(&data);
48
49 self.data = Some(data);
50 self.renderer = Some(renderer);
51
52 let result = self.run_event_loop(&mut terminal);
54
55 disable_raw_mode()?;
57 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
58 terminal.show_cursor()?;
59
60 result
61 }
62
63 fn run_event_loop(
64 &mut self,
65 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
66 ) -> Result<()> {
67 while !self.should_exit {
68 terminal.draw(|frame| self.render_chart(frame))?;
69
70 if event::poll(std::time::Duration::from_millis(100))? {
71 if let Event::Key(key) = event::read()? {
72 if key.kind == KeyEventKind::Press {
73 self.handle_key_event(key.code);
74 }
75 }
76 }
77 }
78
79 Ok(())
80 }
81
82 fn render_chart(&mut self, frame: &mut ratatui::Frame) {
83 let area = frame.area();
84
85 let chunks = Layout::default()
87 .direction(Direction::Vertical)
88 .constraints([
89 Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ])
93 .split(area);
94
95 self.render_title(frame, chunks[0]);
97
98 if let (Some(data), Some(renderer)) = (&self.data, &self.renderer) {
100 let chart_area = chunks[1].inner(Margin {
101 horizontal: 1,
102 vertical: 1,
103 });
104 renderer.render(frame, chart_area, data, &self.config);
105 }
106
107 self.render_controls(frame, chunks[2]);
109 }
110
111 fn render_title(&self, frame: &mut ratatui::Frame, area: ratatui::layout::Rect) {
112 let title = Paragraph::new(Line::from(vec![Span::styled(
113 &self.config.title,
114 Style::default().fg(Color::Yellow),
115 )]))
116 .block(Block::default().borders(Borders::ALL));
117
118 frame.render_widget(title, area);
119 }
120
121 fn render_controls(&self, frame: &mut ratatui::Frame, area: ratatui::layout::Rect) {
122 let controls = vec![
123 Span::raw("Controls: "),
124 Span::styled("hjkl", Style::default().fg(Color::Cyan)),
125 Span::raw(" pan • "),
126 Span::styled("+/-", Style::default().fg(Color::Cyan)),
127 Span::raw(" zoom • "),
128 Span::styled("r", Style::default().fg(Color::Cyan)),
129 Span::raw(" reset • "),
130 Span::styled("q", Style::default().fg(Color::Cyan)),
131 Span::raw(" quit"),
132 ];
133
134 let paragraph =
135 Paragraph::new(Line::from(controls)).block(Block::default().borders(Borders::ALL));
136
137 frame.render_widget(paragraph, area);
138 }
139
140 fn handle_key_event(&mut self, key_code: KeyCode) {
141 if let Some(renderer) = &mut self.renderer {
142 match key_code {
143 KeyCode::Char('q') | KeyCode::Esc => {
145 self.should_exit = true;
146 }
147
148 KeyCode::Char('h') => {
150 renderer.viewport_mut().pan(-1.0, 0.0);
151 }
152 KeyCode::Char('l') => {
153 renderer.viewport_mut().pan(1.0, 0.0);
154 }
155 KeyCode::Char('k') => {
156 renderer.viewport_mut().pan(0.0, 1.0);
157 }
158 KeyCode::Char('j') => {
159 renderer.viewport_mut().pan(0.0, -1.0);
160 }
161
162 KeyCode::Char('+') | KeyCode::Char('=') => {
164 let viewport = renderer.viewport();
165 let center_x = (viewport.x_min + viewport.x_max) / 2.0;
166 let center_y = (viewport.y_min + viewport.y_max) / 2.0;
167 renderer.viewport_mut().zoom(1.2, center_x, center_y);
168 }
169 KeyCode::Char('-') => {
170 let viewport = renderer.viewport();
171 let center_x = (viewport.x_min + viewport.x_max) / 2.0;
172 let center_y = (viewport.y_min + viewport.y_max) / 2.0;
173 renderer.viewport_mut().zoom(0.8, center_x, center_y);
174 }
175
176 KeyCode::Char('r') => {
178 if let Some(data) = &self.data {
179 renderer.viewport_mut().auto_scale(data);
180 }
181 }
182
183 _ => {}
184 }
185 }
186 }
187}