Skip to main content

max / ripgrow

11.7 KB · 345 lines History Blame Raw
1 //! Top-level app state and the render/event dispatch.
2 //!
3 //! While no profile is open the picker takes the whole screen. Once one is
4 //! open, a tab bar sits under the header and `Alt+1`/`Alt+2` (or `Ctrl+Tab`)
5 //! switch between Templates and Log. Screen keys never receive Alt-modified
6 //! digits, so text entry keeps working.
7
8 use std::path::PathBuf;
9
10 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
11 use ratatui::Frame;
12 use ratatui::style::{Modifier, Style};
13 use ratatui::text::{Line, Span};
14 use ratatui::widgets::{Block, Borders, Tabs};
15 use ripgrow_core::{
16 Db, LoadUnit, create_profile_in, list_profiles, profiles_dir, seed_starter_content,
17 };
18
19 use crate::screens::diagnostic::DiagnosticScreen;
20 use crate::screens::generate::GenerateScreen;
21 use crate::screens::history::{HistoryAction, HistoryScreen};
22 use crate::screens::log::LogScreen;
23 use crate::screens::profile_picker::{PickerAction, ProfilePicker};
24 use crate::screens::templates::{TemplatesAction, TemplatesScreen};
25
26 pub struct App {
27 picker: ProfilePicker,
28 opened: Option<Opened>,
29 status: String,
30 pub should_quit: bool,
31 }
32
33 struct Opened {
34 path: PathBuf,
35 db: Db,
36 tab: Tab,
37 templates: TemplatesScreen,
38 log: LogScreen,
39 generate: GenerateScreen,
40 history: HistoryScreen,
41 diagnostic: DiagnosticScreen,
42 }
43
44 #[derive(Clone, Copy, PartialEq, Eq)]
45 enum Tab {
46 Templates,
47 Log,
48 Generate,
49 History,
50 Diagnostic,
51 }
52
53 impl Tab {
54 const ALL: [Tab; 5] = [
55 Tab::Templates,
56 Tab::Log,
57 Tab::Generate,
58 Tab::History,
59 Tab::Diagnostic,
60 ];
61
62 fn title(self) -> &'static str {
63 match self {
64 Tab::Templates => "templates",
65 Tab::Log => "log",
66 Tab::Generate => "generate",
67 Tab::History => "history",
68 Tab::Diagnostic => "diagnostic",
69 }
70 }
71 }
72
73 impl App {
74 pub fn new() -> Result<Self, ripgrow_core::Error> {
75 let profiles = list_profiles()?;
76 Ok(Self {
77 picker: ProfilePicker::new(profiles),
78 opened: None,
79 status: String::new(),
80 should_quit: false,
81 })
82 }
83
84 pub fn render(&mut self, frame: &mut Frame) {
85 use ratatui::layout::{Constraint, Direction, Layout};
86
87 let area = frame.area();
88
89 match &mut self.opened {
90 Some(op) => {
91 let chunks = Layout::default()
92 .direction(Direction::Vertical)
93 .constraints([
94 Constraint::Length(1),
95 Constraint::Length(3),
96 Constraint::Min(1),
97 ])
98 .split(area);
99
100 let header = format!(
101 "ripgrow | {} | {}",
102 op.db.profile_name().ok().flatten().unwrap_or_default(),
103 op.path.display()
104 );
105 frame.render_widget(ratatui::widgets::Paragraph::new(header), chunks[0]);
106
107 let titles: Vec<Line> = Tab::ALL
108 .iter()
109 .map(|t| Line::from(t.title()))
110 .collect();
111 let idx = Tab::ALL.iter().position(|t| *t == op.tab).unwrap_or(0);
112 let tabs = Tabs::new(titles)
113 .block(Block::default().borders(Borders::ALL))
114 .select(idx)
115 .highlight_style(Style::default().add_modifier(Modifier::REVERSED));
116 frame.render_widget(tabs, chunks[1]);
117
118 match op.tab {
119 Tab::Templates => op.templates.render(frame, chunks[2]),
120 Tab::Log => op.log.render(frame, chunks[2]),
121 Tab::Generate => op.generate.render(frame, chunks[2], &op.db),
122 Tab::History => op.history.render(frame, chunks[2], &op.db),
123 Tab::Diagnostic => op.diagnostic.render(frame, chunks[2]),
124 }
125 }
126 None => {
127 let chunks = Layout::default()
128 .direction(Direction::Vertical)
129 .constraints([Constraint::Length(1), Constraint::Min(1)])
130 .split(area);
131 frame.render_widget(
132 ratatui::widgets::Paragraph::new(Span::styled(
133 format!("ripgrow {}", self.status),
134 Style::default().add_modifier(Modifier::DIM),
135 )),
136 chunks[0],
137 );
138 self.picker.render(frame, chunks[1]);
139 }
140 }
141 }
142
143 pub fn on_key(&mut self, key: KeyEvent) {
144 if let Some(op) = self.opened.as_mut() {
145 // Tab-switch keys handled at the app layer before dispatch.
146 if key.modifiers.contains(KeyModifiers::ALT) {
147 match key.code {
148 KeyCode::Char('1') => {
149 op.tab = Tab::Templates;
150 return;
151 }
152 KeyCode::Char('2') => {
153 op.tab = Tab::Log;
154 return;
155 }
156 KeyCode::Char('3') => {
157 op.tab = Tab::Generate;
158 return;
159 }
160 KeyCode::Char('4') => {
161 op.tab = Tab::History;
162 return;
163 }
164 KeyCode::Char('5') => {
165 op.tab = Tab::Diagnostic;
166 return;
167 }
168 _ => {}
169 }
170 }
171 if key.code == KeyCode::Char('c') && key.modifiers == KeyModifiers::CONTROL {
172 self.should_quit = true;
173 return;
174 }
175
176 match op.tab {
177 Tab::Templates => match op.templates.on_key(&op.db, key) {
178 TemplatesAction::None => {}
179 TemplatesAction::Quit => self.should_quit = true,
180 TemplatesAction::Refresh => {
181 // Reload downstream screens so newly-created
182 // templates and tags appear without extra plumbing.
183 if let Ok(next) = TemplatesScreen::load(&op.db) {
184 op.templates = next;
185 }
186 if let Ok(next) = LogScreen::load(&op.db) {
187 op.log = next;
188 }
189 if let Ok(next) = GenerateScreen::load(&op.db) {
190 op.generate = next;
191 }
192 if let Ok(next) = HistoryScreen::load(&op.db) {
193 op.history = next;
194 }
195 if let Ok(next) = DiagnosticScreen::load(&op.db) {
196 op.diagnostic = next;
197 }
198 }
199 },
200 Tab::Log => op.log.on_key(&op.db, key),
201 Tab::Generate => op.generate.on_key(&op.db, key),
202 Tab::History => match op.history.on_key(&op.db, key) {
203 HistoryAction::None => {}
204 HistoryAction::Quit => self.should_quit = true,
205 },
206 Tab::Diagnostic => op.diagnostic.on_key(&op.db, key),
207 }
208 return;
209 }
210
211 match self.picker.on_key(key) {
212 PickerAction::None => {}
213 PickerAction::Quit => self.should_quit = true,
214 PickerAction::Open(path) => self.open_profile(path),
215 PickerAction::Create { display_name, unit } => {
216 self.create_profile(&display_name, unit);
217 }
218 }
219 }
220
221 fn open_profile(&mut self, path: PathBuf) {
222 let db = match Db::open(&path) {
223 Ok(d) => d,
224 Err(e) => {
225 self.status = format!("open failed: {e}");
226 return;
227 }
228 };
229 let templates = match TemplatesScreen::load(&db) {
230 Ok(s) => s,
231 Err(e) => {
232 self.status = format!("load templates failed: {e}");
233 return;
234 }
235 };
236 let log = match LogScreen::load(&db) {
237 Ok(s) => s,
238 Err(e) => {
239 self.status = format!("load log failed: {e}");
240 return;
241 }
242 };
243 let generate = match GenerateScreen::load(&db) {
244 Ok(s) => s,
245 Err(e) => {
246 self.status = format!("load generate failed: {e}");
247 return;
248 }
249 };
250 let history = match HistoryScreen::load(&db) {
251 Ok(s) => s,
252 Err(e) => {
253 self.status = format!("load history failed: {e}");
254 return;
255 }
256 };
257 let diagnostic = match DiagnosticScreen::load(&db) {
258 Ok(s) => s,
259 Err(e) => {
260 self.status = format!("load diagnostic failed: {e}");
261 return;
262 }
263 };
264 self.status = format!("opened {}", path.display());
265 self.opened = Some(Opened {
266 path,
267 db,
268 tab: Tab::Templates,
269 templates,
270 log,
271 generate,
272 history,
273 diagnostic,
274 });
275 }
276
277 fn create_profile(&mut self, display_name: &str, unit: LoadUnit) {
278 let dir = match profiles_dir() {
279 Ok(d) => d,
280 Err(e) => {
281 self.status = format!("profiles dir: {e}");
282 return;
283 }
284 };
285 match create_profile_in(&dir, display_name, unit) {
286 Ok((profile, db)) => {
287 if let Err(e) = seed_starter_content(&db, unit) {
288 self.status = format!("seed failed: {e}");
289 return;
290 }
291 let templates = match TemplatesScreen::load(&db) {
292 Ok(s) => s,
293 Err(e) => {
294 self.status = format!("load templates failed: {e}");
295 return;
296 }
297 };
298 let log = match LogScreen::load(&db) {
299 Ok(s) => s,
300 Err(e) => {
301 self.status = format!("load log failed: {e}");
302 return;
303 }
304 };
305 let generate = match GenerateScreen::load(&db) {
306 Ok(s) => s,
307 Err(e) => {
308 self.status = format!("load generate failed: {e}");
309 return;
310 }
311 };
312 let history = match HistoryScreen::load(&db) {
313 Ok(s) => s,
314 Err(e) => {
315 self.status = format!("load history failed: {e}");
316 return;
317 }
318 };
319 let diagnostic = match DiagnosticScreen::load(&db) {
320 Ok(s) => s,
321 Err(e) => {
322 self.status = format!("load diagnostic failed: {e}");
323 return;
324 }
325 };
326 self.status = format!("created {}", profile.slug);
327 self.opened = Some(Opened {
328 path: profile.path,
329 db,
330 tab: Tab::Templates,
331 templates,
332 log,
333 generate,
334 history,
335 diagnostic,
336 });
337 }
338 Err(e) => self.status = format!("create failed: {e}"),
339 }
340 if let Ok(list) = list_profiles() {
341 self.picker.profiles = list;
342 }
343 }
344 }
345