1use std::thread;
2use std::{sync::mpsc::Receiver};
3
4use std::sync::mpsc::{Sender, channel};
5use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
6
7use elytra_conf::entry::ExtraFlags;
8use ratatui::text::Span;
9use ratatui::prelude::*;
10use ratatui::widgets::{Clear, List, ListDirection, ListItem, Padding, Row, Table};
11use ratatui::{
12 DefaultTerminal, Frame, buffer::Buffer, layout::{Constraint, Layout, Rect}, style::Stylize, symbols::border, text::{Line, Text}, widgets::{Block, Paragraph, Widget}
13};
14
15use crate::{ElytraDevice, Entry, Info, LayoutEntry, Section};
16
17
18type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
19
20enum Progress {
21 Working((String, Vec<([u8; 64], [u8; 64])>)),
22 Failed((String, Vec<([u8; 64], [u8; 64])>)),
23 Done(DeviceInfo)
24}
25
26enum AppState {
27 Working(LoadingWidget),
28 Done(DeviceInfo)
29}
30
31pub fn run(mut device: Box<dyn ElytraDevice + 'static>) -> Result<()> {
32 color_eyre::install()?;
33 let mut terminal = ratatui::init();
34
35 let (tx, rx) = channel();
37
38 thread::spawn(move || {
39 let final_progress = match run_worker(&mut device, tx.clone()) {
40 Ok(di) => Progress::Done(di),
41 Err(e) => Progress::Failed((format!("{:?}", e), device.get_log()))
42 };
43 tx.send(final_progress).unwrap();
44 });
45
46 let result = App{ rx, state: AppState::Working(LoadingWidget::new()), exit: false }.run(&mut terminal);
47 ratatui::restore();
48 Ok(result?)
49}
50
51fn run_worker(mut device: &mut Box<dyn ElytraDevice + 'static>, tx: Sender<Progress>) -> Result<DeviceInfo> {
52 let _ = tx.send(Progress::Working(("Getting device info".to_owned(), vec![])));
53 let info = device.get_info()?;
54
55
56 let mut sections = get_entries(&mut device, &tx, b's', info.section_count as usize, "sections")?;
57 get_layout(&mut device, &tx, &mut sections)?;
58 let props = get_entries(&mut device, &tx, b'c', info.prop_count as usize, "prop fields")?;
59 let infos = get_entries(&mut device, &tx, b'i', info.info_count as usize, "info fields")?;
60 let actions = get_entries(&mut device, &tx, b'a', info.action_count as usize, "actions")?;
61
62
63 tx.send(Progress::Working(("Assembling sections".to_owned(), device.get_log())))?;
64
65 let sections = sections.into_iter().map(|section_entry| {
68 let layout = section_entry.layout.clone().unwrap_or_default();
69 let layout = layout.into_iter().map(|le| {
70 match le {
71 LayoutEntry::Prop(ci) => (le, props[ci as usize].clone()),
72 LayoutEntry::Info(ii) => (le, infos[ii as usize].clone())
73 }
74 }).collect();
75
76 Section {entry: section_entry, layout}
77 }).collect();
78
79
80 Ok(DeviceInfo{
81 info,
82 sections,
83 actions,
84 section_index: 0
85 })
86}
87
88fn get_extras(
89 device: &mut Box<dyn ElytraDevice + 'static>,
90 tx: &Sender<Progress>,
91 entries: &mut Vec<Entry>,
92 extra_type: u8,
93 cond: impl Fn(&Entry) -> bool,
94 apply: impl Fn(&mut Entry, String) -> (),
95 name: &str) -> Result<()> {
96 let count = entries.iter().filter(|e| cond(e)).count();
97 let _ = tx.send(Progress::Working((format!(" Getting {} {}", count, name), device.get_log())));
98 for (index, entry) in entries.iter_mut().enumerate().filter(|(_, e)| cond(e)) {
99 apply(entry, device.get_extra(entry.entry_type, index as u8, extra_type)?);
100 }
101 Ok(())
103}
104
105fn get_layout(
106 device: &mut Box<dyn ElytraDevice + 'static>,
107 tx: &Sender<Progress>,
108 sections: &mut Vec<Entry>) -> Result<()> {
109 let _ = tx.send(Progress::Working((format!(" Getting section layout"), device.get_log())));
110
111
112 for (index, entry) in sections.iter_mut().enumerate() {
113 let layout = device.get_layout(index as u8)?;
114 entry.layout = Some(layout)
115 }
116 Ok(())
118}
119
120fn get_entries(device: &mut Box<dyn ElytraDevice + 'static>, tx: &Sender<Progress>, entry_type: u8, count: usize, n: &str) -> Result<Vec<Entry>> {
121 let _ = tx.send(Progress::Working((format!("Getting {} {}", count, n), device.get_log())));
122 let mut entries = device.get_entries(entry_type, count as usize)?;
124
125 get_extras(device, tx, &mut entries,
126 b'h',
127 |e| e.flags.contains(ExtraFlags::HasHelp),
128 |e, extra| e.help = Some(extra),
129 "help texts")?;
130
131 get_extras(device, tx, &mut entries,
132 b'i',
133 |e| e.flags.contains(ExtraFlags::HasIcon),
134 |e, extra| e.icon = Some(extra),
135 "icons")?;
136
137 Ok(entries)
138}
139
140struct DeviceInfo {
141 info: Info,
142 sections: Vec<Section>,
143 section_index: usize,
144 #[allow(unused)]
145 actions: Vec<Entry>
146}
147
148pub struct App {
149 exit: bool,
150 state: AppState,
151 rx: Receiver<Progress>,
152}
153
154impl App {
155
156 pub fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
158
159
160
161 while !self.exit {
162 terminal.draw(|frame| self.draw(frame))?;
163 self.handle_events()?;
164 if let Ok(progress) = self.rx.try_recv() {
165 match progress {
166 Progress::Done(di) => {
167 self.state = AppState::Done(di)
168 },
169 Progress::Working((status, mut items)) => {
170 match &mut self.state {
171 AppState::Working(loading_widget) => {
172 loading_widget.statuses.push(status);
173 loading_widget.log.append(&mut items);
174 },
175 _ => {
176 self.state = AppState::Working(LoadingWidget {
177 log: items,
178 statuses: vec![status],
179 failure: None
180 });
181 }
182 }
183 },
184 Progress::Failed((err, mut items)) => {
185 match &mut self.state {
186 AppState::Working(loading_widget) => {
187 loading_widget.log.append(&mut items);
188 loading_widget.failure = Some(err);
189 },
190 _ => {
191 self.state = AppState::Working(LoadingWidget {
192 log: items,
193 statuses: vec!["Initialization".into()],
194 failure: Some(err)
195 });
196 }
197 }
198 },
199 }
200 }
201 }
202 Ok(())
203 }
204
205 fn draw(&self, frame: &mut Frame) {
206 frame.render_widget(self, frame.area());
207 }
208
209 fn handle_events(&mut self) -> Result<()> {
210 if ! event::poll(std::time::Duration::from_millis(100))? {
211 return Ok(())
212 }
213 match event::read()? {
214 Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
217 self.handle_key_event(key_event)
218 }
219
220 _ => Ok(())
221 }
222 }
223
224 fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> {
225 match key_event.code {
226 KeyCode::Char('q') => self.exit(),
227 KeyCode::Up => self.update_selection(-1),
228 KeyCode::Down => self.update_selection(1),
229 _ => Ok(())
230 }
231 }
232
233 fn exit(&mut self) -> Result<()> {
234 self.exit = true;
235 Ok(())
236 }
237
238 fn update_selection(&mut self, arg: i32) -> Result<()> {
239 if let AppState::Done(dev_info) = &mut self.state {
240 if arg > 0 {
241 dev_info.section_index = (dev_info.section_index + 1).min(dev_info.sections.len() - 1);
242 } else {
243 dev_info.section_index = dev_info.section_index.saturating_sub(1);
244 }
245 }
246 Ok(())
247 }
248
249}
250
251impl Widget for &App {
252 fn render(self, area: Rect, buf: &mut Buffer) {
253
254 let title = Line::from(" Elytra ".bold());
255 let block = Block::bordered()
256 .title(title.centered())
257 .border_set(border::THICK);
259
260
261 let area = block.inner(area);
262 block.render(area, buf);
263 match &self.state {
264 AppState::Working(progress) => progress.render(area, buf),
265 AppState::Done(device_info) => device_info.render(area, buf),
266 }
268 }
269}
270
271impl Widget for &DeviceInfo {
272 fn render(self, area: Rect, buf: &mut Buffer) {
273
274 let vertical = Layout::vertical([
275 Constraint::Length(6),
276 Constraint::Fill(1)
277 ]).spacing(0)
279 .vertical_margin(1)
280 .horizontal_margin(2);
281
282 let rows = vertical.split(area);
283
284 Paragraph::new(Text::from_iter([
285 Line::from_iter([
286 Span::from("Version:"),
287 Span::from(format!("{}", self.info.proto_version))
288 ])
289 ]))
290 .block(Block::bordered().title(" Info ").padding(Padding::uniform(1)))
291
292 .render(rows[0], buf);
293
294 let max_section_name = self.sections.iter().map(|s| s.entry.name.len()).max().unwrap_or(20);
295
296 let horz = Layout::horizontal([Constraint::Length(max_section_name as u16 + 4), Constraint::Fill(1)])
297 .spacing(1);
298 let horz = horz.split(rows[1]);
299
300
301
302 let tabs = Paragraph::new(
306 Text::from_iter(self.sections.iter().enumerate().map(|(index, section)| {
307 Line::from(format!(" {:max_section_name$} ", section.entry.name)).style(if index == self.section_index {
308 Style::new().bg(Color::White).fg(Color::Black)
309 } else {
310 Style::new()
311 })
312 })))
313 .block(Block::bordered().title(" Sections ").padding(Padding::symmetric(0, 0)).title_alignment(Alignment::Center));
314 tabs.render(horz[0], buf);
315
316 if let Some(section) = self.sections.get(self.section_index) {
317 let section_text = Text::from_iter(section.layout.iter().flat_map(|(_, e)|
318 [
319 Line::from_iter([
320 Span::from(format!("{}", e.name)),
321 ]),
322 Line::from_iter([
324 Span::from(format!("{}", e.help.clone().unwrap_or_default())).fg(Color::DarkGray)
325 ]),
326 Line::from(""),
327 ]
328 ));
329 let para = Paragraph::new(section_text)
330 .left_aligned()
331 .block(Block::bordered().padding(Padding::symmetric(2, 1))
332 .title(Line::from(format!(" {} ", section.entry.name))))
333 ;
334 Widget::render(Clear, horz[1], buf);
335 para.render(horz[1], buf);
336 }
337
338
339 }
384}
385
386struct LoadingWidget {
387 log: Vec<([u8; 64], [u8; 64])>,
388 statuses: Vec<String>,
389 failure: Option<String>
390}
391impl LoadingWidget {
392 fn new() -> Self {
393 Self { log: vec![], statuses: vec![], failure: None }
394 }
395}
396
397impl Widget for &LoadingWidget {
398 fn render(self, area: Rect, buf: &mut Buffer) {
399
400 let vertical = Layout::vertical([
401 Constraint::Percentage(50),
402 Constraint::Percentage(50),
403 ]).spacing(0)
404 .vertical_margin(1)
405 .horizontal_margin(2);
406 let rows = vertical.split(area);
407
408 let loading_text =
409 self.statuses.iter().enumerate().map(|(i, status)|
410 ListItem::from(if i == self.statuses.len() -1 {
411 if let Some(failure) = &self.failure {Line::from_iter([
412 Span::from(format!("{:width$}🅧", "", width = status.chars().take_while(|c| c.is_whitespace()).count())).fg(Color::LightRed),
413 Span::from(format!(" {}", status.trim_start())).fg(Color::Red),
414 Span::from(" Failed: "),
415 Span::from(format!("{}", failure)).fg(Color::White),
416 ])
417 } else {Line::from_iter([
418 Span::from(format!(" {}", status)).style(Style::new().white()),
419 Span::from("...")])
420 }
421 } else {Line::from_iter([
422 Span::from(format!("{:width$}✔", "", width = status.chars().take_while(|c| c.is_whitespace()).count())).style(Style::new().green()),
423 Span::from(format!(" {}", status.trim_start())).style(Style::new().dark_gray()),
424 ])})
425
426 );
427
428 let list = List::new(loading_text)
429 .direction(ListDirection::TopToBottom)
430 .block(Block::bordered().title(" Status ")
431 .padding(Padding::proportional(1)));
432
433 Widget::render(list, rows[0], buf);
434
435 let widths = [
436 Constraint::Length(2),
437 Constraint::Percentage(100),
438 Constraint::Length(32),
439 ];
440
441 let table = Table::new(self.log.iter().rev().flat_map(|(bout, bin)| {
442 let bout = fmt_hex_bytes(bout);
443 let bin = fmt_hex_bytes(bin);
444 [
445 Row::new([ Text::from(">>"), bout.0, bout.1 ]).height(2),
446 Row::new([ Text::from("<<"), bin.0, bin.1 ]).height(2),
447 ]
448 }), widths)
449 .block(Block::bordered().title(" Device Communication ").padding(Padding::proportional(1)))
450 .header(Row::new(vec!["Dir", "Bytes (hex)", "ASCII"])
451 .style(Style::new().bold())
452 .bottom_margin(0)
453 );
454 Widget::render(table, rows[1], buf);
455
456 }
457}
458
459fn fmt_hex_bytes(bytes: &[u8; 64]) -> (Text<'_>, Text<'_>) {
460
461 let (h, a) = bytes.chunks(32)
472 .map(fmt_chunk)
473 .map(|row|
474
475 (
476 Line::from_iter(row.iter().map(|(b, _, style)|
477 Span::from(format!("{:02x} ", b)).style(*style))),
478
479 Line::from_iter(row.iter().map(|(_, c, style)|
480 Span::from(format!("{}", c)).style(*style)))
481 )
482 ).fold((Vec::new(), Vec::new()), |(mut hex, mut ascii), x| {
484 hex.push(x.0);
485 ascii.push(x.1);
486 (hex, ascii)
487 });
488 (Text::from_iter(h), Text::from_iter(a))
489}
490
491fn fmt_chunk(chunk: &[u8]) -> Vec<(u8, char, Style)> {
492 chunk.iter().copied().map(|b| {
493 let c = char::try_from(b).unwrap_or('.');
494 if c.is_control() {
495 if (1..=9).contains(&b) {
496 (b, char::from_digit(b as u32, 10).unwrap(), Style::new().light_cyan())
497 } else {
498 (b, '.', Style::new().dark_gray())
499 }
500 } else {
501 (b, c, Style::new().light_yellow())
502 }
503 }).collect()
504}
505
506