added popup for the new wallet creation
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
This commit is contained in:
parent
b3cebfa0a4
commit
f5066926fd
@ -31,6 +31,7 @@ tokio-util = "0.7.12"
|
|||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "serde"] }
|
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "serde"] }
|
||||||
|
unicode-width = "0.2.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
anyhow = "1.0.91"
|
anyhow = "1.0.91"
|
||||||
|
@ -27,6 +27,7 @@ pub enum Action {
|
|||||||
UsedExplorerBlock(Option<u32>),
|
UsedExplorerBlock(Option<u32>),
|
||||||
UsedExplorerLog(Option<String>),
|
UsedExplorerLog(Option<String>),
|
||||||
UsedAccount(AccountId32),
|
UsedAccount(AccountId32),
|
||||||
|
NewAccount(String),
|
||||||
|
|
||||||
NewBestBlock(u32),
|
NewBestBlock(u32),
|
||||||
NewBestHash(H256),
|
NewBestHash(H256),
|
||||||
|
@ -212,8 +212,8 @@ impl Component for BlockExplorer {
|
|||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('k') | KeyCode::Up if self.is_active => self.previous_row(),
|
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('j') | KeyCode::Down if self.is_active => self.next_row(),
|
||||||
KeyCode::Char('K') if self.is_active => self.first_row(),
|
KeyCode::Char('g') 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.last_row(),
|
||||||
_ => {},
|
_ => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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(¤t_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(¤t_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(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -208,8 +208,8 @@ impl Component for ExtrinsicExplorer {
|
|||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('k') | KeyCode::Up if self.is_active => self.previous_row(),
|
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('j') | KeyCode::Down if self.is_active => self.next_row(),
|
||||||
KeyCode::Char('K') if self.is_active => self.first_row(),
|
KeyCode::Char('g') 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.last_row(),
|
||||||
_ => {},
|
_ => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -34,11 +34,12 @@ use crate::{
|
|||||||
|
|
||||||
pub struct Accounts {
|
pub struct Accounts {
|
||||||
is_active: bool,
|
is_active: bool,
|
||||||
|
wallet_keys_file: PathBuf,
|
||||||
action_tx: Option<UnboundedSender<Action>>,
|
action_tx: Option<UnboundedSender<Action>>,
|
||||||
palette: StylePalette,
|
palette: StylePalette,
|
||||||
scroll_state: ScrollbarState,
|
scroll_state: ScrollbarState,
|
||||||
table_state: TableState,
|
table_state: TableState,
|
||||||
wallet_keys: Vec<(String, String, PairSigner<CasperConfig, Pair>)>,
|
wallet_keys: Vec<(String, String, String, PairSigner<CasperConfig, Pair>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Accounts {
|
impl Default for Accounts {
|
||||||
@ -51,6 +52,7 @@ impl Accounts {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
is_active: false,
|
is_active: false,
|
||||||
|
wallet_keys_file: Default::default(),
|
||||||
action_tx: None,
|
action_tx: None,
|
||||||
scroll_state: ScrollbarState::new(0),
|
scroll_state: ScrollbarState::new(0),
|
||||||
table_state: TableState::new(),
|
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<()> {
|
fn read_or_create(&mut self, file_path: &PathBuf) -> Result<()> {
|
||||||
assert!(self.wallet_keys.len() == 0, "wallet_keys already exists");
|
assert!(self.wallet_keys.len() == 0, "wallet_keys already exists");
|
||||||
match File::open(file_path) {
|
match File::open(file_path) {
|
||||||
@ -81,7 +133,7 @@ impl Accounts {
|
|||||||
let address = AccountId32::from(seed.clone())
|
let address = AccountId32::from(seed.clone())
|
||||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
.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(_) => {
|
Err(_) => {
|
||||||
@ -113,7 +165,7 @@ impl Accounts {
|
|||||||
let address = AccountId32::from(seed.clone())
|
let address = AccountId32::from(seed.clone())
|
||||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
.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));
|
self.table_state.select(Some(0));
|
||||||
@ -124,7 +176,7 @@ impl Accounts {
|
|||||||
|
|
||||||
fn send_wallet_change(&mut self, index: usize) {
|
fn send_wallet_change(&mut self, index: usize) {
|
||||||
if let Some(action_tx) = &self.action_tx {
|
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()));
|
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;
|
let mut wallet_keys_file = config.config.data_dir;
|
||||||
wallet_keys_file.push("wallet-keys");
|
wallet_keys_file.push("wallet-keys");
|
||||||
self.read_or_create(&wallet_keys_file)?;
|
self.read_or_create(&wallet_keys_file)?;
|
||||||
|
self.wallet_keys_file = wallet_keys_file;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||||
match action {
|
match action {
|
||||||
|
Action::NewAccount(name) => self.create_new_account(name),
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@ -224,9 +278,11 @@ impl Component for Accounts {
|
|||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up | KeyCode::Char('k') if self.is_active => self.previous_row(),
|
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::Down | KeyCode::Char('j') if self.is_active => self.next_row(),
|
||||||
KeyCode::Char('K') if self.is_active => self.first_row(),
|
KeyCode::Char('K') if self.is_active => self.swap_up(),
|
||||||
KeyCode::Char('J') if self.is_active => self.last_row(),
|
KeyCode::Char('J') if self.is_active => self.swap_down(),
|
||||||
// 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('D') if self.is_active => self.delete_row(),
|
||||||
_ => {},
|
_ => {},
|
||||||
};
|
};
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
126
src/components/wallet/add_account.rs
Normal file
126
src/components/wallet/add_account.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,8 @@ use crate::{
|
|||||||
|
|
||||||
pub struct AddressBook {
|
pub struct AddressBook {
|
||||||
is_active: bool,
|
is_active: bool,
|
||||||
address_book: Vec<(String, String, AccountId32)>,
|
address_book_file: PathBuf,
|
||||||
|
address_book: Vec<(String, String, AccountId32, String)>,
|
||||||
scroll_state: ScrollbarState,
|
scroll_state: ScrollbarState,
|
||||||
table_state: TableState,
|
table_state: TableState,
|
||||||
palette: StylePalette,
|
palette: StylePalette,
|
||||||
@ -39,6 +40,7 @@ impl AddressBook {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
is_active: false,
|
is_active: false,
|
||||||
|
address_book_file: Default::default(),
|
||||||
address_book: Vec::new(),
|
address_book: Vec::new(),
|
||||||
scroll_state: ScrollbarState::new(0),
|
scroll_state: ScrollbarState::new(0),
|
||||||
table_state: TableState::new(),
|
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<()> {
|
fn read_or_create(&mut self, file_path: &PathBuf) -> Result<()> {
|
||||||
assert!(self.address_book.len() == 0, "address_book already exists");
|
assert!(self.address_book.len() == 0, "address_book already exists");
|
||||||
match File::open(file_path) {
|
match File::open(file_path) {
|
||||||
@ -55,9 +64,9 @@ impl AddressBook {
|
|||||||
let line = line?.replace("\n", "");
|
let line = line?.replace("\n", "");
|
||||||
let line_split_at = line.find(":").unwrap_or(line.len());
|
let line_split_at = line.find(":").unwrap_or(line.len());
|
||||||
let (name, seed) = line.split_at(line_split_at);
|
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")
|
.expect("stored seed is valid hex string; qed")
|
||||||
.as_slice()
|
.as_slice()
|
||||||
.try_into()
|
.try_into()
|
||||||
@ -66,7 +75,7 @@ impl AddressBook {
|
|||||||
let account_id = AccountId32::from(seed);
|
let account_id = AccountId32::from(seed);
|
||||||
let address = AccountId32::from(seed.clone())
|
let address = AccountId32::from(seed.clone())
|
||||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
.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(_) => {
|
Err(_) => {
|
||||||
@ -97,12 +106,11 @@ impl AddressBook {
|
|||||||
let chad_account_id = AccountId32::from(chad_account_id);
|
let chad_account_id = AccountId32::from(chad_account_id);
|
||||||
let address = AccountId32::from(chad_account_id.clone())
|
let address = AccountId32::from(chad_account_id.clone())
|
||||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
.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());
|
self.scroll_state = self.scroll_state.content_length(self.address_book.len());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +120,7 @@ impl AddressBook {
|
|||||||
if src_index > dst_index {
|
if src_index > dst_index {
|
||||||
self.address_book.swap(src_index, dst_index);
|
self.address_book.swap(src_index, dst_index);
|
||||||
self.previous_row();
|
self.previous_row();
|
||||||
|
self.save_to_file();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,6 +131,7 @@ impl AddressBook {
|
|||||||
if dst_index < self.address_book.len() {
|
if dst_index < self.address_book.len() {
|
||||||
self.address_book.swap(src_index, dst_index);
|
self.address_book.swap(src_index, dst_index);
|
||||||
self.next_row();
|
self.next_row();
|
||||||
|
self.save_to_file();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,6 +140,7 @@ impl AddressBook {
|
|||||||
if let Some(index) = self.table_state.selected() {
|
if let Some(index) = self.table_state.selected() {
|
||||||
let _ = self.address_book.remove(index);
|
let _ = self.address_book.remove(index);
|
||||||
self.previous_row();
|
self.previous_row();
|
||||||
|
self.save_to_file();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,6 +219,7 @@ impl Component for AddressBook {
|
|||||||
let mut address_book_file = config.config.data_dir;
|
let mut address_book_file = config.config.data_dir;
|
||||||
address_book_file.push("address-book");
|
address_book_file.push("address-book");
|
||||||
self.read_or_create(&address_book_file)?;
|
self.read_or_create(&address_book_file)?;
|
||||||
|
self.address_book_file = address_book_file;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,12 +227,11 @@ impl Component for AddressBook {
|
|||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up | KeyCode::Char('k') if self.is_active => self.previous_row(),
|
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::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.first_row(),
|
||||||
KeyCode::Char('G') if self.is_active => self.last_row(),
|
KeyCode::Char('G') if self.is_active => self.last_row(),
|
||||||
KeyCode::Char('K') if self.is_active => self.swap_up(),
|
KeyCode::Char('K') if self.is_active => self.swap_up(),
|
||||||
KeyCode::Char('J') if self.is_active => self.swap_down(),
|
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)
|
Ok(None)
|
||||||
|
@ -65,7 +65,7 @@ impl Component for EventLogs {
|
|||||||
.border_type(border_type)
|
.border_type(border_type)
|
||||||
.title_alignment(Alignment::Right)
|
.title_alignment(Alignment::Right)
|
||||||
.title_style(self.palette.create_title_style(false))
|
.title_style(self.palette.create_title_style(false))
|
||||||
.title("Logs"))
|
.title("Latest Logs"))
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.wrap(Wrap { trim: true });
|
.wrap(Wrap { trim: true });
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ use ratatui::{
|
|||||||
mod balance;
|
mod balance;
|
||||||
mod transfer;
|
mod transfer;
|
||||||
mod address_book;
|
mod address_book;
|
||||||
mod add;
|
mod add_account;
|
||||||
mod event_logs;
|
mod event_logs;
|
||||||
mod accounts;
|
mod accounts;
|
||||||
mod overview;
|
mod overview;
|
||||||
@ -17,7 +17,7 @@ use balance::Balance;
|
|||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
use transfer::Transfer;
|
use transfer::Transfer;
|
||||||
use address_book::AddressBook;
|
use address_book::AddressBook;
|
||||||
use add::AddAddress;
|
use add_account::AddAccount;
|
||||||
use event_logs::EventLogs;
|
use event_logs::EventLogs;
|
||||||
use accounts::Accounts;
|
use accounts::Accounts;
|
||||||
use overview::Overview;
|
use overview::Overview;
|
||||||
@ -30,6 +30,7 @@ pub enum CurrentTab {
|
|||||||
Nothing,
|
Nothing,
|
||||||
Accounts,
|
Accounts,
|
||||||
AddressBook,
|
AddressBook,
|
||||||
|
AddAccount,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PartialComponent: Component {
|
pub trait PartialComponent: Component {
|
||||||
@ -54,7 +55,7 @@ impl Default for Wallet {
|
|||||||
Box::new(AddressBook::default()),
|
Box::new(AddressBook::default()),
|
||||||
Box::new(EventLogs::default()),
|
Box::new(EventLogs::default()),
|
||||||
Box::new(Transfer::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>> {
|
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||||
if self.is_active {
|
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 {
|
match key.code {
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
self.is_active = false;
|
self.is_active = false;
|
||||||
@ -108,6 +124,12 @@ impl Component for Wallet {
|
|||||||
}
|
}
|
||||||
return Ok(Some(Action::SetActiveScreen(Mode::Menu)));
|
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 => {
|
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => {
|
||||||
self.move_right();
|
self.move_right();
|
||||||
for component in self.components.iter_mut() {
|
for component in self.components.iter_mut() {
|
||||||
@ -127,6 +149,7 @@ impl Component for Wallet {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +161,12 @@ impl Component for Wallet {
|
|||||||
component.set_active(self.current_tab.clone());
|
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() {
|
for component in self.components.iter_mut() {
|
||||||
component.update(action.clone())?;
|
component.update(action.clone())?;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use crossterm::event::{KeyEvent, KeyCode};
|
use crossterm::event::KeyEvent;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Flex, Layout, Rect},
|
layout::{Constraint, Flex, Layout, Rect},
|
||||||
widgets::{Block, Clear},
|
widgets::{Block, Clear},
|
||||||
@ -54,8 +54,6 @@ impl Component for Transfer {
|
|||||||
|
|
||||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('t') => self.is_shown = true,
|
|
||||||
KeyCode::Char(' ') => self.is_shown = false,
|
|
||||||
_ => {},
|
_ => {},
|
||||||
};
|
};
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
111
src/widgets/input/backend.rs
Normal file
111
src/widgets/input/backend.rs
Normal 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
355
src/widgets/input/input.rs
Normal 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
4
src/widgets/input/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
mod input;
|
||||||
|
|
||||||
|
//pub mod backend;
|
||||||
|
pub use input::{Input, InputRequest};
|
@ -2,11 +2,13 @@ mod dot_spinner;
|
|||||||
mod ogham;
|
mod ogham;
|
||||||
mod vertical_block;
|
mod vertical_block;
|
||||||
mod big_text;
|
mod big_text;
|
||||||
|
mod input;
|
||||||
|
|
||||||
pub use dot_spinner::DotSpinner;
|
pub use dot_spinner::DotSpinner;
|
||||||
pub use vertical_block::VerticalBlocks;
|
pub use vertical_block::VerticalBlocks;
|
||||||
pub use ogham::OghamCenter;
|
pub use ogham::OghamCenter;
|
||||||
pub use big_text::BigText;
|
pub use big_text::BigText;
|
||||||
pub use big_text::PixelSize;
|
pub use big_text::PixelSize;
|
||||||
|
pub use input::{Input, InputRequest};
|
||||||
|
|
||||||
const CYCLE: i64 = 1500;
|
const CYCLE: i64 = 1500;
|
||||||
|
Loading…
Reference in New Issue
Block a user