1use std::time::Duration;
2
3use color_eyre::{eyre::Context, Result};
4use crossterm::event;
5use itertools::Itertools;
6use ratatui::{
7 buffer::Buffer,
8 crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
9 layout::{Constraint, Layout, Rect},
10 style::Color,
11 text::{Line, Span},
12 widgets::{Block, Tabs, Widget},
13 DefaultTerminal, Frame,
14};
15use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
16
17use crate::{
18 destroy,
19 tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
20 THEME,
21};
22
23#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
24pub struct App {
25 mode: Mode,
26 tab: Tab,
27 about_tab: AboutTab,
28 recipe_tab: RecipeTab,
29 email_tab: EmailTab,
30 traceroute_tab: TracerouteTab,
31 weather_tab: WeatherTab,
32}
33
34#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
35enum Mode {
36 #[default]
37 Running,
38 Destroy,
39 Quit,
40}
41
42#[derive(Debug, Clone, Copy, Default, Display, EnumIter, FromRepr, PartialEq, Eq)]
43enum Tab {
44 #[default]
45 About,
46 Recipe,
47 Email,
48 Traceroute,
49 Weather,
50}
51
52impl App {
53 pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
55 while self.is_running() {
56 terminal
57 .draw(|frame| self.draw(frame))
58 .wrap_err("terminal.draw")?;
59 self.handle_events()?;
60 }
61 Ok(())
62 }
63
64 fn is_running(&self) -> bool {
65 self.mode != Mode::Quit
66 }
67
68 fn draw(&self, frame: &mut Frame) {
70 frame.render_widget(self, frame.area());
71 if self.mode == Mode::Destroy {
72 destroy::destroy(frame);
73 }
74 }
75
76 fn handle_events(&mut self) -> Result<()> {
81 let timeout = Duration::from_secs_f64(1.0 / 50.0);
82 if !event::poll(timeout)? {
83 return Ok(());
84 }
85 match event::read()? {
86 Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
87 _ => {}
88 }
89 Ok(())
90 }
91
92 fn handle_key_press(&mut self, key: KeyEvent) {
93 match key.code {
94 KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,
95 KeyCode::Char('h') | KeyCode::Left => self.prev_tab(),
96 KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
97 KeyCode::Char('k') | KeyCode::Up => self.prev(),
98 KeyCode::Char('j') | KeyCode::Down => self.next(),
99 KeyCode::Char('d') | KeyCode::Delete => self.destroy(),
100 _ => {}
101 };
102 }
103
104 fn prev(&mut self) {
105 match self.tab {
106 Tab::About => self.about_tab.prev_row(),
107 Tab::Recipe => self.recipe_tab.prev(),
108 Tab::Email => self.email_tab.prev(),
109 Tab::Traceroute => self.traceroute_tab.prev_row(),
110 Tab::Weather => self.weather_tab.prev(),
111 }
112 }
113
114 fn next(&mut self) {
115 match self.tab {
116 Tab::About => self.about_tab.next_row(),
117 Tab::Recipe => self.recipe_tab.next(),
118 Tab::Email => self.email_tab.next(),
119 Tab::Traceroute => self.traceroute_tab.next_row(),
120 Tab::Weather => self.weather_tab.next(),
121 }
122 }
123
124 fn prev_tab(&mut self) {
125 self.tab = self.tab.prev();
126 }
127
128 fn next_tab(&mut self) {
129 self.tab = self.tab.next();
130 }
131
132 fn destroy(&mut self) {
133 self.mode = Mode::Destroy;
134 }
135}
136
137impl Widget for &App {
141 fn render(self, area: Rect, buf: &mut Buffer) {
142 let vertical = Layout::vertical([
143 Constraint::Length(1),
144 Constraint::Min(0),
145 Constraint::Length(1),
146 ]);
147 let [title_bar, tab, bottom_bar] = vertical.areas(area);
148
149 Block::new().style(THEME.root).render(area, buf);
150 self.render_title_bar(title_bar, buf);
151 self.render_selected_tab(tab, buf);
152 App::render_bottom_bar(bottom_bar, buf);
153 }
154}
155
156impl App {
157 fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
158 let layout = Layout::horizontal([Constraint::Min(0), Constraint::Length(43)]);
159 let [title, tabs] = layout.areas(area);
160
161 Span::styled("Ratatui", THEME.app_title).render(title, buf);
162 let titles = Tab::iter().map(Tab::title);
163 Tabs::new(titles)
164 .style(THEME.tabs)
165 .highlight_style(THEME.tabs_selected)
166 .select(self.tab as usize)
167 .divider("")
168 .padding("", "")
169 .render(tabs, buf);
170 }
171
172 fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
173 match self.tab {
174 Tab::About => self.about_tab.render(area, buf),
175 Tab::Recipe => self.recipe_tab.render(area, buf),
176 Tab::Email => self.email_tab.render(area, buf),
177 Tab::Traceroute => self.traceroute_tab.render(area, buf),
178 Tab::Weather => self.weather_tab.render(area, buf),
179 };
180 }
181
182 fn render_bottom_bar(area: Rect, buf: &mut Buffer) {
183 let keys = [
184 ("H/←", "Left"),
185 ("L/→", "Right"),
186 ("K/↑", "Up"),
187 ("J/↓", "Down"),
188 ("D/Del", "Destroy"),
189 ("Q/Esc", "Quit"),
190 ];
191 let spans = keys
192 .iter()
193 .flat_map(|(key, desc)| {
194 let key = Span::styled(format!(" {key} "), THEME.key_binding.key);
195 let desc = Span::styled(format!(" {desc} "), THEME.key_binding.description);
196 [key, desc]
197 })
198 .collect_vec();
199 Line::from(spans)
200 .centered()
201 .style((Color::Indexed(236), Color::Indexed(232)))
202 .render(area, buf);
203 }
204}
205
206impl Tab {
207 fn next(self) -> Self {
208 let current_index = self as usize;
209 let next_index = current_index.saturating_add(1);
210 Self::from_repr(next_index).unwrap_or(self)
211 }
212
213 fn prev(self) -> Self {
214 let current_index = self as usize;
215 let prev_index = current_index.saturating_sub(1);
216 Self::from_repr(prev_index).unwrap_or(self)
217 }
218
219 fn title(self) -> String {
220 match self {
221 Self::About => String::new(),
222 tab => format!(" {tab} "),
223 }
224 }
225}