use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use ratatui::{ layout::{Alignment, Constraint, Flex, Layout, Position, Rect}, prelude::Margin, style::{Color, Style}, widgets::{ Block, BorderType, Cell, Clear, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState, }, Frame, }; use std::sync::mpsc::Sender; use tokio::sync::mpsc::UnboundedSender; use super::{Component, CurrentTab, PartialComponent}; use crate::{ action::Action, config::Config, palette::StylePalette, widgets::{Input, InputRequest}, }; #[derive(Debug, Default, Eq, PartialEq)] enum Selected { #[default] Input, DefaultRpcs, StoredRpcs, } #[derive(Debug, Default)] pub struct GatekeeperEndpoints { is_active: bool, selected: Selected, chain_id: u64, rpc_input: Input, action_tx: Option>, network_tx: Option>, default_endpoints: Vec, stored_endpoints: Vec, default_scroll_state: ScrollbarState, default_table_state: TableState, stored_scroll_state: ScrollbarState, stored_table_state: TableState, palette: StylePalette, } impl GatekeeperEndpoints { fn close_popup(&mut self) { self.is_active = false; if let Some(action_tx) = &self.action_tx { let _ = action_tx.send(Action::ClosePopup); } } fn set_chain_id(&mut self, chain_id: u64) { self.chain_id = chain_id; if let Some(network_tx) = &self.network_tx { let _ = network_tx.send(Action::GetRpcEndpoints(chain_id)); } } fn submit_message(&mut self) { if let Some(network_tx) = &self.network_tx { if self.selected == Selected::Input { let mut stored_endpoints = self.stored_endpoints.clone(); stored_endpoints.push(self.rpc_input.value().to_string()); let _ = network_tx.send(Action::UpdateStoredRpcEndpoints( self.chain_id, stored_endpoints, )); self.rpc_input = Input::new(String::new()); } } } fn enter_char(&mut self, new_char: char) { match self.selected { Selected::Input => { let _ = self.rpc_input.handle(InputRequest::InsertChar(new_char)); } Selected::StoredRpcs if (new_char == 'd' || new_char == 'D') => { if let Some(index) = self.stored_table_state.selected() { let mut stored_endpoints = self.stored_endpoints.clone(); if let Some(network_tx) = &self.network_tx { stored_endpoints.remove(index); let _ = network_tx.send(Action::UpdateStoredRpcEndpoints( self.chain_id, stored_endpoints, )); } } } _ => match new_char { 'j' => self.move_cursor_down(), 'k' => self.move_cursor_up(), 'l' => self.move_cursor_right(), 'h' => self.move_cursor_left(), _ => {} }, } } fn delete_char(&mut self) { if self.selected == Selected::Input { let _ = self.rpc_input.handle(InputRequest::DeletePrevChar); } } fn move_cursor_up(&mut self) { match self.selected { Selected::Input => {} Selected::DefaultRpcs => { let i = match self.default_table_state.selected() { Some(i) => { if i == 0 { 0 } else { i - 1 } } None => 0, }; self.default_table_state.select(Some(i)); self.default_scroll_state = self.default_scroll_state.position(i); } Selected::StoredRpcs => { let i = match self.stored_table_state.selected() { Some(i) => { if i == 0 { 0 } else { i - 1 } } None => 0, }; self.stored_table_state.select(Some(i)); self.stored_scroll_state = self.stored_scroll_state.position(i); } }; } fn move_cursor_down(&mut self) { match self.selected { Selected::Input => {} Selected::DefaultRpcs => { let i = match self.default_table_state.selected() { Some(i) => { if i >= self.default_endpoints.len() - 1 { i } else { i + 1 } } None => 0, }; self.default_table_state.select(Some(i)); self.default_scroll_state = self.default_scroll_state.position(i); } Selected::StoredRpcs => { let i = match self.stored_table_state.selected() { Some(i) => { if i >= self.stored_endpoints.len() - 1 { i } else { i + 1 } } None => 0, }; self.stored_table_state.select(Some(i)); self.stored_scroll_state = self.stored_scroll_state.position(i); } }; } fn move_cursor_right(&mut self) { self.selected = match self.selected { Selected::Input => Selected::StoredRpcs, Selected::DefaultRpcs => Selected::Input, Selected::StoredRpcs => Selected::StoredRpcs, }; self.update_selected_state(); } fn move_cursor_left(&mut self) { self.selected = match self.selected { Selected::Input => Selected::DefaultRpcs, Selected::StoredRpcs => Selected::Input, Selected::DefaultRpcs => Selected::DefaultRpcs, }; self.update_selected_state(); } fn update_selected_state(&mut self) { self.stored_table_state.select(None); self.default_table_state.select(None); self.stored_scroll_state = self.stored_scroll_state.position(0); self.default_scroll_state = self.default_scroll_state.position(0); } fn prepare_table<'a>( &self, rpcs: &'a Vec, title_name: &'a str, border_style: Color, border_type: BorderType, scrollbar_style: Style, ) -> (Table<'a>, Scrollbar<'a>) { let table = Table::new( rpcs.iter() .map(|endpoint| Row::new(vec![Cell::from(endpoint.as_str())])) .collect::>(), [Constraint::Min(1)], ) .highlight_style(self.palette.create_highlight_style()) .block( Block::bordered() .border_style(border_style) .border_type(border_type) .title_alignment(Alignment::Right) .title_style(self.palette.create_title_style(false)) .title(title_name), ); let scrollbar = Scrollbar::default() .orientation(ScrollbarOrientation::VerticalRight) .begin_symbol(None) .end_symbol(None) .style(scrollbar_style); (table, scrollbar) } } impl PartialComponent for GatekeeperEndpoints { fn set_active(&mut self, current_tab: CurrentTab) { match current_tab { CurrentTab::GatekeeperEndpoints => self.is_active = true, _ => { self.is_active = false; self.rpc_input = Input::new(String::new()); } }; } } impl Component for GatekeeperEndpoints { fn register_network_handler(&mut self, tx: Sender) -> Result<()> { self.network_tx = Some(tx); Ok(()) } 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::Wallet) { self.palette .with_normal_style(style.get("normal_style").copied()); self.palette .with_normal_border_style(style.get("normal_border_style").copied()); self.palette .with_normal_title_style(style.get("normal_title_style").copied()); self.palette .with_popup_style(style.get("popup_style").copied()); self.palette .with_popup_title_style(style.get("popup_title_style").copied()); self.palette .with_hover_style(style.get("hover_style").copied()); self.palette .with_hover_border_style(style.get("hover_border_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 handle_key_event(&mut self, key: KeyEvent) -> Result> { if self.is_active && key.kind == KeyEventKind::Press { match key.code { KeyCode::Enter => self.submit_message(), KeyCode::Char(to_insert) => self.enter_char(to_insert), KeyCode::Backspace => self.delete_char(), KeyCode::Left => self.move_cursor_left(), KeyCode::Right => self.move_cursor_right(), KeyCode::Up => self.move_cursor_up(), KeyCode::Down => self.move_cursor_down(), KeyCode::Esc => self.close_popup(), _ => {} }; } Ok(None) } fn update(&mut self, action: Action) -> Result> { match action { Action::SetChoosenGatekeeper(chain_id) => self.set_chain_id(chain_id), Action::SetGatekeepedNetwork(network) => { self.default_endpoints = network.default_endpoints } Action::SetStoredRpcEndpoints(stored_endpoints) => { self.stored_endpoints = stored_endpoints } _ => {} }; Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { if self.is_active { let (border_style, border_type) = self.palette.create_popup_style(); let (selected_border_style, selected_border_type) = self.palette.create_border_style(true); let scrollbar_style = self.palette.create_scrollbar_style(); let (default_border_style, default_border_type) = match self.selected { Selected::DefaultRpcs => (selected_border_style, selected_border_type), _ => (border_style, border_type), }; let (stored_border_style, stored_border_type) = match self.selected { Selected::StoredRpcs => (selected_border_style, selected_border_type), _ => (border_style, border_type), }; let (input_border_style, input_border_type) = match self.selected { Selected::Input => (selected_border_style, selected_border_type), _ => (border_style, border_type), }; let (default_rpcs, default_scrollbar) = self.prepare_table( &self.default_endpoints, "Default RPCs", default_border_style, default_border_type, scrollbar_style, ); let (stored_rpcs, stored_scrollbar) = self.prepare_table( &self.stored_endpoints, "Stored RPCs", stored_border_style, stored_border_type, scrollbar_style, ); let input = Paragraph::new(self.rpc_input.value()).block( Block::bordered() .border_style(input_border_style) .border_type(input_border_type) .title_style(self.palette.create_popup_title_style()) .title_alignment(Alignment::Right) .title("Input new RPC"), ); let v = Layout::vertical([Constraint::Max(14)]).flex(Flex::Center); let h = Layout::horizontal([Constraint::Max(80)]).flex(Flex::Center); let [area] = v.areas(area); let [area] = h.areas(area); frame.render_widget(Clear, area); let [tables_area, input_area] = Layout::vertical([Constraint::Length(11), Constraint::Length(3)]).areas(area); let [default_table_area, stored_table_area] = Layout::horizontal([Constraint::Max(40), Constraint::Max(40)]).areas(tables_area); frame.render_stateful_widget( default_rpcs, default_table_area, &mut self.default_table_state, ); frame.render_stateful_widget( default_scrollbar, default_table_area.inner(Margin { vertical: 1, horizontal: 1, }), &mut self.default_scroll_state, ); frame.render_stateful_widget( stored_rpcs, stored_table_area, &mut self.stored_table_state, ); frame.render_stateful_widget( stored_scrollbar, stored_table_area.inner(Margin { vertical: 1, horizontal: 1, }), &mut self.stored_scroll_state, ); frame.render_widget(input, input_area); frame.set_cursor_position(Position::new( input_area.x + self.rpc_input.cursor() as u16 + 1, input_area.y + 1, )); } Ok(()) } }