elytra_cli/
tui.rs

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 info = device.get_info()?;
36    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    // Err(format!("Misc error: {:#?}", actions.len()))?;
66
67    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    // thread::sleep(std::time::Duration::from_secs(1));
102    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    // thread::sleep(std::time::Duration::from_secs(1));
117    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    // thread::sleep(std::time::Duration::from_secs(2));
123    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    /// runs the application's main loop until the user quits
157    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            // it's important to check that the event is a key press event as
215            // crossterm also emits key release and repeat events on Windows.
216            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            // .title_bottom(instructions.centered())
258            .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            // AppState::Failed(error) => FailedWidget(error.clone()).render(area, buf),
267        }
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            // Constraint::Percentage(50), 
278        ]).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 = Tabs::new(self.sections.iter().map(|s| s.entry.name.clone()))
303        //         .block(Block::bordered().title(" Sections "))
304        //         .select(0);
305        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("                 ").underlined(),
323                    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        // let horizontal = Layout::horizontal((0..2).map(|_| Constraint::Fill(1))).spacing(1);
340        // let vertical = Layout::vertical((0..3).map(|_| Constraint::Min(20))).spacing(1);
341        // let rows = vertical.split(rows[1]);
342        // let cells = rows.iter().flat_map(|&row| horizontal.split(row).to_vec());
343
344        // // let section_layout = Layout::default().direction(Direction::Vertical).constraints(
345        // //     (0..self.sections.len()).map(|_| Constraint::Max(10))
346        // // ).split(rows[1]);
347
348        // for eor in cells.zip_longest(self.sections.iter()) {
349        //     let (area, section) = match eor {
350        //         itertools::EitherOrBoth::Both(area, section) => (area, section),
351        //         itertools::EitherOrBoth::Left(area) => {
352        //             Widget::render(Clear, area, buf);
353        //             // Clear::default().render(area, buf);
354        //             break;
355        //         },
356        //         itertools::EitherOrBoth::Right(_) => {break},
357        //     };
358        //     let section_text = Text::from_iter(section.layout.iter().flat_map(|(_, e)|
359        //         [
360        //             Line::from_iter([ 
361        //                 Span::from(format!("{}", e.name)), 
362        //             ]),
363        //             // Line::from("                 ").underlined(),
364        //             Line::from_iter([ 
365        //                 Span::from(format!("{}", e.help.clone().unwrap_or_default())).fg(Color::DarkGray)
366        //             ]),
367        //             Line::from(""),
368        //         ]
369        //     ));
370        //     let para = Paragraph::new(section_text)
371        //         .left_aligned()
372        //         .block(Block::bordered().padding(Padding::symmetric(2, 1))
373        //             .title(Line::from(format!(" {} ", section.entry.name))))
374        //         ;
375        //     Widget::render(Clear, area, buf);
376        //     para.render(area, buf);
377        // };
378
379
380        
381       // Widget::render(list, rows[1], buf);
382
383    }
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): (Vec<Span<'_>>, Vec<Span<'_>>) = fmt_chunk(bytes).iter()
462        // .map(|(b, c, color)| 
463        //     (
464        //         Span::from(format!("{:02x} ", b.color(*color))),
465        //         Span::from(format!("{} ", c.color(*color)))
466        //     )
467        // ).enumerate().partition(|(i, _)| *i >= 32);
468    
469
470
471    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            //(Text::from(""), Text::from(""))
483        ).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// fn center(area: Rect, horizontal: Constraint, vertical: Constraint) -> Rect {
507//     let [area] = Layout::horizontal([horizontal])
508//         .flex(Flex::Center)
509//         .areas(area);
510//     let [area] = Layout::vertical([vertical])
511//         .flex(Flex::Center)
512//         .areas(area);
513//     area
514// }