added popup for the new wallet creation

Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
This commit is contained in:
Uncle Stretch 2024-12-02 17:51:37 +03:00
parent b3cebfa0a4
commit f5066926fd
Signed by: str3tch
GPG Key ID: 84F3190747EE79AA
16 changed files with 745 additions and 643 deletions

View File

@ -31,6 +31,7 @@ tokio-util = "0.7.12"
tracing = "0.1.37"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "serde"] }
unicode-width = "0.2.0"
[build-dependencies]
anyhow = "1.0.91"

View File

@ -27,6 +27,7 @@ pub enum Action {
UsedExplorerBlock(Option<u32>),
UsedExplorerLog(Option<String>),
UsedAccount(AccountId32),
NewAccount(String),
NewBestBlock(u32),
NewBestHash(H256),

View File

@ -212,8 +212,8 @@ impl Component for BlockExplorer {
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('K') if self.is_active => self.first_row(),
KeyCode::Char('J') if self.is_active => self.last_row(),
KeyCode::Char('g') if self.is_active => self.first_row(),
KeyCode::Char('G') if self.is_active => self.last_row(),
_ => {},
};

View File

@ -1,510 +0,0 @@
use std::collections::{HashMap, VecDeque};
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
layout::{Alignment, Rect},
prelude::*,
text::Line,
widgets::{Block, BorderType, Paragraph},
Frame
};
use subxt::ext::sp_core::crypto::{Ss58Codec, Ss58AddressFormat};
use subxt::utils::H256;
use codec::Decode;
use super::Component;
use crate::{
types::CasperExtrinsicDetails, CasperAccountId,
config::Config, action::Action, app::Mode, palette::StylePalette,
};
struct BlockInfo {
block_number: u32,
finalized: bool,
}
#[derive(Default)]
pub struct ExplorerBlocks {
blocks: VecDeque<BlockInfo>,
block_headers: HashMap<u32, H256>,
authors: HashMap<H256, CasperAccountId>,
extrinsics: HashMap<H256, Vec<CasperExtrinsicDetails>>,
palette: StylePalette,
current_block_digit_length: u32,
is_active: bool,
used_paragraph_index: usize,
used_block_number: Option<u32>,
used_ext_index: Option<(H256, usize)>,
}
impl ExplorerBlocks {
const MAX_BLOCKS: usize = 50;
const LENGTH_OF_BLOCK_HASH: u16 = 13;
const LENGTH_OF_ADDRESS: u16 = 49;
const TOTAL_OFFSETS: u16 = 18;
fn update_block_author(
&mut self,
hash: H256,
maybe_author: Option<CasperAccountId>,
) -> Result<()> {
if let Some(author) = maybe_author {
self.authors.insert(hash, author);
}
Ok(())
}
fn update_latest_block_info(
&mut self,
hash: H256,
block_number: u32,
extrinsics: Vec<CasperExtrinsicDetails>,
) -> Result<()> {
let front_block_number = match self.blocks.front() {
Some(block_info) => block_info.block_number,
None => 0,
};
if front_block_number < block_number {
self.blocks.push_front(BlockInfo {
block_number,
finalized: false,
});
self.extrinsics.insert(hash, extrinsics);
self.block_headers.insert(block_number, hash);
let block_length = block_number.checked_ilog10().unwrap_or(0) + 1;
if self.current_block_digit_length < block_length {
self.current_block_digit_length = block_length;
}
if self.blocks.len() > Self::MAX_BLOCKS {
if let Some(block) = self.blocks.pop_back() {
if let Some(hash) = self.block_headers.remove(&block.block_number) {
self.extrinsics.remove(&hash);
self.authors.remove(&hash);
}
}
}
}
Ok(())
}
fn update_finalized_block_info(
&mut self,
_hash: H256,
block_number: u32,
_extrinsics: Vec<CasperExtrinsicDetails>,
) -> Result<()> {
for idx in 0..self.blocks.len() {
if self.blocks[idx].finalized { break; }
else if self.blocks[idx].block_number > block_number { continue; }
else { self.blocks[idx].finalized = true; }
}
Ok(())
}
fn prepare_block_line_info(&self, current_block: &BlockInfo, width: u16) -> Line {
let block_number_length = self
.current_block_digit_length
.max(current_block.block_number.checked_ilog10().unwrap_or(0) + 1) as usize;
let free_space = width
.saturating_sub(block_number_length as u16)
.saturating_sub(Self::TOTAL_OFFSETS);
let default_hash = H256::repeat_byte(69u8);
let hash = self
.block_headers
.get(&current_block.block_number)
.unwrap_or(&default_hash);
let author = self
.authors
.get(&hash)
.map_or(String::from("..."), |author| {
let extended_author = CasperAccountId::decode(&mut author.as_ref())
.expect("author should be valid AccountId32; qed");
let account_id = subxt::ext::sp_core::crypto::AccountId32::from(extended_author.0);
account_id.to_ss58check_with_version(Ss58AddressFormat::custom(1996))
});
if free_space < Self::LENGTH_OF_BLOCK_HASH + Self::LENGTH_OF_ADDRESS {
let len_for_author = free_space * Self::LENGTH_OF_ADDRESS / (Self::LENGTH_OF_BLOCK_HASH + Self::LENGTH_OF_ADDRESS);
if &author == "..." {
Line::raw(format!("{:^left$}| {} | {:^right$}",
current_block.block_number,
hash.to_string(),
author,
left=block_number_length,
right=(len_for_author + 2) as usize))
} else {
Line::raw(format!("{} | {} | {}",
current_block.block_number,
hash.to_string(),
format!("{}...", &author[..(len_for_author) as usize])))
}
} else {
let total_space_used = block_number_length as u16 + Self::LENGTH_OF_BLOCK_HASH + Self::LENGTH_OF_ADDRESS;
let str_length = width.saturating_sub(2).saturating_sub(total_space_used) as usize / 3;
Line::raw(format!("{:^length_block_number$}|{:^length_hash$}|{:^length_author$}",
current_block.block_number, hash.to_string(), author,
length_block_number=str_length+block_number_length,
length_hash=str_length+(Self::LENGTH_OF_BLOCK_HASH as usize),
length_author=str_length+(Self::LENGTH_OF_ADDRESS as usize)))
}
}
fn prepare_block_lines(&mut self, rect: Rect) -> Vec<Line> {
let width = rect.as_size().width;
let total_length = rect.as_size().height as usize - 2;
let mut items = Vec::new();
let start_index = match self.used_block_number {
Some(used_block) if total_length < self.blocks.len() => {
self.blocks
.iter()
.position(|info| info.block_number == used_block)
.unwrap_or_default()
.saturating_add(1)
.saturating_sub(total_length)
},
_ => 0,
};
let normal_style = self.palette.create_text_style(false);
let active_style = self.palette.create_text_style(true);
let finalized_style = self.palette.create_highlight_style();
for (idx, current_block_info) in self.blocks.iter().skip(start_index).enumerate() {
if idx == total_length { break; }
let style = match self.used_block_number {
Some(used_block) if current_block_info.block_number == used_block => active_style,
_ => if current_block_info.finalized { finalized_style } else { normal_style }
};
items.push(self.prepare_block_line_info(&current_block_info, width).style(style));
}
items
}
fn prepare_ext_line_info(
&self,
index: usize,
width: u16,
pallet_name: &str,
variant_name: &str,
hash: &str,
) -> Line {
let index_length = 4; // always 4, two digits and two spaces
let hash_length = Self::LENGTH_OF_BLOCK_HASH as usize + 2;
let pallet_name_length = pallet_name.len();
let variant_name_length = variant_name.len();
let offset_variant = (width as usize)
.saturating_sub(index_length)
.saturating_sub(pallet_name_length)
.saturating_sub(variant_name_length)
.saturating_sub(hash_length)
.saturating_sub(2) / 2;
let offset_pallet = if offset_variant % 2 == 0 {
offset_variant
} else {
offset_variant + 1
};
Line::from(format!("{:^index_length$}{:>pallet_name_offset$}::{:<variant_name_offset$}{:^hash_length$}",
index, pallet_name, variant_name, hash,
pallet_name_offset=offset_pallet+pallet_name_length,
variant_name_offset=offset_variant+variant_name_length))
}
fn prepare_ext_lines(&mut self, rect: Rect) -> Vec<Line> {
let width = rect.as_size().width;
let mut total_length = rect.as_size().height - 2;
let mut items = Vec::new();
if let Some(used_block_number) = self.used_block_number {
let default_hash = H256::repeat_byte(69u8);
let hash = self.block_headers
.get(&used_block_number)
.unwrap_or(&default_hash);
let normal_style = self.palette.create_text_style(false);
let active_style = self.palette.create_text_style(true);
if let Some(exts) = self.extrinsics.get(&hash) {
for (index, ext) in exts.iter().enumerate() {
if total_length == 0 { break; }
let style = if let Some((_, used_ext_index)) = self.used_ext_index {
if index == used_ext_index { active_style } else { normal_style }
} else { normal_style };
items.push(self.prepare_ext_line_info(
index,
width.saturating_sub(2),
&ext.pallet_name,
&ext.variant_name,
&ext.hash.to_string()).style(style));
total_length -= 1;
}
}
}
items
}
fn prepare_event_lines(&mut self, rect: Rect) -> Line {
let _width = rect.as_size().width;
match self.used_ext_index {
Some((header, used_index)) if self.extrinsics.get(&header).is_some() => {
let exts = self.extrinsics
.get(&header)
.expect("extrinsics should exists, checked before");
let details = exts
.get(used_index)
.map_or(Vec::new(), |ext| ext.field_bytes.clone());
Line::from(format!("{}", hex::encode(&details)))
},
_ => Line::from(""),
}.style(self.palette.create_text_style(false))
}
fn move_right(&mut self) {
let new_index = self.used_paragraph_index + 1;
if new_index < 2 {
self.used_paragraph_index = new_index;
}
}
fn move_left(&mut self) {
self.used_paragraph_index = self
.used_paragraph_index
.saturating_sub(1);
self.used_ext_index = None;
}
fn move_down(&mut self) {
if self.used_paragraph_index == 0 {
self.move_down_blocks();
} else {
self.move_down_extrinsics();
}
}
fn move_up(&mut self) {
if self.used_paragraph_index == 0 {
self.move_up_blocks();
} else {
self.move_up_extrinsics();
}
}
fn move_up_extrinsics(&mut self) {
match &self.used_ext_index {
Some((header, used_index)) => {
let new_index = used_index.saturating_sub(1);
if let Some(exts) = self.extrinsics.get(header) {
if exts.get(new_index).is_some() {
self.used_ext_index = Some((*header, new_index));
}
}
},
None => {
self.used_ext_index = self.used_block_number
.map(|block_number| {
let header = self.block_headers
.get(&block_number)
.expect("header exists for each block number; qed");
self.extrinsics.get(&header).map(|_| (*header, 0usize))
})
.flatten()
}
}
}
fn move_up_blocks(&mut self) {
self.used_block_number = match &self.used_block_number {
Some(block_number) => {
Some(self.blocks
.iter()
.find(|info| info.block_number == block_number + 1)
.map(|info| info.block_number)
.unwrap_or(*block_number))
},
None => self.blocks.front().map(|info| info.block_number),
}
}
fn move_down_extrinsics(&mut self) {
match &self.used_ext_index {
Some((header, used_index)) => {
let new_index = used_index + 1;
if let Some(exts) = self.extrinsics.get(&header) {
if new_index < exts.len() && exts.get(new_index).is_some() {
self.used_ext_index = Some((*header, new_index));
}
}
},
None => {
self.used_ext_index = self.used_block_number
.map(|block_number| {
let header = self.block_headers
.get(&block_number)
.expect("header exists for each block number; qed");
self.extrinsics.get(&header).map(|_| (*header, 0usize))
})
.flatten()
}
}
}
fn move_down_blocks(&mut self) {
self.used_block_number = match &self.used_block_number {
Some(block_number) => {
Some(self.blocks
.iter()
.find(|info| info.block_number == block_number.saturating_sub(1))
.map(|info| info.block_number)
.unwrap_or(*block_number))
},
None => {
self.blocks.front().map(|info| info.block_number)
}
}
}
fn set_active(&mut self) -> Result<()> {
self.is_active = true;
Ok(())
}
fn unset_active(&mut self) -> Result<()> {
self.is_active = false;
Ok(())
}
fn prepare_blocks_paragraph(
&mut self,
place: Rect,
border_style: Color,
border_type: BorderType,
) -> Paragraph {
let title_style = self
.palette
.create_title_style(self.is_active && self.used_paragraph_index == 0);
Paragraph::new(self.prepare_block_lines(place))
.block(Block::bordered()
.border_style(border_style)
.border_type(border_type)
.title_alignment(Alignment::Right)
.title_style(title_style)
.title("Blocks"))
.alignment(Alignment::Center)
}
fn prepare_extrinsics_paragraph(
&mut self,
place: Rect,
border_style: Color,
border_type: BorderType,
) -> Paragraph {
let title_style = self
.palette
.create_title_style(self.is_active && self.used_paragraph_index == 1);
Paragraph::new(self.prepare_ext_lines(place))
.block(Block::bordered()
.border_style(border_style)
.border_type(border_type)
.title_alignment(Alignment::Right)
.title_style(title_style)
.title("Transactions"))
.alignment(Alignment::Center)
}
fn prepare_event_paragraph(
&mut self,
place: Rect,
border_style: Color,
border_type: BorderType,
) -> Paragraph {
let title_style = self.palette.create_title_style(false);
Paragraph::new(self.prepare_event_lines(place))
.block(Block::bordered()
.border_style(border_style)
.border_type(border_type)
.title_alignment(Alignment::Right)
.title_style(title_style)
.title("Events"))
.alignment(Alignment::Center)
}
}
impl Component for ExplorerBlocks {
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());
}
Ok(())
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
match key.code {
KeyCode::Char('k') | KeyCode::Up if self.is_active => self.move_up(),
KeyCode::Char('j') | KeyCode::Down if self.is_active => self.move_down(),
KeyCode::Char('l') | KeyCode::Right if self.is_active => self.move_right(),
KeyCode::Char('h') | KeyCode::Left if self.is_active => self.move_left(),
KeyCode::Esc => {
self.used_block_number = None;
self.used_ext_index = None;
self.used_paragraph_index = 0;
},
_ => {},
};
Ok(None)
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::BestBlockInformation(hash, block_number, extrinsics) => self.update_latest_block_info(hash, block_number, extrinsics)?,
Action::FinalizedBlockInformation(hash, block_number, extrinsics) => self.update_finalized_block_info(hash, block_number, extrinsics)?,
Action::SetBlockAuthor(hash, maybe_author) => self.update_block_author(hash, maybe_author)?,
Action::SetMode(Mode::ExplorerActive) if !self.is_active => self.set_active()?,
Action::SetMode(_) if self.is_active => self.unset_active()?,
_ => {}
};
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [blocks_place, ext_place] = super::explorer_scrollbars_layout(area);
let [_, _, event_place] = super::explorer_layout(area);
let (border_style_block, border_type_block) = self.palette.create_border_style(self.is_active && self.used_paragraph_index == 0);
let (border_style_extrinsics, border_type_extrinsics) = self.palette.create_border_style(self.is_active && self.used_paragraph_index == 1);
let (border_style_event, border_type_event) = self.palette.create_border_style(self.is_active && self.used_paragraph_index == 2);
frame.render_widget(self.prepare_blocks_paragraph(blocks_place, border_style_block, border_type_block), blocks_place);
frame.render_widget(self.prepare_extrinsics_paragraph(ext_place, border_style_extrinsics, border_type_extrinsics), ext_place);
frame.render_widget(self.prepare_event_paragraph(event_place, border_style_event, border_type_event), event_place);
Ok(())
}
}

