use std::collections::{HashMap, VecDeque}; use std::usize; use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::layout::{Constraint, Margin}; use ratatui::widgets::{Padding, Scrollbar, ScrollbarOrientation}; use ratatui::{ text::Text, layout::{Alignment, Rect}, widgets::{Block, ScrollbarState, Cell, Row, Table, TableState}, Frame }; use tokio::sync::mpsc::UnboundedSender; use super::{Component, CurrentTab, PartialComponent}; use crate::{ types::CasperExtrinsicDetails, action::Action, config::Config, palette::StylePalette, }; pub struct ExtrinsicExplorer { is_active: bool, action_tx: Option>, extrinsics: HashMap>, current_extrinsics: Option>, block_numbers: VecDeque, scroll_state: ScrollbarState, table_state: TableState, palette: StylePalette, } impl Default for ExtrinsicExplorer { fn default() -> Self { Self::new() } } impl ExtrinsicExplorer { const MAX_BLOCKS: usize = 50; pub fn new() -> Self { Self { is_active: false, action_tx: None, current_extrinsics: None, extrinsics: Default::default(), block_numbers: Default::default(), scroll_state: ScrollbarState::new(0), table_state: TableState::new(), palette: StylePalette::default(), } } fn update_extrinsics_for_header( &mut self, block_number: u32, extrinsics: Vec, ) { self.extrinsics.insert(block_number, extrinsics); self.block_numbers.push_front(block_number); if self.block_numbers.len() > Self::MAX_BLOCKS { if let Some(remove_block_number) = self.block_numbers.pop_back() { let _ = self.extrinsics.remove(&remove_block_number); } } } fn send_used_explorer_log(&mut self, index: usize) { if let Some(action_tx) = &self.action_tx { let maybe_log = self.current_extrinsics .as_ref() .map(|ext| { ext.get(index).map(|ext| { hex::encode(&ext.field_bytes.clone()) }) }) .flatten(); let _ = action_tx.send(Action::UsedExplorerLog(maybe_log.clone())); } } fn first_row(&mut self) { match &self.current_extrinsics { Some(exts) if exts.len() > 0 => self.table_state.select(Some(0)), _ => self.table_state.select(None), } self.scroll_state = self.scroll_state.position(0); self.send_used_explorer_log(0); } fn next_row(&mut self) { match &self.current_extrinsics { Some(exts) => { let i = match self.table_state.selected() { Some(i) => { if i >= exts.len() - 1 { i } else { i + 1 } }, None => 0, }; self.table_state.select(Some(i)); self.scroll_state = self.scroll_state.position(i); self.send_used_explorer_log(i); }, None => { self.table_state.select(None); self.scroll_state = self.scroll_state.position(0); self.send_used_explorer_log(0); } } } fn last_row(&mut self) { match &self.current_extrinsics { Some(exts) => { let last = exts.len().saturating_sub(1); self.table_state.select(Some(last)); self.scroll_state = self.scroll_state.position(last); self.send_used_explorer_log(last); }, None => { self.table_state.select(None); self.scroll_state = self.scroll_state.position(0); self.send_used_explorer_log(0); } } } fn previous_row(&mut self) { match &self.current_extrinsics { Some(_) => { let i = self.table_state .selected() .map(|i| i.saturating_sub(1)) .unwrap_or_default(); self.table_state.select(Some(i)); self.scroll_state = self.scroll_state.position(i); self.send_used_explorer_log(i); }, None => { self.table_state.select(None); self.scroll_state = self.scroll_state.position(0); self.send_used_explorer_log(0); } } } } impl PartialComponent for ExtrinsicExplorer { fn set_active(&mut self, current_tab: CurrentTab) { if current_tab == CurrentTab::Extrinsics { self.is_active = true; } else { self.is_active = false; self.table_state.select(None); self.scroll_state = self.scroll_state.position(0); self.send_used_explorer_log(usize::MAX); } } } impl Component for ExtrinsicExplorer { fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.action_tx = Some(tx); Ok(()) } fn register_config_handler(&mut self, config: Config) -> Result<()> { if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { self.palette.with_normal_style(style.get("normal_style").copied()); self.palette.with_hover_style(style.get("hover_style").copied()); self.palette.with_normal_border_style(style.get("normal_border_style").copied()); self.palette.with_hover_border_style(style.get("hover_border_style").copied()); self.palette.with_normal_title_style(style.get("normal_title_style").copied()); self.palette.with_hover_title_style(style.get("hover_title_style").copied()); self.palette.with_highlight_style(style.get("highlight_style").copied()); self.palette.with_scrollbar_style(style.get("scrollbar_style").copied()); } Ok(()) } fn update(&mut self, action: Action) -> Result> { match action { Action::UsedExplorerBlock(maybe_block_number) => { let block_number = maybe_block_number.unwrap_or_default(); if let Some(exts) = self.extrinsics.get(&block_number) { self.current_extrinsics = Some(exts.to_vec()); self.scroll_state = self.scroll_state.content_length(exts.len()); } else { self.current_extrinsics = None; self.scroll_state = self.scroll_state.content_length(0); } }, Action::ExtrinsicsForBlock(block_number, extrinsics) => self.update_extrinsics_for_header(block_number, extrinsics), _ => {}, }; Ok(None) } fn handle_key_event(&mut self, key: KeyEvent) -> Result> { match key.code { KeyCode::Char('k') | KeyCode::Up if self.is_active => self.previous_row(), KeyCode::Char('j') | KeyCode::Down if self.is_active => self.next_row(), KeyCode::Char('g') if self.is_active => self.first_row(), KeyCode::Char('G') if self.is_active => self.last_row(), _ => {}, }; Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let [_, place] = super::explorer_scrollbars_layout(area); let (border_style, border_type) = self.palette.create_border_style(self.is_active); let mut longest_pallet_name_length = 0; let mut longest_variant_name_length = 0; let rows = match &self.current_extrinsics { Some(exts) => exts .iter() .enumerate() .map(|(idx, ext)| { longest_pallet_name_length = longest_pallet_name_length.max(ext.pallet_name.len()); longest_variant_name_length = longest_variant_name_length.max(ext.variant_name.len()); Row::new(vec![ Cell::from(Text::from(idx.to_string()).alignment(Alignment::Left)), Cell::from(Text::from(ext.pallet_name.clone()).alignment(Alignment::Right)), Cell::from(Text::from(ext.variant_name.clone()).alignment(Alignment::Left)), Cell::from(Text::from(ext.hash.to_string()).alignment(Alignment::Right)), ]) }) .collect::>(), None => Vec::new(), }; let table = Table::new( rows, [ Constraint::Max(2), Constraint::Min(longest_pallet_name_length as u16 + 2), Constraint::Min(longest_variant_name_length as u16 + 2), Constraint::Min(0), ], ) .style(self.palette.create_basic_style(false)) .highlight_style(self.palette.create_basic_style(true)) .column_spacing(1) .block(Block::bordered() .border_style(border_style) .border_type(border_type) .padding(Padding::right(2)) .title_alignment(Alignment::Right) .title_style(self.palette.create_title_style(false)) .title("Extrinsics")); let scrollbar = Scrollbar::default() .orientation(ScrollbarOrientation::VerticalRight) .begin_symbol(None) .end_symbol(None) .style(self.palette.create_scrollbar_style()); frame.render_stateful_widget(table, place, &mut self.table_state); frame.render_stateful_widget( scrollbar, place.inner(Margin { vertical: 1, horizontal: 1 }), &mut self.scroll_state, ); Ok(()) } }