From 4d32d4d403b72e6e55a3258ef8bc16d4dcdece51 Mon Sep 17 00:00:00 2001 From: Uncle Stretch Date: Wed, 4 Dec 2024 18:01:24 +0300 Subject: [PATCH] extended address book functionality and basic logging intergration Signed-off-by: Uncle Stretch --- src/action.rs | 7 +- src/components/wallet/accounts.rs | 48 ++++- .../wallet/add_address_book_record.rs | 202 ++++++++++++++++++ src/components/wallet/address_book.rs | 102 ++++++++- src/components/wallet/event_logs.rs | 109 +++++++--- src/components/wallet/mod.rs | 43 +++- .../wallet/rename_address_book_record.rs | 141 ++++++++++++ src/types/log.rs | 10 + src/types/mod.rs | 2 + 9 files changed, 623 insertions(+), 41 deletions(-) create mode 100644 src/components/wallet/add_address_book_record.rs create mode 100644 src/components/wallet/rename_address_book_record.rs create mode 100644 src/types/log.rs diff --git a/src/action.rs b/src/action.rs index 44f43d9..76be6c8 100644 --- a/src/action.rs +++ b/src/action.rs @@ -5,7 +5,7 @@ use subxt::utils::H256; use subxt::config::substrate::DigestItem; use crate::{ - types::{EraInfo, CasperExtrinsicDetails}, + types::{ActionLevel, EraInfo, CasperExtrinsicDetails}, }; use subxt::utils::AccountId32; @@ -28,9 +28,14 @@ pub enum Action { UsedExplorerLog(Option), UsedAccount(AccountId32), NewAccount(String), + NewAddressBookRecord(String, String), RenameAccount(String), + RenameAddressBookRecord(String), UpdateAccountName(String), + UpdateAddressBookRecord(String), + + WalletLog(String, ActionLevel), NewBestBlock(u32), NewBestHash(H256), diff --git a/src/components/wallet/accounts.rs b/src/components/wallet/accounts.rs index 665f0b2..5c16b0b 100644 --- a/src/components/wallet/accounts.rs +++ b/src/components/wallet/accounts.rs @@ -26,6 +26,7 @@ use tokio::sync::mpsc::UnboundedSender; use super::{PartialComponent, Component, CurrentTab}; use crate::casper::CasperConfig; +use crate::types::ActionLevel; use crate::{ action::Action, config::Config, @@ -68,6 +69,13 @@ impl Accounts { let address = AccountId32::from(seed.clone()) .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + format!("new wallet '{}' with public address {} created", &name, &address), + ActionLevel::Info, + )); + } + self.wallet_keys.push((name, address, secret_seed, pair_signer)); self.last_row(); self.save_to_file(); @@ -75,8 +83,18 @@ impl Accounts { fn rename_account(&mut self, new_name: String) { if let Some(index) = self.table_state.selected() { + let old_name = self.wallet_keys[index].0.clone(); + + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + format!("wallet '{}' renamed to {}", &new_name, &old_name), + ActionLevel::Info, + )); + } + self.wallet_keys[index].0 = new_name; self.save_to_file(); + } } @@ -115,9 +133,17 @@ impl Accounts { fn delete_row(&mut self) { if let Some(index) = self.table_state.selected() { if self.wallet_keys.len() > 1 { - let _ = self.wallet_keys.remove(index); + let wallet = self.wallet_keys.remove(index); self.previous_row(); self.save_to_file(); + + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + format!("wallet `{}` with public address {} removed", + &wallet.0, &wallet.1), + ActionLevel::Warn, + )); + } } } } @@ -152,6 +178,13 @@ impl Accounts { let pair_signer = PairSigner::::new(pair); self.wallet_keys.push((wallet_name.to_string(), address, wallet_key.to_string(), pair_signer)); + + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + format!("read {} wallets from disk", self.wallet_keys.len()), + ActionLevel::Info, + )); + } } }, Err(_) => { @@ -165,10 +198,23 @@ impl Accounts { .try_into() .expect("stored seed is valid length; qed"); + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + "wallet read from the `/etc/ghost/wallet-key`".to_string(), + ActionLevel::Warn, + )); + } + let pair = Pair::from_seed(&seed); (pair, seed) } Err(_) => { + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + "no wallets found on disk, new wallet created".to_string(), + ActionLevel::Warn, + )); + } let (pair, seed) = Pair::generate(); (pair, seed) } diff --git a/src/components/wallet/add_address_book_record.rs b/src/components/wallet/add_address_book_record.rs new file mode 100644 index 0000000..c75b31d --- /dev/null +++ b/src/components/wallet/add_address_book_record.rs @@ -0,0 +1,202 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; +use color_eyre::Result; +use ratatui::{ + layout::{Position, Alignment, Constraint, Flex, Layout, Rect}, + widgets::{Block, Clear, Paragraph}, + Frame +}; +use tokio::sync::mpsc::UnboundedSender; + +use super::{Component, PartialComponent, CurrentTab}; +use crate::{ + widgets::{Input, InputRequest}, + action::Action, + config::Config, + palette::StylePalette, +}; + +#[derive(Debug)] +enum NameOrAddress { + Name, + Address, +} + +#[derive(Debug)] +pub struct AddAddressBookRecord { + is_active: bool, + name_or_address: NameOrAddress, + action_tx: Option>, + name: Input, + address: Input, + palette: StylePalette +} + +impl Default for AddAddressBookRecord { + fn default() -> Self { + Self::new() + } +} + +impl AddAddressBookRecord { + pub fn new() -> Self { + Self { + is_active: false, + name_or_address: NameOrAddress::Name, + action_tx: None, + name: Input::new(String::new()), + address: Input::new(String::new()), + palette: StylePalette::default(), + } + } +} + +impl AddAddressBookRecord { + fn submit_message(&mut self) { + match self.name_or_address { + NameOrAddress::Name => self.name_or_address = NameOrAddress::Address, + NameOrAddress::Address => if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::NewAddressBookRecord( + self.name.value().to_string(), + self.address.value().to_string())); + } + } + } + + fn enter_char(&mut self, new_char: char) { + match self.name_or_address { + NameOrAddress::Name => { + let _ = self.name.handle(InputRequest::InsertChar(new_char)); + }, + NameOrAddress::Address => { + let _ = self.address.handle(InputRequest::InsertChar(new_char)); + } + } + } + + fn delete_char(&mut self) { + match self.name_or_address { + NameOrAddress::Name => { + let _ = self.name.handle(InputRequest::DeletePrevChar); + }, + NameOrAddress::Address => { + let _ = self.address.handle(InputRequest::DeletePrevChar); + } + } + } + + fn move_cursor_right(&mut self) { + match self.name_or_address { + NameOrAddress::Name => { + let _ = self.name.handle(InputRequest::GoToNextChar); + }, + NameOrAddress::Address => { + let _ = self.address.handle(InputRequest::GoToNextChar); + } + } + } + + fn move_cursor_left(&mut self) { + match self.name_or_address { + NameOrAddress::Name => { + let _ = self.name.handle(InputRequest::GoToPrevChar); + }, + NameOrAddress::Address => { + let _ = self.address.handle(InputRequest::GoToPrevChar); + } + } + } +} + +impl PartialComponent for AddAddressBookRecord { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::AddAddressBookRecord => self.is_active = true, + _ => { + self.is_active = false; + self.name = Input::new(String::new()); + self.address = Input::new(String::new()); + self.name_or_address = NameOrAddress::Name; + }, + }; + } +} + +impl Component for AddAddressBookRecord { + 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_normal_border_style(style.get("normal_border_style").copied()); + self.palette.with_normal_title_style(style.get("normal_title_style").copied()); + } + Ok(()) + } + + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { + if self.is_active && key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Up => self.name_or_address = NameOrAddress::Name, + KeyCode::Down => self.name_or_address = NameOrAddress::Address, + 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::Esc => self.is_active = false, + _ => {}, + }; + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + if self.is_active { + let size = area.as_size(); + let name_area = Rect::new(size.width / 2, size.height / 2, 50, 3); + let address_area = Rect::new(size.width / 2, size.height / 2 + 3, 50, 3); + + let input_name = Paragraph::new(self.name.value()) + .block(Block::bordered() + .title_alignment(Alignment::Right) + .title("New name in Address Book")); + + let input_address = Paragraph::new(self.address.value()) + .block(Block::bordered() + .title_alignment(Alignment::Right) + .title("Address for new name")); + + let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center); + let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center); + + let [name_area] = v.areas(name_area); + let [name_area] = h.areas(name_area); + let [address_area] = v.areas(address_area); + let [address_area] = h.areas(address_area); + + frame.render_widget(Clear, name_area); + frame.render_widget(Clear, address_area); + frame.render_widget(input_name, name_area); + frame.render_widget(input_address, address_area); + + match self.name_or_address { + NameOrAddress::Name => { + frame.set_cursor_position(Position::new( + name_area.x + self.name.cursor() as u16 + 1, + name_area.y + 1 + )); + }, + NameOrAddress::Address => { + frame.set_cursor_position(Position::new( + address_area.x + self.address.cursor() as u16 + 1, + address_area.y + 1 + )); + } + } + } + Ok(()) + } +} diff --git a/src/components/wallet/address_book.rs b/src/components/wallet/address_book.rs index c2cc2e2..3983566 100644 --- a/src/components/wallet/address_book.rs +++ b/src/components/wallet/address_book.rs @@ -12,9 +12,11 @@ use ratatui::{ widgets::{Block, ScrollbarState, Row, Table, TableState}, Frame }; -use subxt::ext::sp_core::crypto::{Ss58Codec, Ss58AddressFormat, AccountId32}; +use subxt::ext::sp_core::crypto::{ByteArray, Ss58Codec, Ss58AddressFormat, AccountId32}; +use tokio::sync::mpsc::UnboundedSender; use super::{Component, PartialComponent, CurrentTab}; +use crate::types::ActionLevel; use crate::{ action::Action, config::Config, @@ -23,6 +25,7 @@ use crate::{ pub struct AddressBook { is_active: bool, + action_tx: Option>, address_book_file: PathBuf, address_book: Vec<(String, String, AccountId32, String)>, scroll_state: ScrollbarState, @@ -40,6 +43,7 @@ impl AddressBook { pub fn new() -> Self { Self { is_active: false, + action_tx: None, address_book_file: Default::default(), address_book: Vec::new(), scroll_state: ScrollbarState::new(0), @@ -76,6 +80,13 @@ impl AddressBook { let address = AccountId32::from(seed.clone()) .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); self.address_book.push((name.to_string(), address, account_id, seed_str.to_string())); + + } + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + format!("read {} records from address book", self.address_book.len()), + ActionLevel::Info, + )); } }, Err(_) => { @@ -108,12 +119,77 @@ impl AddressBook { .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); self.address_book.push((chad_info.0.to_string(), address, chad_account_id, chad_info.1.to_string())); }); + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + format!("address book is empty, filling it with giga chad boyz as default"), + ActionLevel::Warn, + )); + } } }; self.scroll_state = self.scroll_state.content_length(self.address_book.len()); Ok(()) } + fn rename_record(&mut self, new_name: String) { + if let Some(index) = self.table_state.selected() { + let old_name = self.address_book[index].0.clone(); + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + format!("record renamed from {} to {}", &old_name, &new_name), + ActionLevel::Info, + )); + } + self.address_book[index].0 = new_name; + self.save_to_file(); + } + } + + fn add_new_record(&mut self, name: String, address: String) { + match AccountId32::from_ss58check_with_version(&address) { + Ok((account_id, format)) => { + if format != Ss58AddressFormat::custom(1996) { + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + format!("provided public address for {} is not part of Casper/Ghost ecosystem", &address), + ActionLevel::Error, + )); + } + } + + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + format!("account {} with address {} added to address book", &name, &address), + ActionLevel::Info, + )); + } + + let seed_vec = account_id.to_raw_vec(); + let seed_str = hex::encode(seed_vec); + self.address_book.push((name, address, account_id, seed_str)); + self.save_to_file(); + self.last_row(); + }, + Err(_) => if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::WalletLog( + format!("provided account address {} is invalid", &address), + ActionLevel::Error, + )); + } + } + + } + + fn update_address_book_record(&mut self) { + if let Some(index) = self.table_state.selected() { + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::RenameAddressBookRecord( + self.address_book[index].0.clone() + )); + } + } + } + fn swap_up(&mut self) { if let Some(src_index) = self.table_state.selected() { let dst_index = src_index.saturating_sub(1); @@ -194,11 +270,7 @@ impl PartialComponent for AddressBook { fn set_active(&mut self, current_tab: CurrentTab) { match current_tab { CurrentTab::AddressBook => self.is_active = true, - _ => { - self.is_active = false; - self.table_state.select(None); - self.scroll_state = self.scroll_state.position(0); - } + _ => self.is_active = false, } } } @@ -223,6 +295,23 @@ impl Component for AddressBook { Ok(()) } + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.action_tx = Some(tx); + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::UpdateAddressBookRecord(new_name) => + self.rename_record(new_name), + Action::NewAddressBookRecord(name, address) => + self.add_new_record(name, address), + _ => {} + }; + Ok(None) + } + + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { match key.code { KeyCode::Up | KeyCode::Char('k') if self.is_active => self.previous_row(), @@ -232,6 +321,7 @@ impl Component for AddressBook { KeyCode::Char('K') if self.is_active => self.swap_up(), KeyCode::Char('J') if self.is_active => self.swap_down(), KeyCode::Char('D') if self.is_active => self.delete_row(), + KeyCode::Char('R') if self.is_active => self.update_address_book_record(), _ => {}, }; Ok(None) diff --git a/src/components/wallet/event_logs.rs b/src/components/wallet/event_logs.rs index 46eb9ac..678afb9 100644 --- a/src/components/wallet/event_logs.rs +++ b/src/components/wallet/event_logs.rs @@ -1,38 +1,65 @@ use color_eyre::Result; use ratatui::{ - layout::{Alignment, Rect}, - widgets::{Block, Paragraph, Wrap}, + layout::{Alignment, Constraint, Margin, Rect}, + style::{Color, Style}, + text::Text, + widgets::{ + Block, Padding, Cell, Row, Scrollbar, ScrollbarOrientation, + ScrollbarState, Table, TableState, + }, Frame }; use super::{Component, PartialComponent, CurrentTab}; use crate::{ + types::ActionLevel, action::Action, config::Config, palette::StylePalette, }; -#[derive(Debug)] +#[derive(Debug, Default)] +struct WalletLog { + time: chrono::DateTime, + level: ActionLevel, + message: String, +} + +#[derive(Debug, Default)] pub struct EventLogs { + is_active: bool, + scroll_state: ScrollbarState, + table_state: TableState, + logs: std::collections::VecDeque, palette: StylePalette } -impl Default for EventLogs { - fn default() -> Self { - Self::new() - } -} - impl EventLogs { - pub fn new() -> Self { - Self { - palette: StylePalette::default(), + const MAX_LOGS: usize = 50; + + fn add_new_log(&mut self, message: String, level: ActionLevel) { + self.logs.push_back(WalletLog { + time: chrono::Local::now(), + level, + message, + }); + + if self.logs.len() > Self::MAX_LOGS { + let _ = self.logs.pop_front(); } + + self.table_state.select(Some(self.logs.len() - 1)); + self.scroll_state = self.scroll_state.content_length(self.logs.len()); } } impl PartialComponent for EventLogs { - fn set_active(&mut self, _current_tab: CurrentTab) {} + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::EventLogs => self.is_active = true, + _ => self.is_active = false, + } + } } impl Component for EventLogs { @@ -50,6 +77,7 @@ impl Component for EventLogs { fn update(&mut self, action: Action) -> Result> { match action { + Action::WalletLog(message, level) => self.add_new_log(message, level), _ => {} }; Ok(None) @@ -58,18 +86,51 @@ impl Component for EventLogs { fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let [_, place] = super::wallet_layout(area); - let (border_style, border_type) = self.palette.create_border_style(false); - let paragraph = Paragraph::new("latest logs") - .block(Block::bordered() - .border_style(border_style) - .border_type(border_type) - .title_alignment(Alignment::Right) - .title_style(self.palette.create_title_style(false)) - .title("Latest Logs")) - .alignment(Alignment::Center) - .wrap(Wrap { trim: true }); + let (border_style, border_type) = self.palette.create_border_style(self.is_active); + let error_style = Style::new().fg(Color::Red); + let warn_style = Style::new().fg(Color::Yellow); + let info_style = Style::new().fg(Color::Green); - frame.render_widget(paragraph, place); + let table = Table::new( + self.logs + .iter() + .map(|log| { + let style = match log.level { + ActionLevel::Info => info_style, + ActionLevel::Warn => warn_style, + ActionLevel::Error => error_style, + }; + Row::new(vec![ + Cell::from(Text::from(log.time.format("%H:%M:%S").to_string()).style(style).alignment(Alignment::Left)), + Cell::from(Text::from(log.message.clone()).style(style).alignment(Alignment::Left)), + ]) + }), + [ + Constraint::Max(8), + Constraint::Min(0), + ], + ) + .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("Action Logs")); + + 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(()) } } diff --git a/src/components/wallet/mod.rs b/src/components/wallet/mod.rs index f6e3003..4b2396c 100644 --- a/src/components/wallet/mod.rs +++ b/src/components/wallet/mod.rs @@ -13,6 +13,8 @@ mod rename_account; mod event_logs; mod accounts; mod overview; +mod add_address_book_record; +mod rename_address_book_record; use balance::Balance; use tokio::sync::mpsc::UnboundedSender; @@ -23,6 +25,8 @@ use rename_account::RenameAccount; use event_logs::EventLogs; use accounts::Accounts; use overview::Overview; +use add_address_book_record::AddAddressBookRecord; +use rename_address_book_record::RenameAddressBookRecord; use super::Component; use crate::{action::Action, app::Mode, config::Config}; @@ -32,8 +36,11 @@ pub enum CurrentTab { Nothing, Accounts, AddressBook, + EventLogs, AddAccount, + AddAddressBookRecord, RenameAccount, + RenameAddressBookRecord, } pub trait PartialComponent: Component { @@ -60,6 +67,8 @@ impl Default for Wallet { Box::new(Transfer::default()), Box::new(AddAccount::default()), Box::new(RenameAccount::default()), + Box::new(AddAddressBookRecord::default()), + Box::new(RenameAddressBookRecord::default()), ], } } @@ -67,8 +76,10 @@ impl Default for Wallet { impl Wallet { fn move_left(&mut self) { - if let CurrentTab::AddressBook = self.current_tab { - self.current_tab = CurrentTab::Accounts; + match self.current_tab { + CurrentTab::EventLogs => self.current_tab = CurrentTab::AddressBook, + CurrentTab::AddressBook => self.current_tab = CurrentTab::Accounts, + _ => {} } } @@ -76,6 +87,7 @@ impl Wallet { match self.current_tab { CurrentTab::Nothing => self.current_tab = CurrentTab::Accounts, CurrentTab::Accounts => self.current_tab = CurrentTab::AddressBook, + CurrentTab::AddressBook => self.current_tab = CurrentTab::EventLogs, _ => {} } } @@ -103,13 +115,16 @@ impl Component for Wallet { match self.current_tab { // block the default key handle for popups - CurrentTab::AddAccount | CurrentTab::RenameAccount => match key.code { - KeyCode::Esc => { - self.current_tab = CurrentTab::Accounts; - for component in self.components.iter_mut() { - component.set_active(self.current_tab.clone()); - } - }, + CurrentTab::AddAccount | + CurrentTab::RenameAccount | + CurrentTab::RenameAddressBookRecord | + CurrentTab::AddAddressBookRecord => match key.code { + KeyCode::Esc => { + self.current_tab = CurrentTab::Accounts; + for component in self.components.iter_mut() { + component.set_active(self.current_tab.clone()); + } + }, _ => { for component in self.components.iter_mut() { component.handle_key_event(key)?; @@ -131,6 +146,12 @@ impl Component for Wallet { component.set_active(self.current_tab.clone()); } }, + KeyCode::Char('A') => { + self.current_tab = CurrentTab::AddAddressBookRecord; + for component in self.components.iter_mut() { + component.set_active(self.current_tab.clone()); + } + }, KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => { self.move_right(); for component in self.components.iter_mut() { @@ -158,7 +179,11 @@ impl Component for Wallet { Action::SetActiveScreen(Mode::Wallet) => self.is_active = true, Action::UpdateAccountName(_) | Action::NewAccount(_) => self.current_tab = CurrentTab::Accounts, + Action::UpdateAddressBookRecord(_) | Action::NewAddressBookRecord(_, _) => + self.current_tab = CurrentTab::AddressBook, Action::RenameAccount(_) => self.current_tab = CurrentTab::RenameAccount, + Action::RenameAddressBookRecord(_) => + self.current_tab = CurrentTab::RenameAddressBookRecord, _ => {} } for component in self.components.iter_mut() { diff --git a/src/components/wallet/rename_address_book_record.rs b/src/components/wallet/rename_address_book_record.rs new file mode 100644 index 0000000..db239f8 --- /dev/null +++ b/src/components/wallet/rename_address_book_record.rs @@ -0,0 +1,141 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; +use color_eyre::Result; +use ratatui::{ + layout::{Position, Alignment, Constraint, Flex, Layout, Rect}, + widgets::{Block, Clear, Paragraph}, + Frame +}; +use tokio::sync::mpsc::UnboundedSender; + +use super::{Component, PartialComponent, CurrentTab}; +use crate::{ + widgets::{Input, InputRequest}, + action::Action, + config::Config, + palette::StylePalette, +}; + +#[derive(Debug)] +pub struct RenameAddressBookRecord { + is_active: bool, + action_tx: Option>, + old_name: String, + name: Input, + palette: StylePalette +} + +impl Default for RenameAddressBookRecord { + fn default() -> Self { + Self::new() + } +} + +impl RenameAddressBookRecord { + pub fn new() -> Self { + Self { + is_active: false, + old_name: String::new(), + action_tx: None, + name: Input::new(String::new()), + palette: StylePalette::default(), + } + } +} + +impl RenameAddressBookRecord { + fn submit_message(&mut self) { + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::UpdateAddressBookRecord( + self.name.value().to_string())); + } + } + + fn enter_char(&mut self, new_char: char) { + let _ = self.name.handle(InputRequest::InsertChar(new_char)); + } + + fn delete_char(&mut self) { + let _ = self.name.handle(InputRequest::DeletePrevChar); + } + + fn move_cursor_right(&mut self) { + let _ = self.name.handle(InputRequest::GoToNextChar); + } + + fn move_cursor_left(&mut self) { + let _ = self.name.handle(InputRequest::GoToPrevChar); + } +} + +impl PartialComponent for RenameAddressBookRecord { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::RenameAddressBookRecord => self.is_active = true, + _ => { + self.is_active = false; + self.old_name = String::new(); + self.name = Input::new(String::new()); + } + }; + } +} + +impl Component for RenameAddressBookRecord { + 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()); + } + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::RenameAddressBookRecord(old_name) => self.old_name = old_name, + _ => {} + }; + Ok(None) + } + + 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::Esc => self.is_active = false, + _ => {}, + }; + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + if self.is_active { + let input = Paragraph::new(self.name.value()) + .block(Block::bordered() + .title_alignment(Alignment::Right) + .title(format!("New name for '{}'", self.old_name))); + let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center); + let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center); + let [area] = v.areas(area); + let [area] = h.areas(area); + + frame.render_widget(Clear, area); + frame.render_widget(input, area); + frame.set_cursor_position(Position::new( + area.x + self.name.cursor() as u16 + 1, + area.y + 1 + )); + } + Ok(()) + } +} diff --git a/src/types/log.rs b/src/types/log.rs new file mode 100644 index 0000000..3eca718 --- /dev/null +++ b/src/types/log.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; +use strum::Display; + +#[derive(Default, Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] +pub enum ActionLevel { + #[default] + Info, + Warn, + Error, +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 55a1ebb..24b17a0 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,5 +1,7 @@ mod era; mod extrinsics; +mod log; pub use extrinsics::CasperExtrinsicDetails; pub use era::EraInfo; +pub use log::ActionLevel;