View File

@ -208,8 +208,8 @@ impl Component for ExtrinsicExplorer {
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('K') if self.is_active => self.first_row(),
KeyCode::Char('J') if self.is_active => self.last_row(),
KeyCode::Char('g') if self.is_active => self.first_row(),
KeyCode::Char('G') if self.is_active => self.last_row(),
_ => {},
};

View File

@ -34,11 +34,12 @@ use crate::{
pub struct Accounts {
is_active: bool,
wallet_keys_file: PathBuf,
action_tx: Option<UnboundedSender<Action>>,
palette: StylePalette,
scroll_state: ScrollbarState,
table_state: TableState,
wallet_keys: Vec<(String, String, PairSigner<CasperConfig, Pair>)>,
wallet_keys: Vec<(String, String, String, PairSigner<CasperConfig, Pair>)>,
}
impl Default for Accounts {
@ -51,6 +52,7 @@ impl Accounts {
pub fn new() -> Self {
Self {
is_active: false,
wallet_keys_file: Default::default(),
action_tx: None,
scroll_state: ScrollbarState::new(0),
table_state: TableState::new(),
@ -59,6 +61,56 @@ impl Accounts {
}
}
fn create_new_account(&mut self, name: String) {
let (pair, seed) = Pair::generate();
let secret_seed = hex::encode(seed);
let pair_signer = PairSigner::<CasperConfig, Pair>::new(pair);
let address = AccountId32::from(seed.clone())
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
self.wallet_keys.push((name, address, secret_seed, pair_signer));
self.save_to_file();
}
fn swap_up(&mut self) {
if let Some(src_index) = self.table_state.selected() {
let dst_index = src_index.saturating_sub(1);
if src_index > dst_index {
self.wallet_keys.swap(src_index, dst_index);
self.previous_row();
self.save_to_file();
}
}
}
fn swap_down(&mut self) {
if let Some(src_index) = self.table_state.selected() {
let dst_index = src_index + 1;
if dst_index < self.wallet_keys.len() {
self.wallet_keys.swap(src_index, dst_index);
self.next_row();
self.save_to_file();
}
}
}
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);
self.previous_row();
self.save_to_file();
}
}
}
fn save_to_file(&mut self) {
let mut file = File::create(&self.wallet_keys_file).unwrap();
for wallet in self.wallet_keys.iter() {
writeln!(file, "{}:0x{}", wallet.0, &wallet.2).unwrap();
}
}
fn read_or_create(&mut self, file_path: &PathBuf) -> Result<()> {
assert!(self.wallet_keys.len() == 0, "wallet_keys already exists");
match File::open(file_path) {
@ -81,7 +133,7 @@ impl Accounts {
let address = AccountId32::from(seed.clone())
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
self.wallet_keys.push((wallet_name.to_string(), address, pair_signer));
self.wallet_keys.push((wallet_name.to_string(), address, wallet_key.to_string(), pair_signer));
}
},
Err(_) => {
@ -113,7 +165,7 @@ impl Accounts {
let address = AccountId32::from(seed.clone())
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
self.wallet_keys.push(("ghostie".to_string(), address, pair_signer));
self.wallet_keys.push(("ghostie".to_string(), address, secret_seed, pair_signer));
}
};
self.table_state.select(Some(0));
@ -124,7 +176,7 @@ impl Accounts {
fn send_wallet_change(&mut self, index: usize) {
if let Some(action_tx) = &self.action_tx {
let (_, _, pair) = &self.wallet_keys[index];
let (_, _, _, pair) = &self.wallet_keys[index];
let _ = action_tx.send(Action::UsedAccount(pair.account_id().clone()));
}
}
@ -210,11 +262,13 @@ impl Component for Accounts {
let mut wallet_keys_file = config.config.data_dir;
wallet_keys_file.push("wallet-keys");
self.read_or_create(&wallet_keys_file)?;
self.wallet_keys_file = wallet_keys_file;
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::NewAccount(name) => self.create_new_account(name),
_ => {}
};
Ok(None)
@ -224,9 +278,11 @@ impl Component for Accounts {
match key.code {
KeyCode::Up | KeyCode::Char('k') if self.is_active => self.previous_row(),
KeyCode::Down | KeyCode::Char('j') if self.is_active => self.next_row(),
KeyCode::Char('K') if self.is_active => self.first_row(),
KeyCode::Char('J') if self.is_active => self.last_row(),
// TODO: swap on alt+j or G/gg to bottom and up
KeyCode::Char('K') if self.is_active => self.swap_up(),
KeyCode::Char('J') if self.is_active => self.swap_down(),
KeyCode::Char('g') if self.is_active => self.first_row(),
KeyCode::Char('G') if self.is_active => self.last_row(),
KeyCode::Char('D') if self.is_active => self.delete_row(),
_ => {},
};
Ok(None)

View File

@ -1,82 +0,0 @@
use crossterm::event::{KeyEvent, KeyCode};
use color_eyre::Result;
use ratatui::{
layout::{Constraint, Flex, Layout, Rect},
widgets::{Block, Clear},
Frame
};
use super::{Component, PartialComponent, CurrentTab};
use crate::{
action::Action,
config::Config,
palette::StylePalette,
};
#[derive(Debug)]
pub struct AddAddress {
is_shown: bool,
palette: StylePalette
}
impl Default for AddAddress {
fn default() -> Self {
Self::new()
}
}
impl AddAddress {
pub fn new() -> Self {
Self {
is_shown: false,
palette: StylePalette::default(),
}
}
}
impl PartialComponent for AddAddress {
fn set_active(&mut self, _current_tab: CurrentTab) {}
}
impl Component for AddAddress {
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());
}
Ok(())
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
match key.code {
KeyCode::Char('a') => self.is_shown = true,
KeyCode::Char(' ') => self.is_shown = false,
_ => {},
};
Ok(None)
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
_ => {}
};
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
if self.is_shown {
let block = Block::bordered().title("Transfer");
let v = Layout::vertical([Constraint::Min(55)]).flex(Flex::Center);
let h = Layout::horizontal([Constraint::Min(10)]).flex(Flex::Center);
let [area] = v.areas(area);
let [area] = h.areas(area);
frame.render_widget(Clear, area);
frame.render_widget(block, area);
}
Ok(())
}
}

