initial ghost-eye sketches with basic functionality
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
This commit is contained in:
commit
b1d7add8a3
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
46
Cargo.toml
Normal file
46
Cargo.toml
Normal file
@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "ghost-eye"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
better-panic = "0.3.0"
|
||||
chrono = "0.4.38"
|
||||
clap = { version = "4.5.20", features = ["derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] }
|
||||
codec = { version = "3.6.12", package = "parity-scale-codec" }
|
||||
color-eyre = "0.6.3"
|
||||
config = "0.14.0"
|
||||
crossterm = { version = "0.28.1", features = ["serde", "event-stream"] }
|
||||
derive_builder = "0.20.2"
|
||||
derive_deref = "1.1.1"
|
||||
directories = "5.0.1"
|
||||
font8x8 = "0.3.1"
|
||||
futures = "0.3.31"
|
||||
hex = "0.4.3"
|
||||
human-panic = "2.0.2"
|
||||
json5 = "0.4.1"
|
||||
lazy_static = "1.5.0"
|
||||
libc = "0.2.159"
|
||||
log = "0.4.22"
|
||||
once_cell = "1.20.2"
|
||||
pretty_assertions = "1.4.1"
|
||||
rand = "0.8.5"
|
||||
ratatui = { version = "0.28.1", features = ["serde", "macros"] }
|
||||
reqwest = { version = "0.12.8", features = ["json"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = "1.0.128"
|
||||
signal-hook = "0.3.17"
|
||||
sp-consensus-babe = "0.40.0"
|
||||
sp-core = "34.0.0"
|
||||
sp-runtime = "39.0.2"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
tokio = { version = "1.40.0", features = ["full"] }
|
||||
tokio-util = "0.7.12"
|
||||
tracing = "0.1.37"
|
||||
tracing-error = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "serde"] }
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.91"
|
||||
vergen-gix = { version = "1.0.2", features = ["build", "cargo"] }
|
13
build.rs
Normal file
13
build.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use anyhow::Result;
|
||||
use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let build = BuildBuilder::all_build()?;
|
||||
let gix = GixBuilder::all_git()?;
|
||||
let cargo = CargoBuilder::all_cargo()?;
|
||||
Emitter::default()
|
||||
.add_instructions(&build)?
|
||||
.add_instructions(&gix)?
|
||||
.add_instructions(&cargo)?
|
||||
.emit()
|
||||
}
|
16
config/config.json5
Normal file
16
config/config.json5
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"keybindings": {
|
||||
"Explorer": {
|
||||
"<q>": "Quit",
|
||||
"<Ctrl-d>": "Quit",
|
||||
"<Ctrl-c>": "Quit",
|
||||
"<Ctrl-z>": "Suspend",
|
||||
},
|
||||
"Empty": {
|
||||
"<q>": "Quit",
|
||||
"<Ctrl-d>": "Quit",
|
||||
"<Ctrl-c>": "Quit",
|
||||
"<Ctrl-z>": "Suspend",
|
||||
}
|
||||
}
|
||||
}
|
49
src/action.rs
Normal file
49
src/action.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::Display;
|
||||
|
||||
use crate::types::{
|
||||
block::BlockInfo,
|
||||
era::EraInfo,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
|
||||
pub enum Action {
|
||||
Tick,
|
||||
Render,
|
||||
Resize(u16, u16),
|
||||
Suspend,
|
||||
Resume,
|
||||
Quit,
|
||||
ClearScreen,
|
||||
Error(String),
|
||||
Help,
|
||||
|
||||
SetMode(crate::app::Mode),
|
||||
//MenuItem(bool),
|
||||
|
||||
GetNodeName,
|
||||
GetSyncState,
|
||||
GetGenesisHash,
|
||||
GetChainName,
|
||||
GetNodeVersion,
|
||||
GetPendingExtrinsics,
|
||||
|
||||
GetLatestBlock,
|
||||
GetFinalizedBlock,
|
||||
GetActiveEra,
|
||||
GetEpoch,
|
||||
GetValidators,
|
||||
|
||||
SetNodeName(Option<String>),
|
||||
SetSyncState(Option<u32>, bool, bool),
|
||||
SetGenesisHash(Option<String>),
|
||||
SetChainName(Option<String>),
|
||||
SetNodeVersion(Option<String>),
|
||||
|
||||
SetLatestBlock(String, BlockInfo),
|
||||
SetFinalizedBlock(String, BlockInfo),
|
||||
SetActiveEra(EraInfo),
|
||||
SetEpoch(u64, u64),
|
||||
SetValidators(Vec<String>),
|
||||
SetPendingExtrinsicsLength(usize),
|
||||
}
|
259
src/app.rs
Normal file
259
src/app.rs
Normal file
@ -0,0 +1,259 @@
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::prelude::Rect;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tokio::sync::mpsc::{UnboundedSender, UnboundedReceiver};
|
||||
use std::sync::mpsc::Sender;
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
tui::{Event, Tui},
|
||||
components::{
|
||||
menu::Menu, version::Version, explorer::Explorer, empty::Empty,
|
||||
health::Health, fps::FpsCounter, Component},
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Mode {
|
||||
Explorer(#[serde(skip)] bool),
|
||||
Empty(#[serde(skip)] bool),
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Self::Explorer(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
network_tx: Sender<Action>,
|
||||
action_tx: UnboundedSender<Action>,
|
||||
action_rx: UnboundedReceiver<Action>,
|
||||
frame_rate: f32,
|
||||
tick_rate: f32,
|
||||
mouse: bool,
|
||||
paste: bool,
|
||||
config: Config,
|
||||
components: Vec<Box<dyn Component>>,
|
||||
should_quite: bool,
|
||||
should_suspend: bool,
|
||||
mode: Mode,
|
||||
last_tick_key_events: Vec<KeyEvent>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(
|
||||
network_tx: Sender<Action>,
|
||||
action_tx: UnboundedSender<Action>,
|
||||
action_rx: UnboundedReceiver<Action>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
network_tx,
|
||||
action_tx,
|
||||
action_rx,
|
||||
frame_rate: 4.0,
|
||||
tick_rate: 60.0,
|
||||
mouse: false,
|
||||
paste: false,
|
||||
config: Config::new()?,
|
||||
components: vec![
|
||||
Box::new(Menu::default()),
|
||||
Box::new(FpsCounter::default()),
|
||||
Box::new(Health::default()),
|
||||
Box::new(Version::default()),
|
||||
Box::new(Explorer::default()),
|
||||
Box::new(Empty::default()),
|
||||
],
|
||||
should_quite: false,
|
||||
should_suspend: false,
|
||||
mode: Mode::default(),
|
||||
last_tick_key_events: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_frame_rate(mut self, frame_rate: f32) -> Self {
|
||||
self.frame_rate = frame_rate;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_tick_rate(mut self, tick_rate: f32) -> Self {
|
||||
self.tick_rate = tick_rate;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_mouse(mut self, mouse: bool) -> Self {
|
||||
self.mouse = mouse;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_paste(mut self, paste: bool) -> Self {
|
||||
self.paste = paste;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
let mut tui = Tui::new()?;
|
||||
tui.enter()?;
|
||||
|
||||
for component in self.components.iter_mut() {
|
||||
component.register_action_handler(self.action_tx.clone())?;
|
||||
}
|
||||
for component in self.components.iter_mut() {
|
||||
component.register_config_handler(self.config.clone())?;
|
||||
}
|
||||
for component in self.components.iter_mut() {
|
||||
component.init(tui.size()?)?;
|
||||
}
|
||||
|
||||
let action_tx = self.action_tx.clone();
|
||||
loop {
|
||||
self.handle_events(&mut tui).await?;
|
||||
self.handle_actions(&mut tui)?;
|
||||
if self.should_suspend {
|
||||
tui.suspend()?;
|
||||
action_tx.send(Action::Resume)?;
|
||||
action_tx.send(Action::ClearScreen)?;
|
||||
tui.enter()?;
|
||||
} else if self.should_quite {
|
||||
tui.stop()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
tui.exit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> {
|
||||
let Some(event) = tui.next_event().await else {
|
||||
return Ok(());
|
||||
};
|
||||
let action_tx = self.action_tx.clone();
|
||||
match event {
|
||||
Event::Quit => action_tx.send(Action::Quit)?,
|
||||
Event::Tick => action_tx.send(Action::Tick)?,
|
||||
Event::Render => action_tx.send(Action::Render)?,
|
||||
Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?,
|
||||
Event::Key(key) => self.handle_key_event(key)?,
|
||||
Event::Node => self.trigger_node_events()?,
|
||||
Event::FastNode => self.trigger_node_fast_events()?,
|
||||
Event::Runtime => self.trigger_runtime_events()?,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for component in self.components.iter_mut() {
|
||||
if let Some(action) = component.handle_events(Some(event.clone()))? {
|
||||
action_tx.send(action)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn trigger_node_fast_events(&mut self) -> Result<()> {
|
||||
self.network_tx.send(Action::GetPendingExtrinsics)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn trigger_node_events(&mut self) -> Result<()> {
|
||||
self.network_tx.send(Action::GetNodeName)?;
|
||||
self.network_tx.send(Action::GetSyncState)?;
|
||||
self.network_tx.send(Action::GetGenesisHash)?;
|
||||
self.network_tx.send(Action::GetChainName)?;
|
||||
self.network_tx.send(Action::GetNodeVersion)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn trigger_runtime_events(&mut self) -> Result<()> {
|
||||
self.network_tx.send(Action::GetLatestBlock)?;
|
||||
self.network_tx.send(Action::GetFinalizedBlock)?;
|
||||
self.network_tx.send(Action::GetActiveEra)?;
|
||||
self.network_tx.send(Action::GetEpoch)?;
|
||||
self.network_tx.send(Action::GetValidators)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
|
||||
let action_tx = self.action_tx.clone();
|
||||
let Some(keymap) = self.config.keybindings.get(&self.mode) else {
|
||||
return Ok(());
|
||||
};
|
||||
match keymap.get(&vec![key]) {
|
||||
Some(action) => {
|
||||
info!("got action: {action:?}");
|
||||
action_tx.send(action.clone())?;
|
||||
}
|
||||
_ => {
|
||||
self.last_tick_key_events.push(key);
|
||||
if let Some(action) = keymap.get(&self.last_tick_key_events) {
|
||||
info!("got action: {action:?}");
|
||||
action_tx.send(action.clone())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_actions(&mut self, tui: &mut Tui) -> Result<()> {
|
||||
while let Ok(action) = self.action_rx.try_recv() {
|
||||
match action {
|
||||
Action::Tick => { self.last_tick_key_events.drain(..); },
|
||||
Action::Quit => self.should_quite = true,
|
||||
Action::Suspend => self.should_suspend = true,
|
||||
Action::Resume => self.should_suspend = false,
|
||||
Action::ClearScreen => tui.terminal.clear()?,
|
||||
Action::Resize(x, y) => self.handle_resize(tui, x, y)?,
|
||||
Action::Render => self.render(tui)?,
|
||||
Action::SetMode(mode) => self.mode = mode,
|
||||
_ => {}
|
||||
}
|
||||
for component in self.components.iter_mut() {
|
||||
if let Some(action) = component.update(action.clone())? {
|
||||
self.action_tx.send(action)?
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> {
|
||||
tui.resize(Rect::new(0, 0, w, h))?;
|
||||
self.render(tui)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&mut self, tui: &mut Tui) -> Result<()> {
|
||||
tui.draw(|frame| {
|
||||
for component in self.components.iter_mut().take(4) {
|
||||
if let Err(err) = (*component).draw(frame, frame.area()) {
|
||||
let _ = self
|
||||
.action_tx
|
||||
.send(Action::Error(format!("failed to draw: {:?}", err)));
|
||||
}
|
||||
}
|
||||
|
||||
match self.mode {
|
||||
Mode::Explorer(_) => {
|
||||
if let Some(component) = self.components.get_mut(4) {
|
||||
if let Err(err) = component.draw(frame, frame.area()) {
|
||||
let _ = self
|
||||
.action_tx
|
||||
.send(Action::Error(format!("failed to draw: {:?}", err)));
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
if let Some(component) = self.components.last_mut() {
|
||||
if let Err(err) = component.draw(frame, frame.area()) {
|
||||
let _ = self
|
||||
.action_tx
|
||||
.send(Action::Error(format!("failed to draw: {:?}", err)));
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
57
src/cli.rs
Normal file
57
src/cli.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use clap::Parser;
|
||||
|
||||
use crate::config::{get_config_dir, get_data_dir};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version = version(), about)]
|
||||
pub struct Cli {
|
||||
/// Tick rate, i.e. number of ticks per second
|
||||
#[arg(short, long, value_name = "FLOAT", default_value_t = 4.0)]
|
||||
pub tick_rate: f32,
|
||||
|
||||
/// Frame rate, i.e. number of frames per second
|
||||
#[arg(short, long, value_name = "FLOAT", default_value_t = 60.0)]
|
||||
pub frame_rate: f32,
|
||||
|
||||
/// RPC Endpoint to the nodes JSON RPC
|
||||
#[arg(short, long, default_value_t = String::from("http://localhost:9945"))]
|
||||
pub rpc_endpoint: String,
|
||||
|
||||
/// Request timeout in seconds
|
||||
#[arg(short = 'i', long, default_value_t = 2)]
|
||||
pub timeout: u64,
|
||||
|
||||
/// Mouse usage during execution, EXPERIMENTAL AND NOT RECOMENDED
|
||||
#[arg(short, long)]
|
||||
pub mouse_needed: bool,
|
||||
|
||||
/// Paste available during execution, EXPERIMENTAL AND NOT RECOMENDED
|
||||
#[arg(short, long)]
|
||||
pub paste_needed: bool,
|
||||
}
|
||||
|
||||
const VERSION_MESSAGE: &str = concat!(
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
"-",
|
||||
env!("VERGEN_GIT_DESCRIBE"),
|
||||
" (",
|
||||
env!("VERGEN_BUILD_DATE"),
|
||||
")",
|
||||
);
|
||||
|
||||
fn version() -> String {
|
||||
let author = clap::crate_authors!();
|
||||
|
||||
let config_dir_path = get_config_dir().display().to_string();
|
||||
let data_dir_path = get_data_dir().display().to_string();
|
||||
|
||||
format!(
|
||||
"\
|
||||
{VERSION_MESSAGE}
|
||||
|
||||
Authors: {author}
|
||||
|
||||
Config directory: {config_dir_path}
|
||||
Data directory: {data_dir_path}"
|
||||
)
|
||||
}
|
125
src/components/empty.rs
Normal file
125
src/components/empty.rs
Normal file
@ -0,0 +1,125 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
text::Line,
|
||||
widgets::{Block, Padding, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
use crate::{
|
||||
action::Action, app::Mode
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Empty {
|
||||
is_active: bool,
|
||||
}
|
||||
|
||||
impl Empty {
|
||||
fn prepare_inactive_text(&self) -> Vec<Line> {
|
||||
vec![
|
||||
Line::from(" ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢐⣤⣼⣿⣿⣿⣿⣿⣿⣷⣶⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣢⣾⣿⣿⣿⣿⣿⣿⣿⣿⣯⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⡟⠛⢻⠉⡉⠍⠁⠁⠀⠈⠙⢻⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠏⢠⢀⡼⡄⠃⠤⠀⠀⠀⠀⠀⡐⠸⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⢰⣸⡎⣀⣷⣤⣶⣶⣶⣦⡀⠀⠈⠓⢿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣇⣤⣯⣿⣿⣿⣿⣿⣿⣿⣭⣯⡆⠀⠀⠘⣿⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡿⣻⣿⣿⣼⠀⢹⣿⣿⣿⣿⡿⠋⠁⠀⠀⠀⢘⣿⠙⠡⢽⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢙⣛⣿⣯⠏⠀⢀⣿⣿⣿⣯⣠⡀⠀⠀⠀⢀⣾⡏⠒⢻⣷⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⡟⢘⣏⣺⣤⣬⣭⣼⣿⣿⣯⡉⢻⣦⣌⣦⣾⣿⣿⡚⠾⠿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⢹⡼⣿⣿⢼⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⡿⣿⢿⡟⢳⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢳⣿⣧⡞⣻⣩⣽⡽⣿⣿⣿⣿⣿⣿⣿⣿⡟⣠⣿⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡿⣇⣬⣿⣿⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣿⡿⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡛⣿⣄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢼⡃⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋⠁⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠓⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠈⢳⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⠿⢿⡟⠻⢿⣿⡷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣍⠓⠲⠤⢤⣄⡀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣇⠀⠈⣿⡏⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠈⠈⢯⡁⠀⠀⠀⠉⠹⠶⢤⣀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣻⠀⢀⠹⣿⡆⠀⢰⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⣷⣤⣄⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠴⠚⢩⠀⢸⡄⢹⣿⣦⣸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣷⣤⡄⠀⢀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠴⠋⡀⣀⣰⣿⠀⠄⠹⣾⣿⣿⡿⣿⠀⢠⣤⣀⣴⣤⣤⡴⠶⠶⠿⠿⠛⠛⠋⠉⠉⣠⣿"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠞⠁⢀⡱⠏⠉⡟⠃⠀⠀⠀⢸⣿⣿⠇⣿⡴⠾⠛⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⡿⠟"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠖⢋⣡⣶⣿⣂⡼⠁⠉⠙⠋⠙⠿⠟⣢⣄⢿⡟⠴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠈⠀⠀"),
|
||||
Line::from("⠀⠀⠀⢀⣠⠴⠚⠉⠉⠀⠀⠀⠀⠀⣸⡿⠟⠀⠀⠀⠀⠀⠀⠲⣾⡛⣿⣬⡄⠀⠀⠁⠠⣤⠆⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⣠⠞⠉⠀⠀⠀⠀⠀⠀⠀⠀⠤⠚⠉⠀⠀⠀⠀⠀⠀⠀⠀⠺⣿⡟⣿⡟⠀⠀⠂⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠞⠁⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢐⡀⡀⣼⣿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠈⠁⠆⠀⠀⠀"),
|
||||
Line::from(""),
|
||||
Line::from(""),
|
||||
Line::from("Chads only, neither SoyDevs nor Corps allowed"),
|
||||
Line::from("This page soon will be available, join the future"),
|
||||
Line::from("https://git.ghostchain.io/ghostchain/ghost-eye"),
|
||||
]
|
||||
}
|
||||
|
||||
fn prepare_active_text(&self) -> Vec<Line> {
|
||||
vec![
|
||||
Line::from(" ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠴⠊⠁⠀⠀⠀⠀⠀⠉⠒⠤⡀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠋⠀⠀⠀⢀⣀⣀⣠⣤⣴⣆⣀⠀⠙⡆⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⢋⣀⣤⣶⣯⣯⡶⠯⠭⠔⠚⠛⠂⠀⠀⢷⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⢀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠉⠋⠉⠉⠀⠀⠀⠐⠒⠒⠋⠓⠂⠀⢸⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⣠⠔⠊⠉⠀⠀⠈⠉⠐⠢⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡄⠠⠒⠈⢡⣒⠀⢀⣰⣧⣶⡦⠀⠀⠀⠸⠀⠀⠀⠀"),
|
||||
Line::from("⠀⣠⠎⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⢢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢇⠀⠸⠿⠿⠿⡆⢸⠈⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣶⣄⣀⡀⠀⠀⠀⠀⠸⡄⠀⠀⠀⢀⡇⠀⢧⡀⠀⠀⠀⠀⠀⢠⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠛⣉⣩⣽⠽⠛⣒⡒⠒⠦⠤⢤⣤⣤⣤⢀⡀⠱⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡏⠀⠈⠉⠓⠦⠤⠤⡇⡀⠀⠀⠘⠤⠤⠴⠃⠀⠀⠀⠀⣠⣿⣆⠀⠀⠀"),
|
||||
Line::from("⠀⡿⢛⣁⣈⠉⠉⠒⠍⠉⠩⠭⠽⠂⠢⠭⢷⠍⠀⢹⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⠥⠤⣀⠀⠀⠀⠀⠀⠘⢷⠀⢀⣴⣸⣿⣠⡷⣄⣄⢠⣾⣿⣿⠋⠑⠲⠤"),
|
||||
Line::from("⠀⠈⣁⠀⠈⠑⠄⠀⠀⠀⠀⠀⠀⠠⠖⠒⠲⡄⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢤⡀⠀⠀⠀⠀⣿⣸⣧⣿⣿⣿⣿⡎⢹⣿⣿⡿⠛⠀⠀⠀⠀"),
|
||||
Line::from("⠟⢩⢿⣛⡛⠳⣖⠲⣖⠒⠒⡶⠖⣲⠿⠿⣖⠦⢤⡤⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠻⣯⠙⠿⠿⠋⠀⢰⣾⠿⠁⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⡻⠿⠿⢧⡿⢀⣿⠖⢲⣧⣼⣿⣿⡯⠎⠀⠀⣇⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢄⠀⠀⠀⠙⠟⣶⣤⣶⣦⡿⠏⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⣦⣄⣉⣐⣂⣉⣤⠿⡿⠀⠀⢻⣻⣝⣒⣂⣈⣠⣴⠃⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⡄⠀⠀⠀⠀⠉⠁⠁⠁⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⢠⠇⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⠸⣀⣀⢀⡠⠇⠀⠀⠀⠀⠀⠀⣸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⠀⠀⢠⢆⣈⢁⡀⠀⠀⠀⠀⠀⠀⠀⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠀⠀⠀⠀⠀⢠⡞⢁⣼⠯⠴⣷⣤⡀⠀⠀⠀⢠⣾⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⣀⠀⠀⠀⢀⡾⣠⣿⣿⣿⣷⡎⣟⣷⠀⣀⣴⣶⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⣼⣧⠀⠀⢸⡇⢿⢻⣿⣿⣿⡇⠉⢙⣄⣟⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⣿⡜⣞⡆⡾⢿⣬⠆⠙⠛⠋⢸⠀⣸⠛⣻⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⡻⣿⢹⣸⡇⠛⢣⡜⣖⠲⣖⠁⣷⢳⣶⢿⣼⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⡇⠙⠙⠯⣿⢠⣼⢿⡾⡴⣿⡀⣽⣿⠟⢃⣼⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from("⠁⠀⠀⠀⡟⡿⢾⣻⣿⣯⣿⡷⠿⡍⠀⢸⠘⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"),
|
||||
Line::from(""),
|
||||
Line::from("...if you don't know how to exit Mr.Soyjak"),
|
||||
Line::from("press ESC, noob noob noob noob noob noob"),
|
||||
]
|
||||
}
|
||||
|
||||
fn set_active(&mut self) -> Result<()> {
|
||||
self.is_active = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unset_active(&mut self) -> Result<()> {
|
||||
self.is_active = false;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Empty {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetMode(Mode::Empty(true)) if !self.is_active => self.set_active()?,
|
||||
Action::SetMode(Mode::Empty(false)) if self.is_active => self.unset_active()?,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let screen = super::screen_layout(area);
|
||||
|
||||
let lines = if self.is_active { self.prepare_active_text() } else { self.prepare_inactive_text() };
|
||||
let lines_len = lines.len() as u16;
|
||||
let padding_top = screen
|
||||
.as_size()
|
||||
.height
|
||||
.saturating_sub(lines_len) / 2;
|
||||
|
||||
let paragraph = Paragraph::new(lines)
|
||||
.block(Block::bordered().padding(Padding::new(0, 0, padding_top / 2, 0)))
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
frame.render_widget(paragraph, screen);
|
||||
Ok(())
|
||||
}
|
||||
}
|
118
src/components/explorer/block_ticker.rs
Normal file
118
src/components/explorer/block_ticker.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
widgets::{Block, Padding, Paragraph, Wrap},
|
||||
Frame
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
use crate::{
|
||||
widgets::{BigText, PixelSize},
|
||||
action::Action,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BlockTicker {
|
||||
last_block: u32,
|
||||
last_block_time: Instant,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for BlockTicker {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockTicker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
last_block: 0,
|
||||
last_block_time: Instant::now(),
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn block_found(&mut self, block: &str) -> Result<()> {
|
||||
let block = block.trim_start_matches("0x");
|
||||
let block = u32::from_str_radix(&block, 16)?;
|
||||
if self.last_block < block {
|
||||
self.last_block_time = Instant::now();
|
||||
self.last_block = block;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for BlockTicker {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetLatestBlock(_, block_info) =>
|
||||
self.block_found(&block_info.header.number)?,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [place, _, _] = super::explorer_block_info_layout(area);
|
||||
|
||||
let passed = (Instant::now() - self.last_block_time).as_secs_f64();
|
||||
let text = if passed < 60.0 {
|
||||
format!("{:.1}s", passed)
|
||||
} else if passed < 3600.0 {
|
||||
let passed = passed % 3600.0 / 60.0;
|
||||
format!("{:.1}m", passed)
|
||||
} else {
|
||||
let passed = passed % (24.0 * 3600.0) / 3600.0;
|
||||
format!("{:.1}h", passed)
|
||||
};
|
||||
|
||||
let (border_style, border_type) = self.palette.create_border_style(false);
|
||||
let height = place.as_size().height;
|
||||
let width = place.as_size().width;
|
||||
let text_width = text.len() as u16 * 4 + 2;
|
||||
|
||||
if width < text_width || height < 7 {
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_alignment(Alignment::Right)
|
||||
.title_style(self.palette.create_title_style())
|
||||
.padding(Padding::new(0, 0, (height - 2) / 2, 0))
|
||||
.title("Passed"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, place);
|
||||
} else {
|
||||
let big_text = BigText::builder()
|
||||
.centered()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.style(self.palette.create_text_style(false))
|
||||
.lines(vec![
|
||||
text.into(),
|
||||
])
|
||||
.build();
|
||||
let paragraph = Paragraph::new("")
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_alignment(Alignment::Right)
|
||||
.title_style(self.palette.create_title_style())
|
||||
.title("Passed"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, place);
|
||||
|
||||
let height_offset = (height - 2) / 2;
|
||||
let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 })
|
||||
.intersection(place);
|
||||
frame.render_widget(big_text, place);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
110
src/components/explorer/current_epoch.rs
Normal file
110
src/components/explorer/current_epoch.rs
Normal file
@ -0,0 +1,110 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
text::Line,
|
||||
widgets::{Block, Padding, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
use crate::{action::Action, palette::StylePalette, widgets::{PixelSize, BigText}};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CurrentEpoch {
|
||||
number: u64,
|
||||
progress: u64,
|
||||
palette: StylePalette,
|
||||
}
|
||||
|
||||
impl CurrentEpoch {
|
||||
const SECONDS_IN_BLOCK: u64 = 6;
|
||||
const SESSION_LENGTH: u64 = 2_400;
|
||||
const SECONDS_IN_DAY: u64 = 86_400;
|
||||
const SECONDS_IN_HOUR: u64 = 3_600;
|
||||
|
||||
fn update_epoch(&mut self, number: u64, progress: u64) -> Result<()> {
|
||||
self.number = number;
|
||||
self.progress = progress;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CurrentEpoch {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetEpoch(number, progress) => self.update_epoch(number, progress)?,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [place, _] = super::explorer_era_info_layout(area);
|
||||
|
||||
let seconds_to_next = Self::SESSION_LENGTH.saturating_sub(self.progress) * Self::SECONDS_IN_BLOCK;
|
||||
let hours = (seconds_to_next % Self::SECONDS_IN_DAY) / Self::SECONDS_IN_HOUR;
|
||||
let minutes = (seconds_to_next % Self::SECONDS_IN_HOUR) / 60;
|
||||
let seconds = seconds_to_next % 60;
|
||||
|
||||
let text = self.number.to_string();
|
||||
let big_time = hours > 0;
|
||||
|
||||
let (border_style, border_type) = self.palette.create_border_style(false);
|
||||
|
||||
let height = place.as_size().height;
|
||||
let width = place.as_size().width;
|
||||
let text_width = text.len() as u16 * 4 + 2;
|
||||
|
||||
if width < text_width || height < 7 {
|
||||
let text = vec![
|
||||
Line::from(text),
|
||||
Line::from(format!("{}{} {}{}",
|
||||
if big_time { hours } else { minutes },
|
||||
if big_time { "hrs" } else { "mins" },
|
||||
if big_time { minutes } else { seconds },
|
||||
if big_time { "mins" } else { "secs" })),
|
||||
];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_alignment(Alignment::Right)
|
||||
.title_style(self.palette.create_title_style())
|
||||
.padding(Padding::new(0, 0, (height - 3) / 2, 0))
|
||||
.title("Epoch"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, place);
|
||||
} else {
|
||||
let big_text = BigText::builder()
|
||||
.centered()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.style(self.palette.create_text_style(false))
|
||||
.lines(vec![
|
||||
text.into(),
|
||||
])
|
||||
.build();
|
||||
let paragraph = Paragraph::new("")
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_title_style())
|
||||
.title_top(Line::from("Epoch").right_aligned())
|
||||
.title_top(Line::from(format!("{}{} {}{}",
|
||||
if big_time { hours } else { minutes },
|
||||
if big_time { "hrs" } else { "mins" },
|
||||
if big_time { minutes } else { seconds },
|
||||
if big_time { "mins" } else { "secs" }))
|
||||
.centered()))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, place);
|
||||
|
||||
let height_offset = (height - 2) / 2;
|
||||
let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 })
|
||||
.intersection(place);
|
||||
frame.render_widget(big_text, place);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
125
src/components/explorer/current_era.rs
Normal file
125
src/components/explorer/current_era.rs
Normal file
@ -0,0 +1,125 @@
|
||||
use color_eyre::Result;
|
||||
use std::time::Instant;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
text::Line,
|
||||
widgets::{Block, Padding, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
use crate::{action::Action, palette::StylePalette, types::era::EraInfo, widgets::{PixelSize, BigText}};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CurrentEra{
|
||||
era: EraInfo,
|
||||
palette: StylePalette,
|
||||
}
|
||||
|
||||
impl CurrentEra {
|
||||
const ERA_OFFSET_IN_SLOTS: u64 = 2_400 * 6;
|
||||
const ERA_OFFSET_IN_MILLIS: u64 = Self::ERA_OFFSET_IN_SLOTS * 6_000;
|
||||
|
||||
const MILLIS_IN_DAY: u64 = 86_400_000;
|
||||
const MILLIS_IN_HOUR: u64 = 3_600_000;
|
||||
const MILLIS_IN_MINUTE: u64 = 60_000;
|
||||
|
||||
fn update_era(&mut self, era_info: EraInfo) -> Result<()> {
|
||||
self.era = era_info;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CurrentEra {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetActiveEra(era_info) => self.update_era(era_info)?,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, place] = super::explorer_era_info_layout(area);
|
||||
|
||||
let current_time = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("time went backwards")
|
||||
.as_millis() as u64;
|
||||
|
||||
let era_start = self.era.start.unwrap_or_default();
|
||||
let era_end = era_start + Self::ERA_OFFSET_IN_MILLIS;
|
||||
|
||||
let (difference, reversed_char) = if era_end > current_time {
|
||||
(era_end - current_time, "")
|
||||
} else {
|
||||
(current_time - era_end, "< ")
|
||||
};
|
||||
|
||||
let hours = (difference % Self::MILLIS_IN_DAY) / Self::MILLIS_IN_HOUR;
|
||||
let minutes = (difference % Self::MILLIS_IN_HOUR) / Self::MILLIS_IN_MINUTE;
|
||||
let seconds = difference % Self::MILLIS_IN_MINUTE / 1000;
|
||||
|
||||
let text = self.era.index.to_string();
|
||||
let big_time = hours > 0;
|
||||
|
||||
let (border_style, border_type) = self.palette.create_border_style(false);
|
||||
|
||||
let height = place.as_size().height;
|
||||
let width = place.as_size().width;
|
||||
let text_width = text.len() as u16 * 4 + 2;
|
||||
|
||||
if width < text_width || height < 7 {
|
||||
let text = vec![
|
||||
Line::from(text),
|
||||
Line::from(format!("{}{}{} {}{}",
|
||||
reversed_char,
|
||||
if big_time { hours } else { minutes },
|
||||
if big_time { "hrs" } else { "mins" },
|
||||
if big_time { minutes } else { seconds },
|
||||
if big_time { "mins" } else { "secs" })),
|
||||
];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_alignment(Alignment::Right)
|
||||
.title_style(self.palette.create_title_style())
|
||||
.padding(Padding::new(0, 0, (height - 3) / 2, 0))
|
||||
.title("Era"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, place);
|
||||
} else {
|
||||
let big_text = BigText::builder()
|
||||
.centered()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.style(self.palette.create_text_style(false))
|
||||
.lines(vec![
|
||||
text.into(),
|
||||
])
|
||||
.build();
|
||||
let paragraph = Paragraph::new("")
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_title_style())
|
||||
.title_top(Line::from("Era").right_aligned())
|
||||
.title_top(Line::from(format!("{}{} {}{}",
|
||||
if big_time { hours } else { minutes },
|
||||
if big_time { "hrs" } else { "mins" },
|
||||
if big_time { minutes } else { seconds },
|
||||
if big_time { "mins" } else { "secs" }))
|
||||
.centered()))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, place);
|
||||
|
||||
let height_offset = (height - 2) / 2;
|
||||
let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 })
|
||||
.intersection(place);
|
||||
frame.render_widget(big_text, place);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
484
src/components/explorer/explorer_blocks.rs
Normal file
484
src/components/explorer/explorer_blocks.rs
Normal file
@ -0,0 +1,484 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
prelude::*,
|
||||
style::Style,
|
||||
text::Line,
|
||||
widgets::{Block, BorderType, Paragraph},
|
||||
Frame
|
||||
};
|
||||
use sp_consensus_babe::digests::PreDigest;
|
||||
use sp_runtime::DigestItem;
|
||||
use codec::Decode;
|
||||
|
||||
use super::Component;
|
||||
use crate::{action::Action, app::Mode, palette::StylePalette};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct BlockInfo {
|
||||
block_number: u32,
|
||||
finalized: bool,
|
||||
hash: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ExplorerBlocks {
|
||||
blocks: VecDeque<BlockInfo>,
|
||||
extrinsics: HashMap<String, Vec<String>>,
|
||||
logs: HashMap<String, Vec<String>>,
|
||||
validators: Vec<String>,
|
||||
palette: StylePalette,
|
||||
max_block_len: u32,
|
||||
|
||||
is_active: bool,
|
||||
used_paragraph_index: usize,
|
||||
used_block_index: Option<usize>,
|
||||
used_ext_index: Option<(String, usize, usize)>,
|
||||
}
|
||||
|
||||
impl ExplorerBlocks {
|
||||
const MAX_BLOCKS: usize = 50;
|
||||
const LENGTH_OF_BLOCK_HASH: u16 = 66; // hash + 0x prefix
|
||||
const LENGTH_OF_ADDRESS: u16 = 49;
|
||||
const TOTAL_OFFSETS: u16 = 18;
|
||||
|
||||
fn update_validator_list(&mut self, validators: Vec<String>) -> Result<()> {
|
||||
self.validators = validators;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_author_from_digest(&self, logs: Vec<String>) -> Option<String> {
|
||||
let logs = logs
|
||||
.iter()
|
||||
.map_while(|log| {
|
||||
hex::decode(log.trim_start_matches("0x"))
|
||||
.ok()
|
||||
.map(|log_hex| DigestItem::decode(&mut &log_hex[..]).ok())
|
||||
})
|
||||
.filter_map(|digest| digest)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let maybe_author = match logs.iter().find(|item| matches!(item, DigestItem::PreRuntime(..))) {
|
||||
Some(DigestItem::PreRuntime(engine, data)) if *engine == [b'B', b'A', b'B', b'E'] => {
|
||||
match PreDigest::decode(&mut &data[..]) {
|
||||
Ok(PreDigest::Primary(primary)) => self.validators.get(primary.authority_index as usize),
|
||||
Ok(PreDigest::SecondaryPlain(secondary)) => self.validators.get(secondary.authority_index as usize),
|
||||
Ok(PreDigest::SecondaryVRF(secondary)) => self.validators.get(secondary.authority_index as usize),
|
||||
_ => None,
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
maybe_author.cloned()
|
||||
}
|
||||
|
||||
fn update_latest_block_info(
|
||||
&mut self,
|
||||
hash: String,
|
||||
number_hex_str: String,
|
||||
logs: Vec<String>,
|
||||
extrinsics: Vec<String>,
|
||||
) -> Result<()> {
|
||||
let number_hex_str = number_hex_str.trim_start_matches("0x");
|
||||
let block_number = u32::from_str_radix(&number_hex_str, 16)?;
|
||||
let front_block_number = if self.blocks.is_empty() {
|
||||
0
|
||||
} else {
|
||||
self.blocks.front().unwrap().block_number
|
||||
};
|
||||
|
||||
if front_block_number < block_number {
|
||||
self.blocks.push_front(BlockInfo {
|
||||
block_number,
|
||||
finalized: false,
|
||||
hash: hash.clone(),
|
||||
});
|
||||
|
||||
self.extrinsics.insert(hash.clone(), extrinsics);
|
||||
self.logs.insert(hash, logs);
|
||||
|
||||
let block_length = block_number.checked_ilog10().unwrap_or(0) + 1;
|
||||
if self.max_block_len < block_length {
|
||||
self.max_block_len = block_length;
|
||||
}
|
||||
|
||||
if self.blocks.len() > Self::MAX_BLOCKS {
|
||||
if let Some(removed_block_info) = self.blocks.pop_back() {
|
||||
self.extrinsics.remove(&removed_block_info.hash);
|
||||
self.logs.remove(&removed_block_info.hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_finalized_block_info(
|
||||
&mut self,
|
||||
hash: String,
|
||||
number_hex_str: String,
|
||||
logs: Vec<String>,
|
||||
extrinsics: Vec<String>,
|
||||
) -> Result<()> {
|
||||
let number_hex_str = number_hex_str.trim_start_matches("0x");
|
||||
let block_number = u32::from_str_radix(&number_hex_str, 16)?;
|
||||
|
||||
if !self.blocks.is_empty() {
|
||||
for current_block_info in self.blocks.iter_mut() {
|
||||
if current_block_info.block_number <= block_number {
|
||||
*current_block_info = BlockInfo {
|
||||
block_number: current_block_info.block_number,
|
||||
hash: hash.clone(),
|
||||
finalized: true,
|
||||
};
|
||||
self.extrinsics.insert(hash.clone(), extrinsics.clone());
|
||||
self.logs.insert(hash.clone(), logs.clone());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_block_line_info(&self, current_block: &BlockInfo, width: u16) -> Line {
|
||||
let block_number_length = self
|
||||
.max_block_len
|
||||
.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 author = self
|
||||
.logs
|
||||
.get(¤t_block.hash)
|
||||
.map_or(String::from("..."), |maybe_logs| {
|
||||
self.get_author_from_digest(maybe_logs.to_vec())
|
||||
.map_or(String::from("..."), |maybe_author| maybe_author)
|
||||
});
|
||||
|
||||
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);
|
||||
let len_for_hash = (free_space - len_for_author) / 2;
|
||||
|
||||
let hash_to_print = format!("{}...{}",
|
||||
¤t_block.hash[..len_for_hash as usize],
|
||||
¤t_block.hash[(Self::LENGTH_OF_BLOCK_HASH - len_for_hash) as usize..]);
|
||||
|
||||
if &author == "..." {
|
||||
Line::raw(format!("{:^left$}| {} | {:^right$}",
|
||||
current_block.block_number,
|
||||
hash_to_print,
|
||||
author,
|
||||
left=block_number_length,
|
||||
right=(len_for_author + 2) as usize))
|
||||
} else {
|
||||
Line::raw(format!("{} | {} | {}",
|
||||
current_block.block_number,
|
||||
hash_to_print,
|
||||
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 margin = (width - total_space_used) as usize / 3;
|
||||
Line::raw(format!("{:^margin$}|{:^margin$}|{:^margin$}",
|
||||
current_block.block_number,
|
||||
current_block.hash,
|
||||
author))
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_block_lines(&mut self, rect: Rect) -> Vec<Line> {
|
||||
let width = rect.as_size().width;
|
||||
let total_length = rect.as_size().height - 2;
|
||||
let mut total_index = 0;
|
||||
let mut items = Vec::new();
|
||||
|
||||
let active_style = self.palette.create_text_style(true);
|
||||
let latest_style = self.palette.create_text_style(false);
|
||||
let finalized_style = Style::new().fg(self.palette.foreground_hover());
|
||||
|
||||
for current_block_info in self.blocks.iter() {
|
||||
if total_length == total_index { break; }
|
||||
|
||||
let style = if let Some(used_block_index) = self.used_block_index {
|
||||
if total_index as usize == used_block_index { active_style } else { latest_style }
|
||||
} else {
|
||||
if current_block_info.finalized { finalized_style } else { latest_style }
|
||||
};
|
||||
|
||||
items.push(self.prepare_block_line_info(¤t_block_info, width).style(style));
|
||||
total_index += 1;
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
fn prepare_ext_line_info(&self, index: usize, extrinsic: String, width: u16) -> Line {
|
||||
let index_len = index.checked_ilog10().unwrap_or(0) + 1;
|
||||
let len_for_ext = width.saturating_sub(index_len as u16 + 17) as usize;
|
||||
let len_extrinsic_hash = extrinsic.len();
|
||||
Line::from(format!("{} MODULE METHOD {}",
|
||||
index,
|
||||
format!("{}...{}", &extrinsic[..len_for_ext], &extrinsic[len_extrinsic_hash - len_for_ext..])))
|
||||
}
|
||||
|
||||
fn prepare_ext_lines(&mut self, rect: Rect) -> Vec<Line> {
|
||||
let width = rect.as_size().width;
|
||||
let total_length = rect.as_size().height - 2;
|
||||
let mut total_index = 0;
|
||||
let mut items = Vec::new();
|
||||
|
||||
let normal_style = self.palette.create_text_style(false);
|
||||
let active_style = self.palette.create_text_style(true);
|
||||
|
||||
if let Some((used_block_hash, _, _)) = &self.used_ext_index {
|
||||
if let Some(exts) = self.extrinsics.get(used_block_hash) {
|
||||
for (index, ext) in exts.iter().enumerate() {
|
||||
if total_length == total_index { 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(0, ext.to_string(), width).style(style));
|
||||
total_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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, block_index, used_index)) => {
|
||||
let new_index = used_index + 1;
|
||||
|
||||
let maybe_exts = self.extrinsics.get(&*header);
|
||||
if maybe_exts.is_none() { return }
|
||||
let exts = maybe_exts.unwrap();
|
||||
|
||||
let found = exts
|
||||
.get(new_index)
|
||||
.is_some();
|
||||
|
||||
if found && new_index < exts.len() {
|
||||
self.used_ext_index =
|
||||
Some(((&*header).clone(), *block_index, new_index));
|
||||
}
|
||||
},
|
||||
None => {
|
||||
self.used_ext_index = self.blocks
|
||||
.front()
|
||||
.map(|block| {
|
||||
self.extrinsics
|
||||
.get(&block.hash)
|
||||
.map(|_| (block.hash.clone(), 0, 0))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_up_blocks(&mut self) {
|
||||
match &self.used_block_index {
|
||||
Some(used_index) => {
|
||||
let new_index = used_index + 1;
|
||||
let maybe_new_extrinsic = self.blocks.get(new_index);
|
||||
if new_index < self.blocks.len() && maybe_new_extrinsic.is_some() {
|
||||
self.used_block_index = maybe_new_extrinsic.map(|_| new_index);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
self.used_block_index = self.blocks
|
||||
.front()
|
||||
.map(|_| 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_down_extrinsics(&mut self) {
|
||||
match &self.used_ext_index {
|
||||
Some((header, block_index, used_index)) => {
|
||||
if *used_index == 0 { return }
|
||||
let new_index = used_index - 1;
|
||||
|
||||
let maybe_exts = self.extrinsics.get(&*header);
|
||||
if maybe_exts.is_none() { return }
|
||||
|
||||
let found = maybe_exts
|
||||
.unwrap()
|
||||
.get(new_index)
|
||||
.is_some();
|
||||
|
||||
if found {
|
||||
self.used_ext_index =
|
||||
Some(((&*header).clone(), *block_index, new_index));
|
||||
}
|
||||
|
||||
},
|
||||
None => {
|
||||
self.used_ext_index = self.blocks
|
||||
.front()
|
||||
.map(|block| {
|
||||
self.extrinsics
|
||||
.get(&block.hash)
|
||||
.map(|_| (block.hash.clone(), 0, 0))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_down_blocks(&mut self) {
|
||||
self.used_block_index = match &self.used_block_index {
|
||||
Some(used_index) => {
|
||||
if *used_index == 0 { return }
|
||||
let new_index = used_index - 1;
|
||||
self.blocks.get(new_index).map(|_| new_index)
|
||||
},
|
||||
None => self.blocks.front().map(|_| 0),
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
Paragraph::new("")
|
||||
.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 handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
match key.code {
|
||||
KeyCode::Char('k') | KeyCode::Up => 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_index = 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::SetLatestBlock(hash, block) => self.update_latest_block_info(hash, block.header.number, block.header.digest.logs, block.extrinsics)?,
|
||||
Action::SetFinalizedBlock(hash, block) => self.update_finalized_block_info(hash, block.header.number, block.header.digest.logs, block.extrinsics)?,
|
||||
Action::SetValidators(validators) => self.update_validator_list(validators)?,
|
||||
Action::SetMode(Mode::Explorer(true)) if !self.is_active => self.set_active()?,
|
||||
Action::SetMode(Mode::Explorer(false)) 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(())
|
||||
}
|
||||
}
|
83
src/components/explorer/extrinsics_chart.rs
Normal file
83
src/components/explorer/extrinsics_chart.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use std::collections::VecDeque;
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
text::Line,
|
||||
widgets::{Bar, BarChart, BarGroup, Block},
|
||||
Frame
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
use crate::action::Action;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ExtrinsicsChart {
|
||||
extrinsics: VecDeque<(u32, usize)>,
|
||||
}
|
||||
|
||||
impl ExtrinsicsChart {
|
||||
const MAX_LEN: usize = 50;
|
||||
const BAR_WIDTH: usize = 6;
|
||||
const BAR_GAP: usize = 1;
|
||||
|
||||
fn extrinsics_bar_chart(&self, width: u16) -> BarChart {
|
||||
let length = (width as usize) / (Self::BAR_WIDTH + Self::BAR_GAP);
|
||||
|
||||
let bars: Vec<Bar> = self.extrinsics
|
||||
.iter()
|
||||
.rev()
|
||||
.take(length)
|
||||
.map(|(block_number, ext_len)| self.extrinsic_vertical_bar(block_number, ext_len))
|
||||
.collect();
|
||||
|
||||
BarChart::default()
|
||||
.data(BarGroup::default().bars(&bars))
|
||||
.block(Block::bordered().title_alignment(Alignment::Right).title("Tx. Heat Map"))
|
||||
.bar_width(8)
|
||||
.bar_gap(1)
|
||||
}
|
||||
|
||||
fn extrinsic_vertical_bar(&self, block_number: &u32, ext_len: &usize) -> Bar {
|
||||
Bar::default()
|
||||
.value(*ext_len as u64)
|
||||
.label(Line::from(format!("..{}", block_number % 100)))
|
||||
.text_value(ext_len.to_string())
|
||||
}
|
||||
|
||||
fn update_extrinsics(&mut self, block_number: String, extrinsics_number: usize) -> Result<()> {
|
||||
let block_number = block_number.trim_start_matches("0x");
|
||||
let block_number = u32::from_str_radix(&block_number, 16)?;
|
||||
|
||||
match self.extrinsics.back() {
|
||||
Some(back) => {
|
||||
if back.0 < block_number {
|
||||
self.extrinsics.push_back((block_number, extrinsics_number));
|
||||
if self.extrinsics.len() > Self::MAX_LEN {
|
||||
let _ = self.extrinsics.pop_front();
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
self.extrinsics.push_back((block_number, extrinsics_number));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ExtrinsicsChart {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetLatestBlock(_, block) => self.update_extrinsics(block.header.number, block.extrinsics.len())?,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, place] = super::explorer_header_layout(area);
|
||||
frame.render_widget(self.extrinsics_bar_chart(place.as_size().width), place);
|
||||
Ok(())
|
||||
}
|
||||
}
|
83
src/components/explorer/finalized_block.rs
Normal file
83
src/components/explorer/finalized_block.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
widgets::{Block, Padding, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
use crate::{action::Action, palette::StylePalette, widgets::{PixelSize, BigText}};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FinalizedBlock {
|
||||
number: u32,
|
||||
palette: StylePalette,
|
||||
}
|
||||
|
||||
impl FinalizedBlock {
|
||||
fn update_block_number(&mut self, number_hex_str: String) -> Result<()> {
|
||||
let number_hex_str = number_hex_str.trim_start_matches("0x");
|
||||
self.number = u32::from_str_radix(&number_hex_str, 16)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for FinalizedBlock {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetFinalizedBlock(_, block) => self.update_block_number(block.header.number)?,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, _, place] = super::explorer_block_info_layout(area);
|
||||
|
||||
let text = self.number.to_string();
|
||||
let (border_style, border_type) = self.palette.create_border_style(false);
|
||||
|
||||
let height = place.as_size().height;
|
||||
let width = place.as_size().width;
|
||||
let text_width = text.len() as u16 * 4 + 2;
|
||||
|
||||
if width < text_width || height < 7 {
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_alignment(Alignment::Right)
|
||||
.title_style(self.palette.create_title_style())
|
||||
.padding(Padding::new(0, 0, (height - 2) / 2, 0))
|
||||
.title("Latest"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, place);
|
||||
} else {
|
||||
let big_text = BigText::builder()
|
||||
.centered()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.style(self.palette.create_text_style(false))
|
||||
.lines(vec![
|
||||
text.into(),
|
||||
])
|
||||
.build();
|
||||
let paragraph = Paragraph::new("")
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_alignment(Alignment::Right)
|
||||
.title_style(self.palette.create_title_style())
|
||||
.title("Latest"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, place);
|
||||
|
||||
let height_offset = (height - 2) / 2;
|
||||
let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 })
|
||||
.intersection(place);
|
||||
frame.render_widget(big_text, place);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
83
src/components/explorer/latest_block.rs
Normal file
83
src/components/explorer/latest_block.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
widgets::{Block, Padding, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
use crate::{action::Action, palette::StylePalette, widgets::{PixelSize, BigText}};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LatestBlock {
|
||||
number: u32,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl LatestBlock {
|
||||
fn update_block_number(&mut self, number_hex_str: String) -> Result<()> {
|
||||
let number_hex_str = number_hex_str.trim_start_matches("0x");
|
||||
self.number = u32::from_str_radix(&number_hex_str, 16)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for LatestBlock {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetLatestBlock(_, block) => self.update_block_number(block.header.number)?,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, place, _] = super::explorer_block_info_layout(area);
|
||||
|
||||
let text = self.number.to_string();
|
||||
let (border_style, border_type) = self.palette.create_border_style(false);
|
||||
|
||||
let height = place.as_size().height;
|
||||
let width = place.as_size().width;
|
||||
let text_width = text.len() as u16 * 4 + 2;
|
||||
|
||||
if width < text_width || height < 7 {
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_alignment(Alignment::Right)
|
||||
.title_style(self.palette.create_title_style())
|
||||
.padding(Padding::new(0, 0, (height - 2) / 2, 0))
|
||||
.title("Latest"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, place);
|
||||
} else {
|
||||
let big_text = BigText::builder()
|
||||
.centered()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.style(self.palette.create_text_style(false))
|
||||
.lines(vec![
|
||||
text.into(),
|
||||
])
|
||||
.build();
|
||||
let paragraph = Paragraph::new("")
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_alignment(Alignment::Right)
|
||||
.title_style(self.palette.create_title_style())
|
||||
.title("Latest"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, place);
|
||||
|
||||
let height_offset = (height - 2) / 2;
|
||||
let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 })
|
||||
.intersection(place);
|
||||
frame.render_widget(big_text, place);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
110
src/components/explorer/mod.rs
Normal file
110
src/components/explorer/mod.rs
Normal file
@ -0,0 +1,110 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Flex, Layout, Rect},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
use crate::action::Action;
|
||||
|
||||
mod latest_block;
|
||||
mod finalized_block;
|
||||
mod block_ticker;
|
||||
mod current_era;
|
||||
mod current_epoch;
|
||||
mod extrinsics_chart;
|
||||
mod explorer_blocks;
|
||||
|
||||
use latest_block::LatestBlock;
|
||||
use finalized_block::FinalizedBlock;
|
||||
use block_ticker::BlockTicker;
|
||||
use current_era::CurrentEra;
|
||||
use current_epoch::CurrentEpoch;
|
||||
use extrinsics_chart::ExtrinsicsChart;
|
||||
use explorer_blocks::ExplorerBlocks;
|
||||
|
||||
pub struct Explorer {
|
||||
components: Vec<Box<dyn Component>>
|
||||
}
|
||||
|
||||
impl Default for Explorer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
components: vec![
|
||||
Box::new(BlockTicker::default()),
|
||||
Box::new(LatestBlock::default()),
|
||||
Box::new(FinalizedBlock::default()),
|
||||
Box::new(CurrentEra::default()),
|
||||
Box::new(CurrentEpoch::default()),
|
||||
Box::new(ExtrinsicsChart::default()),
|
||||
Box::new(ExplorerBlocks::default()),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Explorer {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
for component in self.components.iter_mut() {
|
||||
component.update(action.clone())?;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let screen = super::screen_layout(area);
|
||||
for component in self.components.iter_mut() {
|
||||
component.draw(frame, screen)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn explorer_layout(area: Rect) -> [Rect; 3] {
|
||||
Layout::vertical([
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Percentage(30),
|
||||
]).areas(area)
|
||||
}
|
||||
|
||||
pub fn explorer_header_layout(area: Rect) -> [Rect; 2] {
|
||||
let [header, _, _] = explorer_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]).areas(header)
|
||||
}
|
||||
|
||||
pub fn explorer_info_layout(area: Rect) -> [Rect; 2] {
|
||||
let [info, _] = explorer_header_layout(area);
|
||||
Layout::vertical([
|
||||
Constraint::Percentage(100),
|
||||
Constraint::Percentage(100),
|
||||
]).areas(info)
|
||||
}
|
||||
|
||||
pub fn explorer_block_info_layout(area: Rect) -> [Rect; 3] {
|
||||
let [blocks, _] = explorer_info_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
]).flex(Flex::SpaceBetween).areas(blocks)
|
||||
}
|
||||
|
||||
pub fn explorer_era_info_layout(area: Rect) -> [Rect; 2] {
|
||||
let [_, blocks] = explorer_info_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]).flex(Flex::SpaceBetween).areas(blocks)
|
||||
}
|
||||
|
||||
pub fn explorer_scrollbars_layout(area: Rect) -> [Rect; 2] {
|
||||
let [_, place, _] = explorer_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]).areas(place)
|
||||
}
|
93
src/components/fps.rs
Normal file
93
src/components/fps.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Style, Stylize},
|
||||
text::Span,
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
use crate::action::Action;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FpsCounter {
|
||||
last_tick_update: Instant,
|
||||
tick_count: u32,
|
||||
ticks_per_second: f64,
|
||||
|
||||
last_frame_update: Instant,
|
||||
frame_count: u32,
|
||||
frames_per_second: f64,
|
||||
}
|
||||
|
||||
impl Default for FpsCounter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl FpsCounter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
last_tick_update: Instant::now(),
|
||||
tick_count: 0,
|
||||
ticks_per_second: 0.0,
|
||||
last_frame_update: Instant::now(),
|
||||
frame_count: 0,
|
||||
frames_per_second: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn app_tick(&mut self) -> Result<()> {
|
||||
self.tick_count += 1;
|
||||
let now = Instant::now();
|
||||
let elapsed = (now - self.last_tick_update).as_secs_f64();
|
||||
if elapsed >= 1.0 {
|
||||
self.ticks_per_second = self.tick_count as f64 / elapsed;
|
||||
self.last_tick_update = now;
|
||||
self.tick_count = 0;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_tick(&mut self) -> Result<()> {
|
||||
self.frame_count += 1;
|
||||
let now = Instant::now();
|
||||
let elapsed = (now - self.last_frame_update).as_secs_f64();
|
||||
if elapsed >= 1.0 {
|
||||
self.frames_per_second = self.frame_count as f64 / elapsed;
|
||||
self.last_frame_update = now;
|
||||
self.frame_count = 0;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for FpsCounter {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::Tick => self.app_tick()?,
|
||||
Action::Render => self.render_tick()?,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, place] = super::header_layout(area);
|
||||
|
||||
let message = format!(
|
||||
"{:.2} ticks/sec | {:.2} FPS",
|
||||
self.ticks_per_second,
|
||||
self.frames_per_second
|
||||
);
|
||||
|
||||
let span = Span::styled(message, Style::new().dim());
|
||||
let paragraph = Paragraph::new(span).right_aligned();
|
||||
frame.render_widget(paragraph, place);
|
||||
Ok(())
|
||||
}
|
||||
}
|
111
src/components/health.rs
Normal file
111
src/components/health.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Style, Stylize},
|
||||
text::Span,
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
use crate::{
|
||||
action::Action,
|
||||
widgets::{DotSpinner, OghamCenter, VerticalBlocks}
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Health {
|
||||
name: Option<String>,
|
||||
peers: Option<u32>,
|
||||
is_syncing: bool,
|
||||
should_have_peers: bool,
|
||||
tx_pool_length: usize,
|
||||
}
|
||||
|
||||
impl Default for Health {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Health {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
peers: None,
|
||||
is_syncing: true,
|
||||
should_have_peers: false,
|
||||
tx_pool_length: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_sync_state(&mut self, peers: Option<u32>, is_syncing: bool, should_have_peers: bool) -> Result<()> {
|
||||
self.peers = peers;
|
||||
self.is_syncing = is_syncing;
|
||||
self.should_have_peers = should_have_peers;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_tx_pool_length(&mut self, tx_pool_length: usize) -> Result<()> {
|
||||
self.tx_pool_length = tx_pool_length;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_node_name(&mut self, name: Option<String>) -> Result<()> {
|
||||
self.name = name;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_syncing_as_string(&self) -> String {
|
||||
if self.is_syncing {
|
||||
format!("syncing {}", VerticalBlocks::default().to_string())
|
||||
} else {
|
||||
String::from("synced")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peers_as_string(&self) -> String {
|
||||
if self.peers.is_some() {
|
||||
self.peers.unwrap().to_string()
|
||||
} else {
|
||||
DotSpinner::default().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name_as_string(&self) -> String {
|
||||
if self.name.is_some() {
|
||||
self.name.clone().unwrap()
|
||||
} else {
|
||||
OghamCenter::default().to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Health {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetSyncState(peers, is_syncing, should_have_peers) => {
|
||||
self.set_sync_state(peers, is_syncing, should_have_peers)?
|
||||
},
|
||||
Action::SetNodeName(name) => self.set_node_name(name)?,
|
||||
Action::SetPendingExtrinsicsLength(length) => self.set_tx_pool_length(length)?,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [place, _] = super::header_layout(area);
|
||||
|
||||
let message = format!("{:^12} | tx.pool: {:^3} | peers: {:^3} | {:^9}",
|
||||
self.name_as_string(),
|
||||
self.tx_pool_length,
|
||||
self.peers_as_string(),
|
||||
self.is_syncing_as_string());
|
||||
|
||||
let span = Span::styled(message, Style::new().dim());
|
||||
let paragraph = Paragraph::new(span).left_aligned();
|
||||
frame.render_widget(paragraph, place);
|
||||
Ok(())
|
||||
}
|
||||
}
|
137
src/components/menu.rs
Normal file
137
src/components/menu.rs
Normal file
@ -0,0 +1,137 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use crossterm::event::{KeyEvent, KeyCode};
|
||||
|
||||
use super::Component;
|
||||
use super::palette::StylePalette;
|
||||
use crate::{action::Action, app::Mode};
|
||||
|
||||
pub struct Menu {
|
||||
command_tx: Option<UnboundedSender<Action>>,
|
||||
items: Vec<String>,
|
||||
current_item_index: usize,
|
||||
is_active: bool,
|
||||
palette: StylePalette,
|
||||
}
|
||||
|
||||
impl Default for Menu {
|
||||
fn default() -> Self {
|
||||
Menu::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Menu {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
command_tx: None,
|
||||
items: vec![
|
||||
String::from("Explorer"),
|
||||
String::from("Wallet"),
|
||||
String::from("Prices"),
|
||||
String::from("Staking"),
|
||||
String::from("Governance"),
|
||||
String::from("Operations"),
|
||||
],
|
||||
current_item_index: Default::default(),
|
||||
is_active: true,
|
||||
palette: StylePalette::default(),
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fn move_current_up(&mut self) -> Result<()> {
|
||||
self.current_item_index = self
|
||||
.current_item_index
|
||||
.saturating_sub(1);
|
||||
|
||||
if let Some(command_tx) = &self.command_tx {
|
||||
match self.current_item_index {
|
||||
0 => command_tx.send(Action::SetMode(Mode::Explorer(false)))?,
|
||||
_ => command_tx.send(Action::SetMode(Mode::Empty(false)))?,
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn move_current_down(&mut self) -> Result<()> {
|
||||
let new_current = self.current_item_index + 1;
|
||||
if new_current < self.items.len() {
|
||||
self.current_item_index = new_current;
|
||||
}
|
||||
|
||||
if let Some(command_tx) = &self.command_tx {
|
||||
match self.current_item_index {
|
||||
0 => command_tx.send(Action::SetMode(Mode::Explorer(false)))?,
|
||||
_ => command_tx.send(Action::SetMode(Mode::Empty(false)))?,
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_active(&mut self) -> Result<()> {
|
||||
self.is_active = true;
|
||||
if let Some(command_tx) = &self.command_tx {
|
||||
match self.current_item_index {
|
||||
0 => command_tx.send(Action::SetMode(Mode::Explorer(false)))?,
|
||||
_ => command_tx.send(Action::SetMode(Mode::Empty(false)))?,
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unset_active(&mut self) -> Result<()> {
|
||||
self.is_active = false;
|
||||
if let Some(command_tx) = &self.command_tx {
|
||||
match self.current_item_index {
|
||||
0 => command_tx.send(Action::SetMode(Mode::Explorer(true)))?,
|
||||
_ => command_tx.send(Action::SetMode(Mode::Empty(true)))?,
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Menu {
|
||||
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||
self.command_tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
match key.code {
|
||||
KeyCode::Up | KeyCode::Char('k') if self.is_active => self.move_current_up()?,
|
||||
KeyCode::Down | KeyCode::Char('j') if self.is_active => self.move_current_down()?,
|
||||
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active => self.unset_active()?,
|
||||
KeyCode::Esc if !self.is_active => self.set_active()?,
|
||||
_ => {},
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [menu, _] = super::menu_layout(area);
|
||||
let width = menu.as_size().width as usize;
|
||||
|
||||
let mut text = vec![];
|
||||
for (position, item) in self.items.iter().enumerate() {
|
||||
let active = position == self.current_item_index;
|
||||
let line = Line::raw(format!("{:^width$}", item))
|
||||
.style(self.palette.create_text_style(active))
|
||||
.centered();
|
||||
text.push(line);
|
||||
}
|
||||
|
||||
let (color, border_type) = self.palette.create_border_style(self.is_active);
|
||||
let block = Block::bordered()
|
||||
.border_style(color)
|
||||
.border_type(border_type);
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
frame.render_widget(paragraph, menu);
|
||||
Ok(())
|
||||
}
|
||||
}
|
95
src/components/mod.rs
Normal file
95
src/components/mod.rs
Normal file
@ -0,0 +1,95 @@
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout, Rect, Size},
|
||||
Frame,
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{palette, action::Action, config::Config, tui::Event};
|
||||
|
||||
pub mod fps;
|
||||
pub mod health;
|
||||
pub mod menu;
|
||||
pub mod version;
|
||||
pub mod explorer;
|
||||
pub mod empty;
|
||||
|
||||
pub trait Component {
|
||||
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||
let _ = tx;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
let _ = config;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Action>> {
|
||||
let action = match event {
|
||||
Some(Event::Key(key_event)) => self.handle_key_event(key_event)?,
|
||||
Some(Event::Mouse(mouse_event)) => self.handle_mouse_event(mouse_event)?,
|
||||
_ => None,
|
||||
};
|
||||
Ok(action)
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
let _ = key;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn handle_mouse_event(&mut self, mouse: MouseEvent) -> Result<Option<Action>> {
|
||||
let _ = mouse;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn init(&mut self, area: Size) -> Result<()> {
|
||||
let _ = area;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
let _ = action;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()>;
|
||||
}
|
||||
|
||||
pub fn global_layout(area: Rect) -> [Rect; 2] {
|
||||
Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(1),
|
||||
]).areas(area)
|
||||
}
|
||||
|
||||
pub fn header_layout(area: Rect) -> [Rect; 2] {
|
||||
let [header, _] = global_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]).areas(header)
|
||||
}
|
||||
|
||||
pub fn main_layout(area: Rect) -> [Rect; 2] {
|
||||
let [_, main] = global_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Max(30),
|
||||
Constraint::Fill(1),
|
||||
]).areas(main)
|
||||
}
|
||||
|
||||
pub fn menu_layout(area: Rect) -> [Rect; 2] {
|
||||
let [menu, _] = main_layout(area);
|
||||
Layout::vertical([
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(5),
|
||||
]).areas(menu)
|
||||
}
|
||||
|
||||
pub fn screen_layout(area: Rect) -> Rect {
|
||||
let [_, screen] = main_layout(area);
|
||||
screen
|
||||
}
|
81
src/components/version.rs
Normal file
81
src/components/version.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
text::Line,
|
||||
widgets::{Block, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::Component;
|
||||
use crate::{
|
||||
action::Action, palette::StylePalette, widgets::OghamCenter
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Version {
|
||||
genesis_hash: Option<String>,
|
||||
node_version: Option<String>,
|
||||
chain_name: Option<String>,
|
||||
palette: StylePalette,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
fn set_chain_name(&mut self, chain_name: Option<String>) -> Result<()> {
|
||||
self.chain_name = chain_name;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_node_version(&mut self, node_version: Option<String>) -> Result<()> {
|
||||
self.node_version = node_version;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_genesis_hash(&mut self, genesis_hash: Option<String>) -> Result<()> {
|
||||
self.genesis_hash = genesis_hash;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepared_genesis_hash(&self) -> String {
|
||||
if self.genesis_hash.is_some() {
|
||||
let genesis_hash = self.genesis_hash.clone().unwrap();
|
||||
let len = genesis_hash.len();
|
||||
format!("Genesis: {}...{}", &genesis_hash[0..4], &genesis_hash[len-6..])
|
||||
} else {
|
||||
OghamCenter::default().to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Version {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetChainName(name) => self.set_chain_name(name)?,
|
||||
Action::SetNodeVersion(version) => self.set_node_version(version)?,
|
||||
Action::SetGenesisHash(genesis) => self.set_genesis_hash(genesis)?,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, version] = super::menu_layout(area);
|
||||
|
||||
let text_style = self.palette.create_text_style(false);
|
||||
let (border_style, border_type) = self.palette.create_border_style(false);
|
||||
let text = vec![
|
||||
Line::styled(self.chain_name.clone().unwrap_or(OghamCenter::default().to_string()), text_style),
|
||||
Line::styled(self.node_version.clone().unwrap_or(OghamCenter::default().to_string()), text_style),
|
||||
Line::styled(self.prepared_genesis_hash(), text_style),
|
||||
];
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
)
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
|
||||
frame.render_widget(paragraph, version);
|
||||
Ok(())
|
||||
}
|
||||
}
|
454
src/config.rs
Normal file
454
src/config.rs
Normal file
@ -0,0 +1,454 @@
|
||||
use std::{collections::HashMap, env, path::PathBuf};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use directories::ProjectDirs;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use serde::{de::Deserializer, Deserialize};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{action::Action, app::Mode};
|
||||
|
||||
const CONFIG: &str = include_str!("../config/config.json5");
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct AppConfig {
|
||||
#[serde(default)]
|
||||
pub data_dir: PathBuf,
|
||||
#[serde(default)]
|
||||
pub config_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct Config {
|
||||
#[allow(unused)]
|
||||
#[serde(default, flatten)]
|
||||
pub config: AppConfig,
|
||||
#[serde(default)]
|
||||
pub keybindings: KeyBindings,
|
||||
#[serde(default)]
|
||||
pub styles: Styles,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME")
|
||||
.to_uppercase()
|
||||
.to_string();
|
||||
pub static ref DATA_FOLDER: Option<PathBuf> =
|
||||
env::var(format!("{}_DATA", PROJECT_NAME.clone()))
|
||||
.ok()
|
||||
.map(PathBuf::from);
|
||||
pub static ref CONFIG_FOLDER: Option<PathBuf> =
|
||||
env::var(format!("{}_CONFIG", PROJECT_NAME.clone()))
|
||||
.ok()
|
||||
.map(PathBuf::from);
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Result<Self, config::ConfigError> {
|
||||
let default_config: Config = json5::from_str(CONFIG).unwrap();
|
||||
let data_dir = get_data_dir();
|
||||
let config_dir = get_config_dir();
|
||||
|
||||
let mut builder = config::Config::builder()
|
||||
.set_default("data_dir", data_dir.to_str().unwrap())?
|
||||
.set_default("config_dir", config_dir.to_str().unwrap())?;
|
||||
|
||||
let config_files = [
|
||||
("config.json5", config::FileFormat::Json5),
|
||||
("config.json", config::FileFormat::Json),
|
||||
("config.yaml", config::FileFormat::Yaml),
|
||||
("config.toml", config::FileFormat::Toml),
|
||||
("config.ini", config::FileFormat::Ini),
|
||||
];
|
||||
|
||||
let mut found_config = false;
|
||||
for (file, format) in &config_files {
|
||||
let source = config::File::from(config_dir.join(file))
|
||||
.format(*format)
|
||||
.required(false);
|
||||
builder = builder.add_source(source);
|
||||
if config_dir.join(file).exists() {
|
||||
found_config = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !found_config {
|
||||
error!("no configuration file found, application may not behave as expected");
|
||||
}
|
||||
|
||||
let mut cfg: Self = builder.build()?.try_deserialize()?;
|
||||
|
||||
for (mode, default_bindings) in default_config.keybindings.iter() {
|
||||
let user_bindings = cfg.keybindings.entry(*mode).or_default();
|
||||
for (key, cmd) in default_bindings.iter() {
|
||||
user_bindings
|
||||
.entry(key.clone())
|
||||
.or_insert_with(|| cmd.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (mode, default_styles) in default_config.styles.iter() {
|
||||
let user_styles = cfg.styles.entry(*mode).or_default();
|
||||
for (style_key, style) in default_styles.iter() {
|
||||
user_styles
|
||||
.entry(style_key.clone())
|
||||
.or_insert(*style);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_data_dir() -> PathBuf {
|
||||
if let Some(data) = DATA_FOLDER.clone() {
|
||||
data
|
||||
} else if let Some(proj_dir) = project_directory() {
|
||||
proj_dir.data_local_dir().to_path_buf()
|
||||
} else {
|
||||
PathBuf::from(".").join(".data")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_config_dir() -> PathBuf {
|
||||
if let Some(config) = CONFIG_FOLDER.clone() {
|
||||
config
|
||||
} else if let Some(proj_dir) = project_directory() {
|
||||
proj_dir.config_local_dir().to_path_buf()
|
||||
} else {
|
||||
PathBuf::from(".").join(".config")
|
||||
}
|
||||
}
|
||||
|
||||
fn project_directory() -> Option<ProjectDirs> {
|
||||
ProjectDirs::from("com", "ghost", env!("CARGO_PKG_NAME"))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct KeyBindings(pub HashMap<Mode, HashMap<Vec<KeyEvent>, Action>>);
|
||||
|
||||
impl<'de> Deserialize<'de> for KeyBindings {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where D: Deserializer<'de>,
|
||||
{
|
||||
let parsed_map = HashMap::<Mode, HashMap<String, Action>>::deserialize(deserializer)?;
|
||||
|
||||
let keybindings = parsed_map
|
||||
.into_iter()
|
||||
.map(|(mode, inner_map)| {
|
||||
let converted_inner_map = inner_map
|
||||
.into_iter()
|
||||
.map(|(key_str, cmd)| (parse_key_sequence(&key_str).unwrap(), cmd))
|
||||
.collect();
|
||||
(mode, converted_inner_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(KeyBindings(keybindings))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
|
||||
let raw_lower = raw.to_ascii_lowercase();
|
||||
let (remaining, modifiers) = extract_modifiers(&raw_lower);
|
||||
parse_key_code_with_modifiers(remaining, modifiers)
|
||||
}
|
||||
|
||||
fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
|
||||
let mut modifiers = KeyModifiers::empty();
|
||||
let mut current = raw;
|
||||
|
||||
loop {
|
||||
match current {
|
||||
rest if rest.starts_with("alt-") => {
|
||||
modifiers.insert(KeyModifiers::ALT);
|
||||
current = &rest[4..];
|
||||
}
|
||||
rest if rest.starts_with("ctrl-") => {
|
||||
modifiers.insert(KeyModifiers::CONTROL);
|
||||
current = &rest[5..];
|
||||
}
|
||||
rest if rest.starts_with("shift-") => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
current = &rest[6..];
|
||||
}
|
||||
_ => break,
|
||||
};
|
||||
}
|
||||
|
||||
(current, modifiers)
|
||||
}
|
||||
|
||||
fn parse_key_code_with_modifiers(
|
||||
raw: &str,
|
||||
mut modifiers: KeyModifiers,
|
||||
) -> Result<KeyEvent, String> {
|
||||
let c = match raw {
|
||||
"esc" => KeyCode::Esc,
|
||||
"enter" => KeyCode::Enter,
|
||||
"left" => KeyCode::Left,
|
||||
"right" => KeyCode::Right,
|
||||
"up" => KeyCode::Up,
|
||||
"down" => KeyCode::Down,
|
||||
"home" => KeyCode::Home,
|
||||
"end" => KeyCode::End,
|
||||
"pageup" => KeyCode::PageUp,
|
||||
"pagedown" => KeyCode::PageDown,
|
||||
"backtab" => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
KeyCode::BackTab
|
||||
},
|
||||
"backspace" => KeyCode::Backspace,
|
||||
"delete" => KeyCode::Delete,
|
||||
"insert" => KeyCode::Insert,
|
||||
"f1" => KeyCode::F(1),
|
||||
"f2" => KeyCode::F(2),
|
||||
"f3" => KeyCode::F(3),
|
||||
"f4" => KeyCode::F(4),
|
||||
"f5" => KeyCode::F(5),
|
||||
"f6" => KeyCode::F(6),
|
||||
"f7" => KeyCode::F(7),
|
||||
"f8" => KeyCode::F(8),
|
||||
"f9" => KeyCode::F(9),
|
||||
"f10" => KeyCode::F(10),
|
||||
"f11" => KeyCode::F(11),
|
||||
"f12" => KeyCode::F(12),
|
||||
"space" => KeyCode::Char(' '),
|
||||
"hyphen" => KeyCode::Char('-'),
|
||||
"minus" => KeyCode::Char('-'),
|
||||
"tab" => KeyCode::Tab,
|
||||
c if c.len() == 1 => {
|
||||
let mut c = c.chars().next().unwrap();
|
||||
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||
c = c.to_ascii_uppercase();
|
||||
}
|
||||
KeyCode::Char(c)
|
||||
}
|
||||
_ => return Err(format!("unable to parse {raw}")),
|
||||
};
|
||||
Ok(KeyEvent::new(c, modifiers))
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
|
||||
let char;
|
||||
let key_code = match key_event.code {
|
||||
KeyCode::Backspace => "backspace",
|
||||
KeyCode::Enter => "enter",
|
||||
KeyCode::Left => "left",
|
||||
KeyCode::Right => "right",
|
||||
KeyCode::Up => "up",
|
||||
KeyCode::Down => "down",
|
||||
KeyCode::Home => "home",
|
||||
KeyCode::End => "end",
|
||||
KeyCode::PageUp => "pageup",
|
||||
KeyCode::PageDown => "pagedown",
|
||||
KeyCode::Tab => "tab",
|
||||
KeyCode::BackTab => "backtab",
|
||||
KeyCode::Delete => "delete",
|
||||
KeyCode::Insert => "insert",
|
||||
KeyCode::F(c) => {
|
||||
char = format!("f({c})");
|
||||
&char
|
||||
},
|
||||
KeyCode::Char(' ') => "space",
|
||||
KeyCode::Char(c) => {
|
||||
char = c.to_string();
|
||||
&char
|
||||
},
|
||||
KeyCode::Esc => "esc",
|
||||
KeyCode::Null => "",
|
||||
KeyCode::CapsLock => "",
|
||||
KeyCode::Menu => "",
|
||||
KeyCode::ScrollLock => "",
|
||||
KeyCode::Media(_) => "",
|
||||
KeyCode::NumLock => "",
|
||||
KeyCode::PrintScreen => "",
|
||||
KeyCode::Pause => "",
|
||||
KeyCode::KeypadBegin => "",
|
||||
KeyCode::Modifier(_) => "",
|
||||
};
|
||||
|
||||
let mut modifiers = Vec::with_capacity(3);
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
modifiers.push("ctrl");
|
||||
}
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
|
||||
modifiers.push("shift");
|
||||
}
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::ALT) {
|
||||
modifiers.push("alt");
|
||||
}
|
||||
|
||||
let mut key = modifiers.join("-");
|
||||
if !key.is_empty() {
|
||||
key.push('-');
|
||||
}
|
||||
key.push_str(key_code);
|
||||
|
||||
key
|
||||
}
|
||||
|
||||
pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
|
||||
if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
|
||||
return Err(format!("unable to parse `{}`", raw));
|
||||
}
|
||||
let raw = if !raw.contains("><") {
|
||||
let raw = raw.strip_prefix('<').unwrap_or(raw);
|
||||
let raw = raw.strip_prefix('>').unwrap_or(raw);
|
||||
raw
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
let sequence = raw
|
||||
.split("><")
|
||||
.map(|seq| {
|
||||
if let Some(s) = seq.strip_prefix('<') {
|
||||
s
|
||||
} else if let Some(s) = seq.strip_suffix('>') {
|
||||
s
|
||||
} else {
|
||||
seq
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
sequence.into_iter().map(parse_key_event).collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
|
||||
|
||||
impl<'de> Deserialize<'de> for Styles {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
{
|
||||
let parsed_map = HashMap::<Mode, HashMap<String, String>>::deserialize(deserializer)?;
|
||||
|
||||
let styles = parsed_map
|
||||
.into_iter()
|
||||
.map(|(mode, inner_map)| {
|
||||
let converted_inner_map = inner_map
|
||||
.into_iter()
|
||||
.map(|(str, style)| (str, parse_style(&style)))
|
||||
.collect();
|
||||
(mode, converted_inner_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Styles(styles))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_style(line: &str) -> Style {
|
||||
let (foreground, background) =
|
||||
line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
|
||||
let foreground = process_color_string(foreground);
|
||||
let background = process_color_string(&background.replace("on ", ""));
|
||||
|
||||
let mut style = Style::default();
|
||||
if let Some(fg) = parse_color(&foreground.0) {
|
||||
style = style.fg(fg);
|
||||
}
|
||||
if let Some(bg) = parse_color(&background.0) {
|
||||
style = style.bg(bg);
|
||||
}
|
||||
|
||||
style = style.add_modifier(foreground.1 | background.1);
|
||||
style
|
||||
}
|
||||
|
||||
fn process_color_string(color_str: &str) -> (String, Modifier) {
|
||||
let color = color_str
|
||||
.replace("grey", "gray")
|
||||
.replace("bright", "")
|
||||
.replace("bold", "")
|
||||
.replace("underline", "")
|
||||
.replace("inverse", "");
|
||||
|
||||
let mut modifiers = Modifier::empty();
|
||||
if color_str.contains("underline") {
|
||||
modifiers |= Modifier::UNDERLINED;
|
||||
}
|
||||
if color_str.contains("bold") {
|
||||
modifiers |= Modifier::BOLD;
|
||||
}
|
||||
if color_str.contains("inverse") {
|
||||
modifiers |= Modifier::REVERSED;
|
||||
}
|
||||
|
||||
(color, modifiers)
|
||||
}
|
||||
|
||||
fn parse_color(s: &str) -> Option<Color> {
|
||||
let s = s.trim_start();
|
||||
let s = s.trim_end();
|
||||
|
||||
if s.contains("bright color") {
|
||||
let s = s.trim_start_matches("bright ");
|
||||
let c = s
|
||||
.trim_start_matches("color")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c.wrapping_shl(8)))
|
||||
} else if s.contains("color") {
|
||||
let c = s
|
||||
.trim_start_matches("color")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c))
|
||||
} else if s.contains("gray") {
|
||||
let c = 232
|
||||
+ s.trim_start_matches("gray")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c))
|
||||
} else if s.contains("rgb") {
|
||||
let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
|
||||
let c = 16 + red * 36 + green * 6 + blue;
|
||||
Some(Color::Indexed(c))
|
||||
} else if s == "bold black" {
|
||||
Some(Color::Indexed(8))
|
||||
} else if s == "bold red" {
|
||||
Some(Color::Indexed(9))
|
||||
} else if s == "bold green" {
|
||||
Some(Color::Indexed(10))
|
||||
} else if s == "bold yellow" {
|
||||
Some(Color::Indexed(11))
|
||||
} else if s == "bold blue" {
|
||||
Some(Color::Indexed(12))
|
||||
} else if s == "bold magenta" {
|
||||
Some(Color::Indexed(13))
|
||||
} else if s == "bold cyan" {
|
||||
Some(Color::Indexed(14))
|
||||
} else if s == "bold white" {
|
||||
Some(Color::Indexed(15))
|
||||
} else if s == "black" {
|
||||
Some(Color::Indexed(0))
|
||||
} else if s == "red" {
|
||||
Some(Color::Indexed(1))
|
||||
} else if s == "green" {
|
||||
Some(Color::Indexed(2))
|
||||
} else if s == "yellow" {
|
||||
Some(Color::Indexed(3))
|
||||
} else if s == "blue" {
|
||||
Some(Color::Indexed(4))
|
||||
} else if s == "magenta" {
|
||||
Some(Color::Indexed(5))
|
||||
} else if s == "cyan" {
|
||||
Some(Color::Indexed(6))
|
||||
} else if s == "white" {
|
||||
Some(Color::Indexed(7))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
67
src/errors.rs
Normal file
67
src/errors.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use std::env;
|
||||
|
||||
use color_eyre::Result;
|
||||
use tracing::error;
|
||||
|
||||
pub fn init() -> Result<()> {
|
||||
let (_panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
|
||||
.panic_section(format!(
|
||||
"This is a bug. Consider reporting it at {}",
|
||||
env!("CARGO_PKG_REPOSITORY")
|
||||
))
|
||||
.capture_span_trace_by_default(false)
|
||||
.display_location_section(false)
|
||||
.display_env_section(false)
|
||||
.into_hooks();
|
||||
eyre_hook.install()?;
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
if let Ok(mut t) = crate::tui::Tui::new() {
|
||||
if let Err(r) = t.exit() {
|
||||
error!("Unable to exit Terminal: {:?}", r);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
use human_panic::{handle_dump, metadata, print_msg};
|
||||
let metadata = metadata!();
|
||||
let file_path = handle_dump(&metadata, panic_info);
|
||||
print_msg(file_path, &metadata)
|
||||
.expect("human-panic: printing error message to console failed");
|
||||
eprint!("{}", _panic_hook.panic_report(panic_info));
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
better_panic::Settings::auto()
|
||||
.most_recent_first(false)
|
||||
.lineno_suffix(true)
|
||||
.verbosity(better_panic::Verbosity::Full)
|
||||
.create_panic_handler()(panic_info);
|
||||
}
|
||||
|
||||
std::process::exit(libc::EXIT_FAILURE);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! trace_dbg {
|
||||
(target: $target:expr, level: $level:expr, $ex:expr) => {{
|
||||
match $ex {
|
||||
value => {
|
||||
tracing::event!(target: $target, $level, ?value, stringify!($ex));
|
||||
value
|
||||
}
|
||||
}
|
||||
}};
|
||||
(level: $level:expr, $ex:expr) => {
|
||||
trace_dbg!(target: module_path!(), level: $level, $ex)
|
||||
};
|
||||
(target: $target:expr, $ex:expr) => {
|
||||
trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex)
|
||||
};
|
||||
($ex:expr) => {
|
||||
trace_dbg!(level: tracing::Level::DEBUG, $ex)
|
||||
};
|
||||
}
|
39
src/logging.rs
Normal file
39
src/logging.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use color_eyre::Result;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
|
||||
use crate::config;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref LOG_ENV: String = format!("{}_LOG_LEVEL", config::PROJECT_NAME.clone());
|
||||
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME"));
|
||||
}
|
||||
|
||||
pub fn init() -> Result<()> {
|
||||
let directory = config::get_data_dir();
|
||||
std::fs::create_dir_all(directory.clone())?;
|
||||
|
||||
let log_path = directory.join(LOG_FILE.clone());
|
||||
let log_file = std::fs::File::create(log_path)?;
|
||||
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(tracing::Level::INFO.into());
|
||||
let env_filter = env_filter
|
||||
.try_from_env()
|
||||
.or_else(|_| env_filter.with_env_var(LOG_ENV.clone()).from_env())?;
|
||||
|
||||
let file_subscriber = fmt::layer()
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(log_file)
|
||||
.with_target(false)
|
||||
.with_ansi(false)
|
||||
.with_filter(env_filter);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(file_subscriber)
|
||||
.with(ErrorLayer::default())
|
||||
.try_init()?;
|
||||
|
||||
Ok(())
|
||||
}
|
53
src/main.rs
Normal file
53
src/main.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
|
||||
mod action;
|
||||
mod app;
|
||||
mod cli;
|
||||
mod components;
|
||||
mod config;
|
||||
mod errors;
|
||||
mod logging;
|
||||
mod tui;
|
||||
mod network;
|
||||
mod widgets;
|
||||
mod types;
|
||||
mod palette;
|
||||
|
||||
#[tokio::main]
|
||||
async fn start_tokio(
|
||||
io_rx: std::sync::mpsc::Receiver<action::Action>,
|
||||
network: &mut network::Network,
|
||||
) {
|
||||
while let Ok(io_event) = io_rx.recv() {
|
||||
let _ = network.handle_network_event(io_event).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
crate::errors::init()?;
|
||||
crate::logging::init()?;
|
||||
|
||||
let args = cli::Cli::parse();
|
||||
let (sync_io_tx, sync_io_rx) = std::sync::mpsc::channel();
|
||||
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let cloned_action_tx = action_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut network = network::Network::new(cloned_action_tx)
|
||||
.with_url(&args.rpc_endpoint)
|
||||
.with_timeout(args.timeout);
|
||||
start_tokio(sync_io_rx, &mut network);
|
||||
});
|
||||
|
||||
app::App::new(sync_io_tx, action_tx, action_rx)?
|
||||
.with_frame_rate(args.frame_rate)
|
||||
.with_tick_rate(args.tick_rate)
|
||||
.with_mouse(args.mouse_needed)
|
||||
.with_paste(args.paste_needed)
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
33
src/network/active_era.rs
Normal file
33
src/network/active_era.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use color_eyre::Result;
|
||||
use codec::Decode;
|
||||
|
||||
use crate::network::GhostRequest;
|
||||
use crate::action::Action;
|
||||
use crate::rpc_params;
|
||||
use crate::types::{era::EraInfo, storage::GhostStorage};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ActiveEraRequest<'a>(pub GhostRequest<'a>);
|
||||
impl<'a> ActiveEraRequest<'a> {
|
||||
pub async fn send(self) -> Result<()> {
|
||||
let storage_key = GhostStorage::new()
|
||||
.with_module("Staking")
|
||||
.with_method("ActiveEra")
|
||||
.build_storage_key();
|
||||
|
||||
let result_hex = self
|
||||
.0
|
||||
.send::<String>("state_getStorage", rpc_params![storage_key])
|
||||
.await
|
||||
.map(|r| {
|
||||
hex::decode(r.result.trim_start_matches("0x")).or::<Vec<u8>>(Ok(vec![]))
|
||||
})
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let active_era = EraInfo::decode(&mut &result_hex[..])?;
|
||||
self.0.action_tx.send(Action::SetActiveEra(active_era))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
20
src/network/chain_name.rs
Normal file
20
src/network/chain_name.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
use crate::network::GhostRequest;
|
||||
use crate::action::Action;
|
||||
use crate::types::params::RpcParams;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChainNameRequest<'a>(pub GhostRequest<'a>);
|
||||
impl<'a> ChainNameRequest<'a> {
|
||||
pub async fn send(self) -> Result<()> {
|
||||
let chain_name = self
|
||||
.0
|
||||
.send::<String>("system_chain", RpcParams::new())
|
||||
.await
|
||||
.ok()
|
||||
.map(|response| response.result);
|
||||
self.0.action_tx.send(Action::SetChainName(chain_name))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
41
src/network/current_epoch.rs
Normal file
41
src/network/current_epoch.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use color_eyre::Result;
|
||||
use codec::Decode;
|
||||
|
||||
use crate::network::GhostRequest;
|
||||
use crate::action::Action;
|
||||
use crate::rpc_params;
|
||||
use crate::types::storage::GhostStorage;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CurrentEpochRequest<'a>(pub GhostRequest<'a>);
|
||||
impl<'a> CurrentEpochRequest<'a> {
|
||||
async fn do_request(&self, module: &str, method: &str) -> Result<u64> {
|
||||
let storage_key = GhostStorage::new()
|
||||
.with_module(module)
|
||||
.with_method(method)
|
||||
.build_storage_key();
|
||||
|
||||
let result_hex = self
|
||||
.0
|
||||
.send::<String>("state_getStorage", rpc_params![storage_key])
|
||||
.await
|
||||
.map(|r| {
|
||||
hex::decode(r.result.trim_start_matches("0x")).or::<Vec<u8>>(Ok(vec![]))
|
||||
})
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let value = u64::decode(&mut &result_hex[..])?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub async fn send(self) -> Result<()> {
|
||||
let current_slot = self.do_request("Babe", "CurrentSlot").await?;
|
||||
let epoch_index = self.do_request("Babe", "EpochIndex").await?;
|
||||
let genesis_slot = self.do_request("Babe", "GenesisSlot").await?;
|
||||
|
||||
let epoch_start_slot = epoch_index * 2_400 + genesis_slot;
|
||||
let progress = current_slot.saturating_sub(epoch_start_slot);
|
||||
self.0.action_tx.send(Action::SetEpoch(epoch_index, progress))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
35
src/network/finalized_block.rs
Normal file
35
src/network/finalized_block.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use color_eyre::Result;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::network::GhostRequest;
|
||||
use crate::action::Action;
|
||||
use crate::rpc_params;
|
||||
use crate::types::block::BlockInfo;
|
||||
use crate::types::params::RpcParams;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LatestBlockResponse {
|
||||
pub block: BlockInfo,
|
||||
// justifications: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FinalizedBlockRequest<'a>(pub GhostRequest<'a>);
|
||||
impl<'a> FinalizedBlockRequest<'a> {
|
||||
pub async fn send(self) -> Result<()> {
|
||||
let finalized_head = self
|
||||
.0
|
||||
.send::<String>("chain_getFinalizedHead", RpcParams::new())
|
||||
.await
|
||||
.map_or(String::new(), |response| response.result);
|
||||
let finalized_block = self
|
||||
.0
|
||||
.send::<LatestBlockResponse>("chain_getBlock", rpc_params![finalized_head.clone()])
|
||||
.await
|
||||
.map_or(BlockInfo::default(), |response| response.result.block);
|
||||
self.0.action_tx.send(Action::SetFinalizedBlock(finalized_head, finalized_block))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
18
src/network/genesis_hash.rs
Normal file
18
src/network/genesis_hash.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
use crate::network::GhostRequest;
|
||||
use crate::action::Action;
|
||||
use crate::rpc_params;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GenesisHashRequest<'a>(pub GhostRequest<'a>);
|
||||
impl<'a> GenesisHashRequest<'a> {
|
||||
pub async fn send(self) -> Result<()> {
|
||||
let genesis_hash = self.0.send::<String>("chain_getBlockHash", rpc_params!["0"])
|
||||
.await
|
||||
.ok()
|
||||
.map(|response| response.result);
|
||||
self.0.action_tx.send(Action::SetGenesisHash(genesis_hash))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
37
src/network/health_check.rs
Normal file
37
src/network/health_check.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use color_eyre::Result;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::network::GhostRequest;
|
||||
use crate::action::Action;
|
||||
use crate::types::params::RpcParams;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HealthCheckResponse {
|
||||
pub peers: u32,
|
||||
pub is_syncing: bool,
|
||||
pub should_have_peers: bool
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HealthCheckRequest<'a>(pub GhostRequest<'a>);
|
||||
impl<'a> HealthCheckRequest<'a> {
|
||||
pub async fn send(self) -> Result<()> {
|
||||
let (peers, is_syncing, should_have_peers) = self
|
||||
.0
|
||||
.send::<HealthCheckResponse>("system_health", RpcParams::new())
|
||||
.await
|
||||
.ok()
|
||||
.map_or((None, false, false), |response| (
|
||||
Some(response.result.peers),
|
||||
response.result.is_syncing,
|
||||
response.result.should_have_peers,
|
||||
));
|
||||
|
||||
self.0.action_tx.send(Action::SetSyncState(
|
||||
peers,
|
||||
is_syncing,
|
||||
should_have_peers))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
35
src/network/latest_block.rs
Normal file
35
src/network/latest_block.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use color_eyre::Result;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::network::GhostRequest;
|
||||
use crate::action::Action;
|
||||
use crate::rpc_params;
|
||||
use crate::types::block::BlockInfo;
|
||||
use crate::types::params::RpcParams;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LatestBlockResponse {
|
||||
pub block: BlockInfo,
|
||||
// justifications: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LatestBlockRequest<'a>(pub GhostRequest<'a>);
|
||||
impl<'a> LatestBlockRequest<'a> {
|
||||
pub async fn send(self) -> Result<()> {
|
||||
let latest_head = self
|
||||
.0
|
||||
.send::<String>("chain_getBlockHash", RpcParams::new())
|
||||
.await
|
||||
.map_or(String::new(), |response| response.result);
|
||||
let latest_block = self
|
||||
.0
|
||||
.send::<LatestBlockResponse>("chain_getBlock", rpc_params![latest_head.clone()])
|
||||
.await
|
||||
.map_or(BlockInfo::default(), |response| response.result.block);
|
||||
self.0.action_tx.send(Action::SetLatestBlock(latest_head, latest_block))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
165
src/network/mod.rs
Normal file
165
src/network/mod.rs
Normal file
@ -0,0 +1,165 @@
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::Client;
|
||||
use color_eyre::Result;
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, de::DeserializeOwned};
|
||||
|
||||
use crate::{action::Action, types::params::RpcParams};
|
||||
|
||||
mod active_era;
|
||||
mod health_check;
|
||||
mod node_name;
|
||||
mod genesis_hash;
|
||||
mod chain_name;
|
||||
mod node_version;
|
||||
mod latest_block;
|
||||
mod finalized_block;
|
||||
mod current_epoch;
|
||||
mod validators;
|
||||
mod tx_pool;
|
||||
|
||||
pub use active_era::ActiveEraRequest;
|
||||
pub use health_check::HealthCheckRequest;
|
||||
pub use node_name::NodeNameRequest;
|
||||
pub use genesis_hash::GenesisHashRequest;
|
||||
pub use chain_name::ChainNameRequest;
|
||||
pub use node_version::NodeVersionRequest;
|
||||
pub use latest_block::LatestBlockRequest;
|
||||
pub use finalized_block::FinalizedBlockRequest;
|
||||
pub use current_epoch::CurrentEpochRequest;
|
||||
pub use validators::ValidatorsRequest;
|
||||
pub use tx_pool::TxPoolRequest;
|
||||
|
||||
static CLIENT: Lazy<Arc<Client>> = Lazy::new(|| Arc::new(Client::new()));
|
||||
const DEFAULT_URL: &str = "http://localhost:9945";
|
||||
|
||||
pub type AppActionSender = UnboundedSender<Action>;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GhostResponse<ResponseType> {
|
||||
result: ResponseType,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GhostRequestBuilder<'a> {
|
||||
action_tx: Option<AppActionSender>,
|
||||
id: u32,
|
||||
url: &'a str,
|
||||
timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl<'a> GhostRequestBuilder<'a> {
|
||||
pub fn with_id(mut self, id: u32) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_url(mut self, url: &'a str) -> Self {
|
||||
self.url = url;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_action_tx(mut self, action_tx: AppActionSender) -> Self {
|
||||
self.action_tx = Some(action_tx);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> GhostRequest<'a> {
|
||||
GhostRequest {
|
||||
action_tx: self.action_tx.expect("channel sender should exist"),
|
||||
id: self.id,
|
||||
url: self.url,
|
||||
timeout: self.timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GhostRequest<'a> {
|
||||
action_tx: AppActionSender,
|
||||
id: u32,
|
||||
url: &'a str,
|
||||
timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl<'a> GhostRequest<'a> {
|
||||
pub async fn send<ResponseType>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: RpcParams,
|
||||
) -> Result<GhostResponse<ResponseType>>
|
||||
where
|
||||
ResponseType: DeserializeOwned,
|
||||
{
|
||||
Ok(CLIENT
|
||||
.post(self.url)
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.body(format!("{{\"id\":{},\"jsonrpc\":\"2.0\",\"method\":{:?},\"params\":{}}}",
|
||||
self.id, method, params.build()))
|
||||
.timeout(self.timeout)
|
||||
.send()
|
||||
.await?
|
||||
.json::<GhostResponse<ResponseType>>()
|
||||
.await?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Network<'a> {
|
||||
action_tx: AppActionSender,
|
||||
timeout: std::time::Duration,
|
||||
internal_randomness: rand::rngs::ThreadRng,
|
||||
url: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Network<'a> {
|
||||
pub fn new(action_tx: AppActionSender) -> Self {
|
||||
Self {
|
||||
action_tx,
|
||||
timeout: Default::default(),
|
||||
internal_randomness: rand::thread_rng(),
|
||||
url: DEFAULT_URL,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_url(mut self, url: &'a str) -> Self {
|
||||
self.url = url;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_timeout(mut self, timeout: u64) -> Self {
|
||||
self.timeout = std::time::Duration::from_secs(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn handle_network_event(&mut self, io_event: Action) -> Result<()> {
|
||||
let request = GhostRequestBuilder::default()
|
||||
.with_action_tx(self.action_tx.clone())
|
||||
.with_id(self.internal_randomness.next_u32())
|
||||
.with_url(self.url)
|
||||
.with_timeout(self.timeout)
|
||||
.build();
|
||||
|
||||
match io_event {
|
||||
Action::GetSyncState => HealthCheckRequest(request).send().await,
|
||||
Action::GetNodeName => NodeNameRequest(request).send().await,
|
||||
Action::GetGenesisHash => GenesisHashRequest(request).send().await,
|
||||
Action::GetChainName => ChainNameRequest(request).send().await,
|
||||
Action::GetNodeVersion => NodeVersionRequest(request).send().await,
|
||||
Action::GetLatestBlock => LatestBlockRequest(request).send().await,
|
||||
Action::GetFinalizedBlock => FinalizedBlockRequest(request).send().await,
|
||||
Action::GetActiveEra => ActiveEraRequest(request).send().await,
|
||||
Action::GetEpoch => CurrentEpochRequest(request).send().await,
|
||||
Action::GetValidators => ValidatorsRequest(request).send().await,
|
||||
Action::GetPendingExtrinsics => TxPoolRequest(request).send().await,
|
||||
_ => Ok(())
|
||||
}
|
||||
}
|
||||
}
|
20
src/network/node_name.rs
Normal file
20
src/network/node_name.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
use crate::network::GhostRequest;
|
||||
use crate::action::Action;
|
||||
use crate::types::params::RpcParams;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeNameRequest<'a>(pub GhostRequest<'a>);
|
||||
impl<'a> NodeNameRequest<'a> {
|
||||
pub async fn send(self) -> Result<()> {
|
||||
let name = self
|
||||
.0
|
||||
.send::<String>("system_name", RpcParams::new())
|
||||
.await
|
||||
.ok()
|
||||
.map(|response| response.result);
|
||||
self.0.action_tx.send(Action::SetNodeName(name))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
20
src/network/node_version.rs
Normal file
20
src/network/node_version.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
use crate::network::GhostRequest;
|
||||
use crate::action::Action;
|
||||
use crate::types::params::RpcParams;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeVersionRequest<'a>(pub GhostRequest<'a>);
|
||||
impl<'a> NodeVersionRequest<'a> {
|
||||
pub async fn send(self) -> Result<()> {
|
||||
let version = self
|
||||
.0
|
||||
.send::<String>("system_version", RpcParams::new())
|
||||
.await
|
||||
.ok()
|
||||
.map(|response| response.result);
|
||||
self.0.action_tx.send(Action::SetNodeVersion(version))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
0
src/network/runtime_version.rs
Normal file
0
src/network/runtime_version.rs
Normal file
21
src/network/tx_pool.rs
Normal file
21
src/network/tx_pool.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
use crate::network::GhostRequest;
|
||||
use crate::action::Action;
|
||||
use crate::types::params::RpcParams;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TxPoolRequest<'a>(pub GhostRequest<'a>);
|
||||
impl<'a> TxPoolRequest<'a> {
|
||||
pub async fn send(self) -> Result<()> {
|
||||
let tx_pool = self
|
||||
.0
|
||||
.send::<Vec<String>>("author_pendingExtrinsics", RpcParams::new())
|
||||
.await
|
||||
.ok()
|
||||
.map(|response| response.result)
|
||||
.unwrap_or_default();
|
||||
self.0.action_tx.send(Action::SetPendingExtrinsicsLength(tx_pool.len()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
37
src/network/validators.rs
Normal file
37
src/network/validators.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use color_eyre::Result;
|
||||
use codec::Decode;
|
||||
use sp_core::crypto::{AccountId32, Ss58AddressFormat, Ss58Codec};
|
||||
|
||||
use crate::network::GhostRequest;
|
||||
use crate::action::Action;
|
||||
use crate::rpc_params;
|
||||
use crate::types::storage::GhostStorage;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ValidatorsRequest<'a>(pub GhostRequest<'a>);
|
||||
impl<'a> ValidatorsRequest<'a> {
|
||||
pub async fn send(self) -> Result<()> {
|
||||
let storage_key = GhostStorage::new()
|
||||
.with_module("Session")
|
||||
.with_method("Validators")
|
||||
.build_storage_key();
|
||||
|
||||
let result_hex = self.0.send::<String>("state_getStorage", rpc_params![storage_key])
|
||||
.await
|
||||
.map(|response| {
|
||||
hex::decode(response.result.trim_start_matches("0x"))
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap();
|
||||
let validators = <Vec<AccountId32>>::decode(&mut &result_hex[..])
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|v| v.to_ss58check_with_version(Ss58AddressFormat::custom(1996)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.0.action_tx.send(Action::SetValidators(validators))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
103
src/palette.rs
Normal file
103
src/palette.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use ratatui::style::{Style, Color, Modifier};
|
||||
use ratatui::widgets::block::BorderType;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StylePalette {
|
||||
background: Option<Color>,
|
||||
foreground: Option<Color>,
|
||||
modifiers: Vec<Modifier>,
|
||||
|
||||
background_hover: Option<Color>,
|
||||
foreground_hover: Option<Color>,
|
||||
modifiers_hover: Vec<Modifier>,
|
||||
|
||||
border_color: Option<Color>,
|
||||
title_color: Option<Color>,
|
||||
border_type: BorderType,
|
||||
|
||||
border_color_hover: Option<Color>,
|
||||
//title_color_hover: Option<Color>,
|
||||
border_type_hover: BorderType,
|
||||
}
|
||||
|
||||
impl Default for StylePalette {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl StylePalette {
|
||||
// TODO: make read from the config by default
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
background: None,
|
||||
foreground: None,
|
||||
modifiers: Vec::new(),
|
||||
|
||||
background_hover: Some(Color::Blue),
|
||||
foreground_hover: Some(Color::Yellow),
|
||||
modifiers_hover: vec![
|
||||
Modifier::ITALIC,
|
||||
Modifier::BOLD,
|
||||
],
|
||||
|
||||
border_color: Some(Color::Blue),
|
||||
title_color: Some(Color::Blue),
|
||||
border_type: BorderType::Plain,
|
||||
|
||||
border_color_hover: Some(Color::Blue),
|
||||
//title_color_hover: Some(Color::Blue),
|
||||
border_type_hover: BorderType::Double,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn foreground_hover(&self) -> Color {
|
||||
self.foreground_hover.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn create_text_style(&mut self, active: bool) -> Style {
|
||||
if active {
|
||||
self.create_text_style_hover()
|
||||
} else {
|
||||
self.create_text_style_normal()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_border_style(&self, active: bool) -> (Color, BorderType) {
|
||||
if active {
|
||||
(
|
||||
self.border_color_hover.unwrap_or_default(),
|
||||
self.border_type_hover,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
self.border_color.unwrap_or_default(),
|
||||
self.border_type,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_title_style(&mut self) -> Style {
|
||||
Style::default().fg(self.title_color.unwrap_or_default())
|
||||
}
|
||||
|
||||
fn create_text_style_normal(&self) -> Style {
|
||||
let mut style = Style::default()
|
||||
.fg(self.foreground.unwrap_or_default())
|
||||
.bg(self.background.unwrap_or_default());
|
||||
for modifier in self.modifiers.iter() {
|
||||
style = style.add_modifier(*modifier);
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
fn create_text_style_hover(&self) -> Style {
|
||||
let mut style = Style::default()
|
||||
.fg(self.foreground_hover.unwrap_or_default())
|
||||
.bg(self.background_hover.unwrap_or_default());
|
||||
for modifier in self.modifiers_hover.iter() {
|
||||
style = style.add_modifier(*modifier);
|
||||
}
|
||||
style
|
||||
}
|
||||
}
|
220
src/tui.rs
Normal file
220
src/tui.rs
Normal file
@ -0,0 +1,220 @@
|
||||
use std::{
|
||||
io::{stdout, Stdout},
|
||||
ops::{Deref, DerefMut},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{
|
||||
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste,
|
||||
EnableMouseCapture, Event as CrosstermEvent, EventStream, KeyEvent,
|
||||
KeyEventKind, MouseEvent,
|
||||
},
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ratatui::backend::CrosstermBackend as Backend;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tokio::{
|
||||
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
task::JoinHandle,
|
||||
time::interval,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum Event {
|
||||
Init,
|
||||
Quit,
|
||||
Error,
|
||||
Closed,
|
||||
Tick,
|
||||
Render,
|
||||
FocusGained,
|
||||
FocusLost,
|
||||
Paste(String),
|
||||
Key(KeyEvent),
|
||||
Mouse(MouseEvent),
|
||||
Resize(u16, u16),
|
||||
Node,
|
||||
FastNode,
|
||||
Runtime,
|
||||
}
|
||||
|
||||
pub struct Tui {
|
||||
pub terminal: ratatui::Terminal<Backend<Stdout>>,
|
||||
pub task: JoinHandle<()>,
|
||||
pub cancellation_token: CancellationToken,
|
||||
pub event_rx: UnboundedReceiver<Event>,
|
||||
pub event_tx: UnboundedSender<Event>,
|
||||
pub frame_rate: f64,
|
||||
pub tick_rate: f64,
|
||||
pub mouse: bool,
|
||||
pub paste: bool,
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
pub fn new() -> Result<Self> {
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
Ok(Self {
|
||||
terminal: ratatui::Terminal::new(Backend::new(stdout()))?,
|
||||
task: tokio::spawn(async {}),
|
||||
cancellation_token: CancellationToken::new(),
|
||||
event_rx,
|
||||
event_tx,
|
||||
frame_rate: 60.0,
|
||||
tick_rate: 4.0,
|
||||
mouse: false,
|
||||
paste: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
self.cancel();
|
||||
self.cancellation_token = CancellationToken::new();
|
||||
|
||||
let event_loop = Self::event_loop(
|
||||
self.event_tx.clone(),
|
||||
self.cancellation_token.clone(),
|
||||
self.tick_rate,
|
||||
self.frame_rate,
|
||||
);
|
||||
self.task = tokio::spawn(async {
|
||||
event_loop.await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn event_loop(
|
||||
event_tx: UnboundedSender<Event>,
|
||||
cancellation_token: CancellationToken,
|
||||
tick_rate: f64,
|
||||
frame_rate: f64,
|
||||
) {
|
||||
let mut event_stream = EventStream::new();
|
||||
let mut tick_interval = interval(Duration::from_secs_f64(1.0 / tick_rate));
|
||||
let mut render_interval = interval(Duration::from_secs_f64(1.0 / frame_rate));
|
||||
|
||||
let mut normal_node_interval = interval(Duration::from_secs_f64(9.0));
|
||||
let mut fast_node_interval = interval(Duration::from_secs_f64(9.0));
|
||||
let mut runtime_interval = interval(Duration::from_secs_f64(1.69));
|
||||
|
||||
event_tx
|
||||
.send(Event::Init)
|
||||
.expect("failed to send init event");
|
||||
|
||||
loop {
|
||||
let event = tokio::select! {
|
||||
_ = cancellation_token.cancelled() => {
|
||||
break;
|
||||
},
|
||||
_ = tick_interval.tick() => Event::Tick,
|
||||
_ = render_interval.tick() => Event::Render,
|
||||
_ = normal_node_interval.tick() => Event::Node,
|
||||
_ = fast_node_interval.tick() => Event::FastNode,
|
||||
_ = runtime_interval.tick() => Event::Runtime,
|
||||
crossterm_event = event_stream.next().fuse() => match crossterm_event {
|
||||
Some(Ok(event)) => match event {
|
||||
CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => Event::Key(key),
|
||||
CrosstermEvent::Mouse(mouse) => Event::Mouse(mouse),
|
||||
CrosstermEvent::Resize(x, y) => Event::Resize(x, y),
|
||||
CrosstermEvent::FocusLost => Event::FocusLost,
|
||||
CrosstermEvent::FocusGained => Event::FocusGained,
|
||||
CrosstermEvent::Paste(s) => Event::Paste(s),
|
||||
_ => continue,
|
||||
}
|
||||
Some(Err(_)) => Event::Error,
|
||||
None => break,
|
||||
},
|
||||
};
|
||||
|
||||
if event_tx.send(event).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<()> {
|
||||
self.cancel();
|
||||
let mut counter = 0;
|
||||
while !self.task.is_finished() {
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
counter += 1;
|
||||
if counter > 50 {
|
||||
self.task.abort();
|
||||
}
|
||||
if counter > 100 {
|
||||
error!("failed to abort task in 100 milliseconds for unknown reason");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn enter(&mut self) -> Result<()> {
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
crossterm::execute!(stdout(), EnterAlternateScreen, cursor::Hide)?;
|
||||
if self.mouse {
|
||||
crossterm::execute!(stdout(), EnableMouseCapture)?;
|
||||
}
|
||||
if self.paste {
|
||||
crossterm::execute!(stdout(), EnableBracketedPaste)?;
|
||||
}
|
||||
self.start();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exit(&mut self) -> Result<()> {
|
||||
self.stop()?;
|
||||
if crossterm::terminal::is_raw_mode_enabled()? {
|
||||
self.flush()?;
|
||||
if self.paste {
|
||||
crossterm::execute!(stdout(), DisableBracketedPaste)?;
|
||||
}
|
||||
if self.mouse {
|
||||
crossterm::execute!(stdout(), DisableMouseCapture)?;
|
||||
}
|
||||
crossterm::execute!(stdout(), LeaveAlternateScreen, cursor::Show)?;
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.cancellation_token.cancel();
|
||||
}
|
||||
|
||||
pub fn suspend(&mut self) -> Result<()> {
|
||||
self.exit()?;
|
||||
#[cfg(not(windows))]
|
||||
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn next_event(&mut self) -> Option<Event> {
|
||||
self.event_rx.recv().await
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Tui {
|
||||
type Target = ratatui::Terminal<Backend<Stdout>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Tui {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Tui {
|
||||
fn drop(&mut self) {
|
||||
self.exit().unwrap();
|
||||
}
|
||||
}
|
24
src/types/block.rs
Normal file
24
src/types/block.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogInfo {
|
||||
pub logs: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HeaderInfo {
|
||||
pub parent_hash: String,
|
||||
pub number: String,
|
||||
pub state_root: String,
|
||||
pub extrinsics_root: String,
|
||||
pub digest: LogInfo
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlockInfo {
|
||||
pub header: HeaderInfo,
|
||||
pub extrinsics: Vec<String>,
|
||||
}
|
8
src/types/era.rs
Normal file
8
src/types/era.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use codec::Decode;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)]
|
||||
pub struct EraInfo {
|
||||
pub index: u32,
|
||||
pub start: Option<u64>,
|
||||
}
|
14
src/types/macros.rs
Normal file
14
src/types/macros.rs
Normal file
@ -0,0 +1,14 @@
|
||||
#[macro_export]
|
||||
macro_rules! rpc_params {
|
||||
($($param:expr),*) => {{
|
||||
use crate::types::params::RpcParams;
|
||||
|
||||
let mut params = RpcParams::new();
|
||||
$(
|
||||
if let Err(err) = params.insert($param) {
|
||||
panic!("parameter `{}` cannot be serialized: {:?}", stringify!($param), err);
|
||||
}
|
||||
)*
|
||||
params
|
||||
}}
|
||||
}
|
5
src/types/mod.rs
Normal file
5
src/types/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod block;
|
||||
pub mod params;
|
||||
pub mod storage;
|
||||
pub mod macros;
|
||||
pub mod era;
|
75
src/types/params.rs
Normal file
75
src/types/params.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use color_eyre::Result;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RpcParams(ParamsBuilder);
|
||||
|
||||
impl RpcParams {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn insert<P: Serialize>(&mut self, value: P) -> Result<()> {
|
||||
self.0.insert(value)
|
||||
}
|
||||
|
||||
pub fn build(self) -> String {
|
||||
self.0.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RpcParams {
|
||||
fn default() -> Self {
|
||||
Self(ParamsBuilder::positional())
|
||||
}
|
||||
}
|
||||
|
||||
const PARAM_BYTES_CAPACITY: usize = 128;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParamsBuilder {
|
||||
bytes: Vec<u8>,
|
||||
start: char,
|
||||
end: char,
|
||||
}
|
||||
|
||||
impl ParamsBuilder {
|
||||
fn new(start: char, end: char) -> Self {
|
||||
Self { bytes: Vec::new(), start, end }
|
||||
}
|
||||
|
||||
fn positional() -> Self {
|
||||
Self::new('[', ']')
|
||||
}
|
||||
|
||||
fn maybe_initialize(&mut self) {
|
||||
if self.bytes.is_empty() {
|
||||
self.bytes.reserve(PARAM_BYTES_CAPACITY);
|
||||
self.bytes.push(self.start as u8);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> String {
|
||||
if self.bytes.is_empty() {
|
||||
return format!("{}{}", self.start, self.end);
|
||||
}
|
||||
|
||||
let index = self.bytes.len() - 1;
|
||||
if self.bytes[index] == b',' {
|
||||
self.bytes[index] = self.end as u8;
|
||||
} else {
|
||||
self.bytes.push(self.end as u8);
|
||||
}
|
||||
|
||||
unsafe { String::from_utf8_unchecked(self.bytes) }
|
||||
}
|
||||
|
||||
pub fn insert<P: Serialize>(&mut self, value: P) -> Result<()> {
|
||||
self.maybe_initialize();
|
||||
|
||||
serde_json::to_writer(&mut self.bytes, &value)?;
|
||||
self.bytes.push(b',');
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
32
src/types/storage.rs
Normal file
32
src/types/storage.rs
Normal file
@ -0,0 +1,32 @@
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GhostStorage<'a> {
|
||||
module: &'a str,
|
||||
method: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> GhostStorage<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
module: "",
|
||||
method: "",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_module(mut self, module: &'a str) -> Self {
|
||||
self.module = module;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_method(mut self, method: &'a str) -> Self {
|
||||
self.method = method;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build_storage_key(&self) -> String {
|
||||
let module_hex = hex::encode(sp_core::twox_128(self.module.as_bytes()));
|
||||
let method_hex = hex::encode(sp_core::twox_128(self.method.as_bytes()));
|
||||
|
||||
let storage_key = format!("0x{}{}", module_hex, method_hex);
|
||||
storage_key
|
||||
}
|
||||
}
|
305
src/widgets/big_text.rs
Normal file
305
src/widgets/big_text.rs
Normal file
@ -0,0 +1,305 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::cmp::min;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use font8x8::UnicodeFonts;
|
||||
use ratatui::{prelude::*, text::StyledGrapheme, widgets::Widget};
|
||||
|
||||
#[derive(Debug, Builder, Clone, PartialEq, Eq, Hash)]
|
||||
#[builder(build_fn(skip))]
|
||||
pub struct BigText<'a> {
|
||||
#[builder(default, setter(into))]
|
||||
pub lines: Vec<Line<'a>>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub style: Style,
|
||||
|
||||
#[builder(default)]
|
||||
pub pixel_size: PixelSize,
|
||||
|
||||
#[builder(default)]
|
||||
pub alignment: Alignment,
|
||||
}
|
||||
|
||||
impl BigText<'static> {
|
||||
pub fn builder() -> BigTextBuilder<'static> {
|
||||
BigTextBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||
pub enum PixelSize {
|
||||
#[default]
|
||||
Full,
|
||||
HalfHeight,
|
||||
HalfWidth,
|
||||
Quadrant,
|
||||
ThirdHeight,
|
||||
Sextant,
|
||||
}
|
||||
|
||||
impl PixelSize {
|
||||
pub(crate) fn pixels_per_cell(self) -> (u16, u16) {
|
||||
match self {
|
||||
PixelSize::Full => (1, 1),
|
||||
PixelSize::HalfHeight => (1, 2),
|
||||
PixelSize::HalfWidth => (2, 1),
|
||||
PixelSize::Quadrant => (2, 2),
|
||||
PixelSize::ThirdHeight => (1, 3),
|
||||
PixelSize::Sextant => (2, 3),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn symbol_for_position(self, glyph: &[u8; 8], row: usize, col: i32) -> char {
|
||||
match self {
|
||||
PixelSize::Full => match glyph[row] & (1 << col) {
|
||||
0 => ' ',
|
||||
_ => '█',
|
||||
},
|
||||
PixelSize::HalfHeight => {
|
||||
let top = glyph[row] & (1 << col);
|
||||
let bottom = glyph[row + 1] & (1 << col);
|
||||
get_symbol_half_height(top, bottom)
|
||||
}
|
||||
PixelSize::HalfWidth => {
|
||||
let left = glyph[row] & (1 << col);
|
||||
let right = glyph[row] & (1 << (col + 1));
|
||||
get_symbol_half_width(left, right)
|
||||
}
|
||||
PixelSize::Quadrant => {
|
||||
let top_left = glyph[row] & (1 << col);
|
||||
let top_right = glyph[row] & (1 << (col + 1));
|
||||
let bottom_left = glyph[row + 1] & (1 << col);
|
||||
let bottom_right = glyph[row + 1] & (1 << (col + 1));
|
||||
get_symbol_quadrant_size(top_left, top_right, bottom_left, bottom_right)
|
||||
}
|
||||
PixelSize::ThirdHeight => {
|
||||
let top = glyph[row] & (1 << col);
|
||||
let is_middle_available = (row + 1) < glyph.len();
|
||||
let middle = if is_middle_available {
|
||||
glyph[row + 1] & (1 << col)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let is_bottom_available = (row + 2) < glyph.len();
|
||||
let bottom = if is_bottom_available {
|
||||
glyph[row + 2] & (1 << col)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
get_symbol_third_height(top, middle, bottom)
|
||||
}
|
||||
PixelSize::Sextant => {
|
||||
let top_left = glyph[row] & (1 << col);
|
||||
let top_right = glyph[row] & (1 << (col + 1));
|
||||
let is_middle_available = (row + 1) < glyph.len();
|
||||
let (middle_left, middle_right) = if is_middle_available {
|
||||
(
|
||||
glyph[row + 1] & (1 << col),
|
||||
glyph[row + 1] & (1 << (col + 1)),
|
||||
)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
let is_bottom_available = (row + 2) < glyph.len();
|
||||
let (bottom_left, bottom_right) = if is_bottom_available {
|
||||
(
|
||||
glyph[row + 2] & (1 << col),
|
||||
glyph[row + 2] & (1 << (col + 1)),
|
||||
)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
get_symbol_sextant_size(
|
||||
top_left,
|
||||
top_right,
|
||||
middle_left,
|
||||
middle_right,
|
||||
bottom_left,
|
||||
bottom_right,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_symbol_half_height(top: u8, bottom: u8) -> char {
|
||||
match top {
|
||||
0 => match bottom {
|
||||
0 => ' ',
|
||||
_ => '▄',
|
||||
},
|
||||
_ => match bottom {
|
||||
0 => '▀',
|
||||
_ => '█',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_symbol_half_width(left: u8, right: u8) -> char {
|
||||
match left {
|
||||
0 => match right {
|
||||
0 => ' ',
|
||||
_ => '▐',
|
||||
},
|
||||
_ => match right {
|
||||
0 => '▌',
|
||||
_ => '█',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_symbol_quadrant_size(
|
||||
top_left: u8,
|
||||
top_right: u8,
|
||||
bottom_left: u8,
|
||||
bottom_right: u8,
|
||||
) -> char {
|
||||
let top_left = if top_left > 0 { 1 } else { 0 };
|
||||
let top_right = if top_right > 0 { 1 } else { 0 };
|
||||
let bottom_left = if bottom_left > 0 { 1 } else { 0 };
|
||||
let bottom_right = if bottom_right > 0 { 1 } else { 0 };
|
||||
|
||||
const QUADRANT_SYMBOLS: [char; 16] = [
|
||||
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
|
||||
];
|
||||
let character_index = top_left + (top_right << 1) + (bottom_left << 2) + (bottom_right << 3);
|
||||
|
||||
QUADRANT_SYMBOLS[character_index]
|
||||
}
|
||||
|
||||
fn get_symbol_third_height(top: u8, middle: u8, bottom: u8) -> char {
|
||||
get_symbol_sextant_size(top, top, middle, middle, bottom, bottom)
|
||||
}
|
||||
|
||||
fn get_symbol_sextant_size(
|
||||
top_left: u8,
|
||||
top_right: u8,
|
||||
middle_left: u8,
|
||||
middle_right: u8,
|
||||
bottom_left: u8,
|
||||
bottom_right: u8,
|
||||
) -> char {
|
||||
let top_left = if top_left > 0 { 1 } else { 0 };
|
||||
let top_right = if top_right > 0 { 1 } else { 0 };
|
||||
let middle_left = if middle_left > 0 { 1 } else { 0 };
|
||||
let middle_right = if middle_right > 0 { 1 } else { 0 };
|
||||
let bottom_left = if bottom_left > 0 { 1 } else { 0 };
|
||||
let bottom_right = if bottom_right > 0 { 1 } else { 0 };
|
||||
|
||||
const SEXANT_SYMBOLS: [char; 64] = [
|
||||
' ', '🬀', '🬁', '🬂', '🬃', '🬄', '🬅', '🬆', '🬇', '🬈', '🬉', '🬊', '🬋', '🬌', '🬍', '🬎', '🬏', '🬐',
|
||||
'🬑', '🬒', '🬓', '▌', '🬔', '🬕', '🬖', '🬗', '🬘', '🬙', '🬚', '🬛', '🬜', '🬝', '🬞', '🬟', '🬠', '🬡',
|
||||
'🬢', '🬣', '🬤', '🬥', '🬦', '🬧', '▐', '🬨', '🬩', '🬪', '🬫', '🬬', '🬭', '🬮', '🬯', '🬰', '🬱', '🬲',
|
||||
'🬳', '🬴', '🬵', '🬶', '🬷', '🬸', '🬹', '🬺', '🬻', '█',
|
||||
];
|
||||
let character_index = top_left
|
||||
+ (top_right << 1)
|
||||
+ (middle_left << 2)
|
||||
+ (middle_right << 3)
|
||||
+ (bottom_left << 4)
|
||||
+ (bottom_right << 5);
|
||||
|
||||
SEXANT_SYMBOLS[character_index]
|
||||
}
|
||||
|
||||
impl BigTextBuilder<'_> {
|
||||
pub fn left_aligned(&mut self) -> &mut Self {
|
||||
self.alignment(Alignment::Left)
|
||||
}
|
||||
pub fn centered(&mut self) -> &mut Self {
|
||||
self.alignment(Alignment::Center)
|
||||
}
|
||||
pub fn right_aligned(&mut self) -> &mut Self {
|
||||
self.alignment(Alignment::Right)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BigTextBuilder<'a> {
|
||||
pub fn build(&self) -> BigText<'a> {
|
||||
BigText {
|
||||
lines: match &self.lines {
|
||||
Some(lines) => lines.clone(),
|
||||
None => Vec::new(),
|
||||
},
|
||||
style: match &self.style {
|
||||
Some(style) => *style,
|
||||
None => Style::default(),
|
||||
},
|
||||
pixel_size: self.pixel_size.unwrap_or_default(),
|
||||
alignment: self.alignment.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for BigText<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = layout(area, &self.pixel_size, self.alignment, &self.lines);
|
||||
for (line, line_layout) in self.lines.iter().zip(layout) {
|
||||
for (g, cell) in line.styled_graphemes(self.style).zip(line_layout) {
|
||||
render_symbol(g, cell, buf, &self.pixel_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout<'a>(
|
||||
area: Rect,
|
||||
pixel_size: &PixelSize,
|
||||
alignment: Alignment,
|
||||
lines: &'a [Line<'a>],
|
||||
) -> impl IntoIterator<Item = impl IntoIterator<Item = Rect>> + 'a {
|
||||
let (step_x, step_y) = pixel_size.pixels_per_cell();
|
||||
let width = 8u16.div_ceil(step_x);
|
||||
let height = 8u16.div_ceil(step_y);
|
||||
|
||||
(area.top()..area.bottom())
|
||||
.step_by(height as usize)
|
||||
.zip(lines.iter())
|
||||
.map(move |(y, line)| {
|
||||
let offset = get_alignment_offset(area.width, width, alignment, line);
|
||||
(area.left() + offset..area.right())
|
||||
.step_by(width as usize)
|
||||
.map(move |x| {
|
||||
let width = min(area.right() - x, width);
|
||||
let height = min(area.bottom() - y, height);
|
||||
Rect::new(x, y, width, height)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn get_alignment_offset<'a>(
|
||||
area_width: u16,
|
||||
letter_width: u16,
|
||||
alignment: Alignment,
|
||||
line: &'a Line<'a>,
|
||||
) -> u16 {
|
||||
let big_line_width = line.width() as u16 * letter_width;
|
||||
match alignment {
|
||||
Alignment::Left => 0,
|
||||
Alignment::Center => (area_width / 2).saturating_sub(big_line_width / 2),
|
||||
Alignment::Right => area_width.saturating_sub(big_line_width),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_symbol(grapheme: StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_size: &PixelSize) {
|
||||
buf.set_style(area, grapheme.style);
|
||||
let c = grapheme.symbol.chars().next().unwrap();
|
||||
if let Some(glyph) = font8x8::BASIC_FONTS.get(c) {
|
||||
render_glyph(glyph, area, buf, pixel_size);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: &PixelSize) {
|
||||
let (step_x, step_y) = pixel_size.pixels_per_cell();
|
||||
|
||||
let glyph_vertical_index = (0..glyph.len()).step_by(step_y as usize);
|
||||
let glyph_horizontal_bit_index = (0..8).step_by(step_x as usize);
|
||||
|
||||
for (y, row) in glyph_vertical_index.zip(area.rows()) {
|
||||
for (x, col) in glyph_horizontal_bit_index.clone().zip(row.columns()) {
|
||||
buf[col].set_char(pixel_size.symbol_for_position(&glyph, y, x));
|
||||
}
|
||||
}
|
||||
}
|
21
src/widgets/dot_spinner.rs
Normal file
21
src/widgets/dot_spinner.rs
Normal file
@ -0,0 +1,21 @@
|
||||
const SPINNER: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
|
||||
pub struct DotSpinner {
|
||||
elements: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for DotSpinner {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
elements: SPINNER.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for DotSpinner {
|
||||
fn to_string(&self) -> String {
|
||||
self.elements[((chrono::Utc::now().timestamp_millis() % super::CYCLE)
|
||||
/ (super::CYCLE / self.elements.len() as i64)) as usize]
|
||||
.to_owned()
|
||||
}
|
||||
}
|
12
src/widgets/mod.rs
Normal file
12
src/widgets/mod.rs
Normal file
@ -0,0 +1,12 @@
|
||||
mod dot_spinner;
|
||||
mod ogham;
|
||||
mod vertical_block;
|
||||
mod big_text;
|
||||
|
||||
pub use dot_spinner::DotSpinner;
|
||||
pub use vertical_block::VerticalBlocks;
|
||||
pub use ogham::OghamCenter;
|
||||
pub use big_text::BigText;
|
||||
pub use big_text::PixelSize;
|
||||
|
||||
const CYCLE: i64 = 1500;
|
21
src/widgets/ogham.rs
Normal file
21
src/widgets/ogham.rs
Normal file
@ -0,0 +1,21 @@
|
||||
const PROGRESS_CENTER: [&str; 6] = [" ", "ᚐ", "ᚑ", "ᚒ", "ᚓ", "ᚔ"];
|
||||
|
||||
pub struct OghamCenter {
|
||||
elements: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for OghamCenter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
elements: PROGRESS_CENTER.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for OghamCenter {
|
||||
fn to_string(&self) -> String {
|
||||
self.elements[((chrono::Utc::now().timestamp_millis() % super::CYCLE)
|
||||
/ (super::CYCLE / self.elements.len() as i64)) as usize]
|
||||
.to_owned()
|
||||
}
|
||||
}
|
21
src/widgets/vertical_block.rs
Normal file
21
src/widgets/vertical_block.rs
Normal file
@ -0,0 +1,21 @@
|
||||
const PROGRESS: [&str; 8] = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
||||
|
||||
pub struct VerticalBlocks {
|
||||
elements: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for VerticalBlocks {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
elements: PROGRESS.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for VerticalBlocks {
|
||||
fn to_string(&self) -> String {
|
||||
self.elements[((chrono::Utc::now().timestamp_millis() % super::CYCLE)
|
||||
/ (super::CYCLE / self.elements.len() as i64)) as usize]
|
||||
.to_owned()
|
||||
}
|
||||
}
|
21
test.json
Normal file
21
test.json
Normal file
@ -0,0 +1,21 @@
|
||||
"result":{
|
||||
"block":{
|
||||
"header":{
|
||||
"parentHash":"0x2f27b44ed3c9acef8876d12751355547046477b965c319235baffa67dffa2cc1",
|
||||
"number":"0x1c0bb",
|
||||
"stateRoot":"0x1652a3aa3be9a9f600bf8017a49210c5fce85d8eca063fa8ebbf8b3a844e72a1",
|
||||
"extrinsicsRoot":"0xbdc6d615ae89a071d5385e0015cd3597c5b21a9b294e2346be9b4fd793a21303",
|
||||
"digest":{
|
||||
"logs":[
|
||||
"0x0642414245b5010302000000f4b9311100000000eeb68f56e5a888622cd2e4bb10c9c5c2c20d973bc372ed4f10f3b872507059410dba82bdb1dfc5ab439c2a708e1d8726fe151aea2a3cf2e1aa1f5a947e11ce0213f5f9eb5dd3dc3ff34af3e961d5c6d101e1353af79b9c6284e3a2ec26795207",
|
||||
"0x054241424501015ae1a245af3f3b2caf37930037fab5a8c38c0ca803760dfb571f9df726659e72cf873f5994b7ac97522bb7e635193ae0d4c5a229eb9e3b9aa9ab8472cc5ee587"
|
||||
]
|
||||
}
|
||||
},
|
||||
"extrinsics":[
|
||||
"0x280403000bc14676fd9201"
|
||||
]
|
||||
},
|
||||
"justifications":null
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user