View File

@ -0,0 +1,126 @@
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 AddAccount {
is_active: bool,
action_tx: Option<UnboundedSender<Action>>,
name: Input,
palette: StylePalette
}
impl Default for AddAccount {
fn default() -> Self {
Self::new()
}
}
impl AddAccount {
pub fn new() -> Self {
Self {
is_active: false,
action_tx: None,
name: Input::new(String::new()),
palette: StylePalette::default(),
}
}
}
impl AddAccount {
fn submit_message(&mut self) {
if let Some(action_tx) = &self.action_tx {
let _ = action_tx.send(Action::NewAccount(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 AddAccount {
fn set_active(&mut self, current_tab: CurrentTab) {
match current_tab {
CurrentTab::AddAccount => self.is_active = true,
_ => self.is_active = false,
}
}
}
impl Component for AddAccount {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> 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<Option<Action>> {
if 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("New wallet 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(())
}
}

View File

@ -23,7 +23,8 @@ use crate::{
pub struct AddressBook {
is_active: bool,
address_book: Vec<(String, String, AccountId32)>,
address_book_file: PathBuf,
address_book: Vec<(String, String, AccountId32, String)>,
scroll_state: ScrollbarState,
table_state: TableState,
palette: StylePalette,
@ -39,6 +40,7 @@ impl AddressBook {
pub fn new() -> Self {
Self {
is_active: false,
address_book_file: Default::default(),
address_book: Vec::new(),
scroll_state: ScrollbarState::new(0),
table_state: TableState::new(),
@ -46,6 +48,13 @@ impl AddressBook {
}
}
fn save_to_file(&mut self) {
let mut file = File::create(&self.address_book_file).unwrap();
for wallet in self.address_book.iter() {
writeln!(file, "{}:0x{}", wallet.0, &wallet.3).unwrap();
}
}
fn read_or_create(&mut self, file_path: &PathBuf) -> Result<()> {
assert!(self.address_book.len() == 0, "address_book already exists");
match File::open(file_path) {
@ -55,9 +64,9 @@ impl AddressBook {
let line = line?.replace("\n", "");
let line_split_at = line.find(":").unwrap_or(line.len());
let (name, seed) = line.split_at(line_split_at);
let seed = &seed[3..];
let seed_str = &seed[3..];
let seed: [u8; 32] = hex::decode(seed)
let seed: [u8; 32] = hex::decode(seed_str)
.expect("stored seed is valid hex string; qed")
.as_slice()
.try_into()
@ -66,7 +75,7 @@ impl AddressBook {
let account_id = AccountId32::from(seed);
let address = AccountId32::from(seed.clone())
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
self.address_book.push((name.to_string(), address, account_id));
self.address_book.push((name.to_string(), address, account_id, seed_str.to_string()));
}
},
Err(_) => {
@ -97,12 +106,11 @@ impl AddressBook {
let chad_account_id = AccountId32::from(chad_account_id);
let address = AccountId32::from(chad_account_id.clone())
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
self.address_book.push((chad_info.0.to_string(), address, chad_account_id));
self.address_book.push((chad_info.0.to_string(), address, chad_account_id, chad_info.1.to_string()));
});
}
};
self.scroll_state = self.scroll_state.content_length(self.address_book.len());
Ok(())
}
@ -112,6 +120,7 @@ impl AddressBook {
if src_index > dst_index {
self.address_book.swap(src_index, dst_index);
self.previous_row();
self.save_to_file();
}
}
}
@ -122,6 +131,7 @@ impl AddressBook {
if dst_index < self.address_book.len() {
self.address_book.swap(src_index, dst_index);
self.next_row();
self.save_to_file();
}
}
}
@ -130,6 +140,7 @@ impl AddressBook {
if let Some(index) = self.table_state.selected() {
let _ = self.address_book.remove(index);
self.previous_row();
self.save_to_file();
}
}
@ -208,6 +219,7 @@ impl Component for AddressBook {
let mut address_book_file = config.config.data_dir;
address_book_file.push("address-book");
self.read_or_create(&address_book_file)?;
self.address_book_file = address_book_file;
Ok(())
}
@ -215,12 +227,11 @@ impl Component for AddressBook {
match key.code {
KeyCode::Up | KeyCode::Char('k') if self.is_active => self.previous_row(),
KeyCode::Down | KeyCode::Char('j') if self.is_active => self.next_row(),
// TODO: swap on alt+j or G/gg to bottom and up
KeyCode::Char('g') if self.is_active => self.first_row(),
KeyCode::Char('G') if self.is_active => self.last_row(),
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('D') if self.is_active => self.delete_row(),
_ => {},
};
Ok(None)

View File

@ -65,7 +65,7 @@ impl Component for EventLogs {
.border_type(border_type)
.title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style(false))
.title("Logs"))
.title("Latest Logs"))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });

View File

@ -8,7 +8,7 @@ use ratatui::{
mod balance;
mod transfer;
mod address_book;
mod add;
mod add_account;
mod event_logs;
mod accounts;
mod overview;
@ -17,7 +17,7 @@ use balance::Balance;
use tokio::sync::mpsc::UnboundedSender;
use transfer::Transfer;
use address_book::AddressBook;
use add::AddAddress;
use add_account::AddAccount;
use event_logs::EventLogs;
use accounts::Accounts;
use overview::Overview;
@ -30,6 +30,7 @@ pub enum CurrentTab {
Nothing,
Accounts,
AddressBook,
AddAccount,
}
pub trait PartialComponent: Component {
@ -54,7 +55,7 @@ impl Default for Wallet {
Box::new(AddressBook::default()),
Box::new(EventLogs::default()),
Box::new(Transfer::default()),
Box::new(AddAddress::default()),
Box::new(AddAccount::default()),
],
}
}
@ -99,6 +100,21 @@ impl Component for Wallet {
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
if self.is_active {
if self.current_tab == CurrentTab::AddAccount {
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)?;
}
}
}
} else {
match key.code {
KeyCode::Esc => {
self.is_active = false;
@ -108,6 +124,12 @@ impl Component for Wallet {
}
return Ok(Some(Action::SetActiveScreen(Mode::Menu)));
},
KeyCode::Char('w') => {
self.current_tab = CurrentTab::AddAccount;
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() {
@ -127,6 +149,7 @@ impl Component for Wallet {
},
}
}
}
Ok(None)
}
@ -138,6 +161,12 @@ impl Component for Wallet {
component.set_active(self.current_tab.clone());
}
}
if let Action::NewAccount(_) = action {
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.update(action.clone())?;
}

View File

@ -1,5 +1,5 @@
use color_eyre::Result;
use crossterm::event::{KeyEvent, KeyCode};
use crossterm::event::KeyEvent;
use ratatui::{
layout::{Constraint, Flex, Layout, Rect},
widgets::{Block, Clear},
@ -54,8 +54,6 @@ impl Component for Transfer {
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
match key.code {
KeyCode::Char('t') => self.is_shown = true,
KeyCode::Char(' ') => self.is_shown = false,
_ => {},
};
Ok(None)

View File

@ -0,0 +1,111 @@
use super::{Input, InputRequest, StateChanged};
use ratatui::crossterm::event::{
Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
};
use ratatui::crossterm::{
cursor::MoveTo,
queue,
style::{Attribute as CAttribute, Print, SetAttribute},
};
use std::io::{Result, Write};
pub fn to_input_request(evt: &CrosstermEvent) -> Option<InputRequest> {
use InputRequest::*;
use KeyCode::*;
match evt {
CrosstermEvent::Key(KeyEvent {
code,
modifiers,
kind,
state: _,
}) if *kind == KeyEventKind::Press || *kind == KeyEventKind::Repeat => {
match (*code, *modifiers) {
(Backspace, KeyModifiers::NONE) | (Char('h'), KeyModifiers::CONTROL) => {
Some(DeletePrevChar)
}
(Delete, KeyModifiers::NONE) => Some(DeleteNextChar),
(Tab, KeyModifiers::NONE) => None,
(Left, KeyModifiers::NONE) | (Char('b'), KeyModifiers::CONTROL) => {
Some(GoToPrevChar)
}
(Left, KeyModifiers::CONTROL) | (Char('b'), KeyModifiers::META) => {
Some(GoToPrevWord)
}
(Right, KeyModifiers::NONE) | (Char('f'), KeyModifiers::CONTROL) => {
Some(GoToNextChar)
}
(Right, KeyModifiers::CONTROL) | (Char('f'), KeyModifiers::META) => {
Some(GoToNextWord)
}
(Char('u'), KeyModifiers::CONTROL) => Some(DeleteLine),
(Char('w'), KeyModifiers::CONTROL)
| (Char('d'), KeyModifiers::META)
| (Backspace, KeyModifiers::META)
| (Backspace, KeyModifiers::ALT) => Some(DeletePrevWord),
(Delete, KeyModifiers::CONTROL) => Some(DeleteNextWord),
(Char('k'), KeyModifiers::CONTROL) => Some(DeleteTillEnd),
(Char('a'), KeyModifiers::CONTROL) | (Home, KeyModifiers::NONE) => {
Some(GoToStart)
}
(Char('e'), KeyModifiers::CONTROL) | (End, KeyModifiers::NONE) => {
Some(GoToEnd)
}
(Char(c), KeyModifiers::NONE) => Some(InsertChar(c)),
(Char(c), KeyModifiers::SHIFT) => Some(InsertChar(c)),
(_, _) => None,
}
}
_ => None,
}
}
pub fn write<W: Write>(
stdout: &mut W,
value: &str,
cursor: usize,
(x, y): (u16, u16),
width: u16,
) -> Result<()> {
queue!(stdout, MoveTo(x, y), SetAttribute(CAttribute::NoReverse))?;
let val_width = width.max(1) as usize - 1;
let len = value.chars().count();
let start = (len.max(val_width) - val_width).min(cursor);
let mut chars = value.chars().skip(start);
let mut i = start;
while i < cursor {
i += 1;
let c = chars.next().unwrap_or(' ');
queue!(stdout, Print(c))?;
}
i += 1;
let c = chars.next().unwrap_or(' ');
queue!(
stdout,
SetAttribute(CAttribute::Reverse),
Print(c),
SetAttribute(CAttribute::NoReverse)
)?;
while i <= start + val_width {
i += 1;
let c = chars.next().unwrap_or(' ');
queue!(stdout, Print(c))?;
}
Ok(())
}
pub trait EventHandler {
fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged>;
}
impl EventHandler for Input {
fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged> {
to_input_request(evt).and_then(|req| self.handle(req))
}
}

355
src/widgets/input/input.rs Normal file
View File

@ -0,0 +1,355 @@
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[derive(serde::Serialize, serde::Deserialize)]
pub enum InputRequest {
SetCursor(usize),
InsertChar(char),
GoToPrevChar,
GoToNextChar,
GoToPrevWord,
GoToNextWord,
GoToStart,
GoToEnd,
DeletePrevChar,
DeleteNextChar,
DeletePrevWord,
DeleteNextWord,
DeleteLine,
DeleteTillEnd,
}
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct StateChanged {
pub value: bool,
pub cursor: bool,
}
pub type InputResponse = Option<StateChanged>;
#[derive(Default, Debug, Clone)]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Input {
value: String,
cursor: usize,
}
impl Input {
pub fn new(value: String) -> Self {
let len = value.chars().count();
Self { value, cursor: len }
}
pub fn with_value(mut self, value: String) -> Self {
self.cursor = value.chars().count();
self.value = value;
self
}
pub fn with_cursor(mut self, cursor: usize) -> Self {
self.cursor = cursor.min(self.value.chars().count());
self
}
pub fn reset(&mut self) {
self.cursor = Default::default();
self.value = Default::default();
}
pub fn handle(&mut self, req: InputRequest) -> InputResponse {
use InputRequest::*;
match req {
SetCursor(pos) => {
let pos = pos.min(self.value.chars().count());
if self.cursor == pos {
None
} else {
self.cursor = pos;
Some(StateChanged {
value: false,
cursor: true,
})
}
}
InsertChar(c) => {
if self.cursor == self.value.chars().count() {
self.value.push(c);
} else {
self.value = self
.value
.chars()
.take(self.cursor)
.chain(
std::iter::once(c)
.chain(self.value.chars().skip(self.cursor)),
)
.collect();
}
self.cursor += 1;
Some(StateChanged {
value: true,
cursor: true,
})
}
DeletePrevChar => {
if self.cursor == 0 {
None
} else {
self.cursor -= 1;
self.value = self
.value
.chars()
.enumerate()
.filter(|(i, _)| i != &self.cursor)
.map(|(_, c)| c)
.collect();
Some(StateChanged {
value: true,
cursor: true,
})
}
}
DeleteNextChar => {
if self.cursor == self.value.chars().count() {
None
} else {
self.value = self
.value
.chars()
.enumerate()
.filter(|(i, _)| i != &self.cursor)
.map(|(_, c)| c)
.collect();
Some(StateChanged {
value: true,
cursor: false,
})
}
}
GoToPrevChar => {
if self.cursor == 0 {
None
} else {
self.cursor -= 1;
Some(StateChanged {
value: false,
cursor: true,
})
}
}
GoToPrevWord => {
if self.cursor == 0 {
None
} else {
self.cursor = self
.value
.chars()
.rev()
.skip(self.value.chars().count().max(self.cursor) - self.cursor)
.skip_while(|c| !c.is_alphanumeric())
.skip_while(|c| c.is_alphanumeric())
.count();
Some(StateChanged {
value: false,
cursor: true,
})
}
}
GoToNextChar => {
if self.cursor == self.value.chars().count() {
None
} else {
self.cursor += 1;
Some(StateChanged {
value: false,
cursor: true,
})
}
}
GoToNextWord => {
if self.cursor == self.value.chars().count() {
None
} else {
self.cursor = self
.value
.chars()
.enumerate()
.skip(self.cursor)
.skip_while(|(_, c)| c.is_alphanumeric())
.find(|(_, c)| c.is_alphanumeric())
.map(|(i, _)| i)
.unwrap_or_else(|| self.value.chars().count());
Some(StateChanged {
value: false,
cursor: true,
})
}
}
DeleteLine => {
if self.value.is_empty() {
None
} else {
let cursor = self.cursor;
self.value = "".into();
self.cursor = 0;
Some(StateChanged {
value: true,
cursor: self.cursor == cursor,
})
}
}
DeletePrevWord => {
if self.cursor == 0 {
None
} else {
let remaining = self.value.chars().skip(self.cursor);
let rev = self
.value
.chars()
.rev()
.skip(self.value.chars().count().max(self.cursor) - self.cursor)
.skip_while(|c| !c.is_alphanumeric())
.skip_while(|c| c.is_alphanumeric())
.collect::<Vec<char>>();
let rev_len = rev.len();
self.value = rev.into_iter().rev().chain(remaining).collect();
self.cursor = rev_len;
Some(StateChanged {
value: true,
cursor: true,
})
}
}
DeleteNextWord => {
if self.cursor == self.value.chars().count() {
None
} else {
self.value = self
.value
.chars()
.take(self.cursor)
.chain(
self.value
.chars()
.skip(self.cursor)
.skip_while(|c| c.is_alphanumeric())
.skip_while(|c| !c.is_alphanumeric()),
)
.collect();
Some(StateChanged {
value: true,
cursor: false,
})
}
}
GoToStart => {
if self.cursor == 0 {
None
} else {
self.cursor = 0;
Some(StateChanged {
value: false,
cursor: true,
})
}
}
GoToEnd => {
let count = self.value.chars().count();
if self.cursor == count {
None
} else {
self.cursor = count;
Some(StateChanged {
value: false,
cursor: true,
})
}
}
DeleteTillEnd => {
self.value = self.value.chars().take(self.cursor).collect();
Some(StateChanged {
value: true,
cursor: false,
})
}
}
}
pub fn value(&self) -> &str {
self.value.as_str()
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn visual_cursor(&self) -> usize {
if self.cursor == 0 {
return 0;
}
unicode_width::UnicodeWidthStr::width(unsafe {
self.value.get_unchecked(
0..self
.value
.char_indices()
.nth(self.cursor)
.map_or_else(|| self.value.len(), |(index, _)| index),
)
})
}
pub fn visual_scroll(&self, width: usize) -> usize {
let scroll = (self.visual_cursor()).max(width) - width;
let mut uscroll = 0;
let mut chars = self.value().chars();
while uscroll < scroll {
match chars.next() {
Some(c) => {
uscroll += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
}
None => break,
}
}
uscroll
}
}
impl From<Input> for String {
fn from(input: Input) -> Self {
input.value
}
}
impl From<String> for Input {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&str> for Input {
fn from(value: &str) -> Self {
Self::new(value.into())
}
}
impl std::fmt::Display for Input {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.value.fmt(f)
}
}

4
src/widgets/input/mod.rs Normal file
View File

@ -0,0 +1,4 @@
mod input;
//pub mod backend;
pub use input::{Input, InputRequest};

View File

@ -2,11 +2,13 @@ mod dot_spinner;
mod ogham;
mod vertical_block;
mod big_text;
mod input;
pub use dot_spinner::DotSpinner;
pub use vertical_block::VerticalBlocks;
pub use ogham::OghamCenter;
pub use big_text::BigText;
pub use big_text::PixelSize;
pub use input::{Input, InputRequest};
const CYCLE: i64 = 1500;