Compare commits
2 Commits
7561c525e3
...
6a43dab6dd
Author | SHA1 | Date | |
---|---|---|---|
6a43dab6dd | |||
8eb3871e34 |
@ -2,7 +2,7 @@
|
|||||||
name = "ghost-eye"
|
name = "ghost-eye"
|
||||||
authors = ["str3tch <stretch@ghostchain.io>"]
|
authors = ["str3tch <stretch@ghostchain.io>"]
|
||||||
description = "Application for interacting with Casper/Ghost nodes that are exposing RPC only to the localhost"
|
description = "Application for interacting with Casper/Ghost nodes that are exposing RPC only to the localhost"
|
||||||
version = "0.3.58"
|
version = "0.3.60"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://git.ghostchain.io/ghostchain"
|
homepage = "https://git.ghostchain.io/ghostchain"
|
||||||
repository = "https://git.ghostchain.io/ghostchain/ghost-eye"
|
repository = "https://git.ghostchain.io/ghostchain/ghost-eye"
|
||||||
|
@ -9,6 +9,15 @@
|
|||||||
"hover_title_style": "",
|
"hover_title_style": "",
|
||||||
"highlight_style": "yellow italic",
|
"highlight_style": "yellow italic",
|
||||||
},
|
},
|
||||||
|
"Help": {
|
||||||
|
"normal_style": "",
|
||||||
|
"hover_style": "bold yellow italic on blue",
|
||||||
|
"normal_border_style": "blue",
|
||||||
|
"hover_border_style": "blue",
|
||||||
|
"normal_title_style": "blue",
|
||||||
|
"hover_title_style": "",
|
||||||
|
"highlight_style": "yellow italic",
|
||||||
|
},
|
||||||
"Explorer": {
|
"Explorer": {
|
||||||
"normal_style": "",
|
"normal_style": "",
|
||||||
"hover_style": "bold yellow italic on blue",
|
"hover_style": "bold yellow italic on blue",
|
||||||
|
58
src/app.rs
58
src/app.rs
@ -1,7 +1,6 @@
|
|||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use ratatui::prelude::Rect;
|
use ratatui::prelude::Rect;
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use tokio::sync::mpsc::{UnboundedSender, UnboundedReceiver};
|
use tokio::sync::mpsc::{UnboundedSender, UnboundedReceiver};
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@ -12,26 +11,12 @@ use crate::{
|
|||||||
tui::{Event, Tui},
|
tui::{Event, Tui},
|
||||||
components::{
|
components::{
|
||||||
menu::Menu, version::Version, explorer::Explorer, wallet::Wallet,
|
menu::Menu, version::Version, explorer::Explorer, wallet::Wallet,
|
||||||
validator::Validator, empty::Empty,
|
validator::Validator, empty::Empty, help::Help, health::Health,
|
||||||
health::Health, fps::FpsCounter,
|
fps::FpsCounter, Component,
|
||||||
Component,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
pub use crate::modes::Mode;
|
||||||
pub enum Mode {
|
|
||||||
Menu,
|
|
||||||
Explorer,
|
|
||||||
Wallet,
|
|
||||||
Validator,
|
|
||||||
Empty,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Mode {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Explorer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
network_tx: Sender<Action>,
|
network_tx: Sender<Action>,
|
||||||
@ -69,6 +54,7 @@ impl App {
|
|||||||
Box::new(FpsCounter::default()),
|
Box::new(FpsCounter::default()),
|
||||||
Box::new(Health::default()),
|
Box::new(Health::default()),
|
||||||
Box::new(Version::default()),
|
Box::new(Version::default()),
|
||||||
|
Box::new(Help::default()),
|
||||||
Box::new(Explorer::default()),
|
Box::new(Explorer::default()),
|
||||||
Box::new(Wallet::default()),
|
Box::new(Wallet::default()),
|
||||||
Box::new(Validator::default()),
|
Box::new(Validator::default()),
|
||||||
@ -230,25 +216,8 @@ impl App {
|
|||||||
|
|
||||||
fn render(&mut self, tui: &mut Tui) -> Result<()> {
|
fn render(&mut self, tui: &mut Tui) -> Result<()> {
|
||||||
tui.draw(|frame| {
|
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 {
|
match self.mode {
|
||||||
Mode::Explorer => {
|
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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Mode::Wallet => {
|
|
||||||
if let Some(component) = self.components.get_mut(5) {
|
if let Some(component) = self.components.get_mut(5) {
|
||||||
if let Err(err) = component.draw(frame, frame.area()) {
|
if let Err(err) = component.draw(frame, frame.area()) {
|
||||||
let _ = self
|
let _ = self
|
||||||
@ -257,7 +226,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Mode::Validator => {
|
Mode::Wallet => {
|
||||||
if let Some(component) = self.components.get_mut(6) {
|
if let Some(component) = self.components.get_mut(6) {
|
||||||
if let Err(err) = component.draw(frame, frame.area()) {
|
if let Err(err) = component.draw(frame, frame.area()) {
|
||||||
let _ = self
|
let _ = self
|
||||||
@ -266,6 +235,15 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Mode::Validator => {
|
||||||
|
if let Some(component) = self.components.get_mut(7) {
|
||||||
|
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 Some(component) = self.components.last_mut() {
|
||||||
if let Err(err) = component.draw(frame, frame.area()) {
|
if let Err(err) = component.draw(frame, frame.area()) {
|
||||||
@ -276,6 +254,14 @@ impl App {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for component in self.components.iter_mut().take(5) {
|
||||||
|
if let Err(err) = (*component).draw(frame, frame.area()) {
|
||||||
|
let _ = self
|
||||||
|
.action_tx
|
||||||
|
.send(Action::Error(format!("failed to draw: {:?}", err)));
|
||||||
|
}
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
6
src/components/generic/activatable.rs
Normal file
6
src/components/generic/activatable.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
pub trait Activatable {
|
||||||
|
fn is_active(&self) -> bool;
|
||||||
|
fn is_inactive(&self) -> bool;
|
||||||
|
fn set_active(&mut self);
|
||||||
|
fn set_inactive(&mut self);
|
||||||
|
}
|
11
src/components/generic/helpable.rs
Normal file
11
src/components/generic/helpable.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use color_eyre::Result;
|
||||||
|
use crate::action::Action;
|
||||||
|
|
||||||
|
use super::Activatable;
|
||||||
|
|
||||||
|
pub trait Helpable: Activatable {
|
||||||
|
fn open_help_popup(&mut self) -> Result<Option<Action>> {
|
||||||
|
self.set_inactive();
|
||||||
|
Ok(Some(Action::Help))
|
||||||
|
}
|
||||||
|
}
|
7
src/components/generic/mod.rs
Normal file
7
src/components/generic/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
mod activatable;
|
||||||
|
mod helpable;
|
||||||
|
mod scrollable;
|
||||||
|
|
||||||
|
pub use activatable::Activatable;
|
||||||
|
pub use helpable::Helpable;
|
||||||
|
pub use scrollable::Scrollable;
|
58
src/components/generic/scrollable.rs
Normal file
58
src/components/generic/scrollable.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use std::{
|
||||||
|
cmp::{PartialEq, PartialOrd}, ops::{Add, Sub}
|
||||||
|
};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use crate::action::Action;
|
||||||
|
|
||||||
|
pub trait Scrollable {
|
||||||
|
type IndexType: Add<Output = Self::IndexType>
|
||||||
|
+ Sub<Output = Self::IndexType>
|
||||||
|
+ PartialOrd
|
||||||
|
+ PartialEq
|
||||||
|
+ From<u8>
|
||||||
|
+ Copy;
|
||||||
|
|
||||||
|
fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>>;
|
||||||
|
fn apply_prev_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>>;
|
||||||
|
|
||||||
|
fn selected_index(&self) -> Option<Self::IndexType>;
|
||||||
|
fn items_length(&self) -> Self::IndexType;
|
||||||
|
|
||||||
|
fn next_row(&mut self) -> Result<Option<Action>> {
|
||||||
|
let length = self.items_length();
|
||||||
|
if length == 0.into() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_index = match self.selected_index() {
|
||||||
|
Some(index) => {
|
||||||
|
if index < length - 1.into() {
|
||||||
|
index + 1.into()
|
||||||
|
} else {
|
||||||
|
index
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => 0.into(),
|
||||||
|
};
|
||||||
|
self.apply_next_row(new_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev_row(&mut self) -> Result<Option<Action>> {
|
||||||
|
if self.items_length() == 0.into() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_index = match self.selected_index() {
|
||||||
|
Some(index) => {
|
||||||
|
if index == 0.into() {
|
||||||
|
0.into()
|
||||||
|
} else {
|
||||||
|
index - 1.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0.into()
|
||||||
|
};
|
||||||
|
self.apply_prev_row(new_index)
|
||||||
|
}
|
||||||
|
}
|
202
src/components/help.rs
Normal file
202
src/components/help.rs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
use color_eyre::Result;
|
||||||
|
use crossterm::event::{KeyEvent, KeyCode};
|
||||||
|
use ratatui::{
|
||||||
|
layout::{Alignment, Constraint, Flex, Layout, Margin, Rect},
|
||||||
|
style::{palette::tailwind, Style, Modifier},
|
||||||
|
text::Text,
|
||||||
|
widgets::{
|
||||||
|
Block, BorderType, Cell, Clear, HighlightSpacing, Paragraph, Row,
|
||||||
|
Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState,
|
||||||
|
},
|
||||||
|
Frame
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::palette::StylePalette;
|
||||||
|
use super::generic::{Activatable, Helpable, Scrollable};
|
||||||
|
use super::Component;
|
||||||
|
|
||||||
|
use crate::{action::Action, app::Mode, config::Config};
|
||||||
|
|
||||||
|
const ITEM_HEIGHT: usize = 3;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Help {
|
||||||
|
is_active: bool,
|
||||||
|
current_mode: Mode,
|
||||||
|
palette: StylePalette,
|
||||||
|
scroll_state: ScrollbarState,
|
||||||
|
table_state: TableState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Help {
|
||||||
|
fn move_index(&mut self, new_index: usize) -> Result<Option<Action>> {
|
||||||
|
self.table_state.select(Some(new_index));
|
||||||
|
self.scroll_state = self.scroll_state.position(new_index * ITEM_HEIGHT);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Help {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
is_active: false,
|
||||||
|
current_mode: Mode::Menu,
|
||||||
|
palette: Default::default(),
|
||||||
|
scroll_state: ScrollbarState::new((Mode::Menu.get_help_data().len() - 1) * ITEM_HEIGHT),
|
||||||
|
table_state: TableState::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Activatable for Help {
|
||||||
|
fn is_active(&self) -> bool { self.is_active }
|
||||||
|
fn is_inactive(&self) -> bool { !self.is_active }
|
||||||
|
fn set_inactive(&mut self) { self.is_active = false; }
|
||||||
|
fn set_active(&mut self) { self.is_active = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Helpable for Help { }
|
||||||
|
|
||||||
|
impl Scrollable for Help {
|
||||||
|
type IndexType = usize;
|
||||||
|
|
||||||
|
fn selected_index(&self) -> Option<Self::IndexType> {
|
||||||
|
self.table_state.selected()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn items_length(&self) -> Self::IndexType {
|
||||||
|
self.current_mode.get_help_data().len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||||
|
self.move_index(new_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_prev_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||||
|
self.move_index(new_index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Help {
|
||||||
|
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||||
|
if let Some(style) = config.styles.get(&crate::app::Mode::Help) {
|
||||||
|
self.palette.with_normal_style(style.get("normal_style").copied());
|
||||||
|
self.palette.with_normal_border_style(style.get("normal_border_style").copied());
|
||||||
|
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||||
|
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||||
|
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||||
|
match action {
|
||||||
|
Action::Help if self.is_inactive() => self.set_active(),
|
||||||
|
Action::SetActiveScreen(mode) => {
|
||||||
|
if self.current_mode != mode {
|
||||||
|
self.current_mode = mode;
|
||||||
|
self.table_state.select(None);
|
||||||
|
self.scroll_state = ScrollbarState::new((self.current_mode.get_help_data().len() - 1) * ITEM_HEIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up | KeyCode::Char('k') if self.is_active() => self.next_row(),
|
||||||
|
KeyCode::Down | KeyCode::Char('j') if self.is_active() => self.prev_row(),
|
||||||
|
KeyCode::Esc if self.is_active() => {
|
||||||
|
self.set_inactive();
|
||||||
|
Ok(Some(Action::SetActiveScreen(self.current_mode)))
|
||||||
|
},
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||||
|
if self.is_active() {
|
||||||
|
let highlight_symbol = Text::from(vec![
|
||||||
|
"".into(),
|
||||||
|
" █ ".into(),
|
||||||
|
"".into(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let table = Table::new(
|
||||||
|
self.current_mode
|
||||||
|
.get_help_data()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, content)| {
|
||||||
|
let color = match i % 2 {
|
||||||
|
0 => tailwind::SLATE.c950,
|
||||||
|
_ => tailwind::SLATE.c900,
|
||||||
|
};
|
||||||
|
|
||||||
|
Row::from(content
|
||||||
|
.into_iter()
|
||||||
|
.map(|data| Cell::from(Text::from(*data)))
|
||||||
|
.collect::<Row>()
|
||||||
|
.style(Style::default().fg(tailwind::BLUE.c200).bg(color))
|
||||||
|
.height(ITEM_HEIGHT as u16))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
self.current_mode.get_help_constraints()
|
||||||
|
)
|
||||||
|
.header(self.current_mode
|
||||||
|
.get_help_headers()
|
||||||
|
.into_iter()
|
||||||
|
.map(|h| Cell::from(Text::from(*h)))
|
||||||
|
.collect::<Row>()
|
||||||
|
.style(Style::default()
|
||||||
|
.fg(tailwind::SLATE.c200)
|
||||||
|
.bg(tailwind::BLUE.c900)
|
||||||
|
)
|
||||||
|
.height(1)
|
||||||
|
)
|
||||||
|
.highlight_style(Style::default()
|
||||||
|
.add_modifier(Modifier::REVERSED)
|
||||||
|
.fg(tailwind::BLUE.c400)
|
||||||
|
.bg(tailwind::YELLOW.c200))
|
||||||
|
.highlight_spacing(HighlightSpacing::Always)
|
||||||
|
.highlight_symbol(highlight_symbol)
|
||||||
|
.block(Block::default()
|
||||||
|
.style(Style::default().bg(tailwind::SLATE.c950))
|
||||||
|
.title_alignment(Alignment::Right)
|
||||||
|
.title_style(tailwind::YELLOW.c200)
|
||||||
|
.title(self.current_mode.get_help_title()));
|
||||||
|
|
||||||
|
let scrollbar = Scrollbar::default()
|
||||||
|
.orientation(ScrollbarOrientation::VerticalRight)
|
||||||
|
.begin_symbol(None)
|
||||||
|
.end_symbol(None);
|
||||||
|
|
||||||
|
let footer = Paragraph::new(Text::from(self.current_mode.get_help_text()))
|
||||||
|
.style(Style::default().fg(tailwind::SLATE.c200).bg(tailwind::SLATE.c950))
|
||||||
|
.centered()
|
||||||
|
.block(Block::bordered()
|
||||||
|
.border_type(BorderType::Double)
|
||||||
|
.border_style(Style::default().fg(tailwind::BLUE.c400))
|
||||||
|
);
|
||||||
|
|
||||||
|
let v = Layout::vertical([Constraint::Length(23)]).flex(Flex::Center);
|
||||||
|
let h = Layout::horizontal([Constraint::Length(65)]).flex(Flex::Center);
|
||||||
|
|
||||||
|
let [area] = v.areas(area);
|
||||||
|
let [area] = h.areas(area);
|
||||||
|
|
||||||
|
let [main_area, footer_area] = Layout::vertical([
|
||||||
|
Constraint::Fill(1),
|
||||||
|
Constraint::Length(3),
|
||||||
|
]).areas(area);
|
||||||
|
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
frame.render_stateful_widget(table, main_area, &mut self.table_state);
|
||||||
|
frame.render_stateful_widget(scrollbar, main_area.inner(Margin::new(1, 2)), &mut self.scroll_state);
|
||||||
|
frame.render_widget(footer, footer_area);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use ratatui::{prelude::*, widgets::*};
|
use ratatui::{prelude::*, widgets::*};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
|
||||||
use crossterm::event::{KeyEvent, KeyCode};
|
use crossterm::event::{KeyEvent, KeyCode};
|
||||||
|
|
||||||
use super::Component;
|
use super::Component;
|
||||||
use super::palette::StylePalette;
|
use super::palette::StylePalette;
|
||||||
use crate::{config::Config, action::Action, app::Mode};
|
use crate::{config::Config, action::Action, app::Mode};
|
||||||
|
|
||||||
|
use super::generic::{Activatable, Helpable, Scrollable};
|
||||||
|
|
||||||
pub struct Menu {
|
pub struct Menu {
|
||||||
command_tx: Option<UnboundedSender<Action>>,
|
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
items: Vec<String>,
|
items: Vec<String>,
|
||||||
is_active: bool,
|
is_active: bool,
|
||||||
@ -16,15 +16,41 @@ pub struct Menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Menu {
|
impl Default for Menu {
|
||||||
fn default() -> Self {
|
fn default() -> Self { Menu::new() }
|
||||||
Menu::new()
|
}
|
||||||
|
|
||||||
|
impl Activatable for Menu {
|
||||||
|
fn is_active(&self) -> bool { self.is_active }
|
||||||
|
fn is_inactive(&self) -> bool { !self.is_active }
|
||||||
|
fn set_inactive(&mut self) { self.is_active = false; }
|
||||||
|
fn set_active(&mut self) { self.is_active = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Helpable for Menu { }
|
||||||
|
|
||||||
|
impl Scrollable for Menu {
|
||||||
|
type IndexType = usize;
|
||||||
|
|
||||||
|
fn selected_index(&self) -> Option<Self::IndexType> {
|
||||||
|
self.list_state.selected()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn items_length(&self) -> Self::IndexType {
|
||||||
|
self.items.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||||
|
self.move_index(new_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_prev_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||||
|
self.move_index(new_index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Menu {
|
impl Menu {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut new_list = Self {
|
let mut new_list = Self {
|
||||||
command_tx: None,
|
|
||||||
list_state: ListState::default(),
|
list_state: ListState::default(),
|
||||||
items: vec![
|
items: vec![
|
||||||
String::from("Explorer"),
|
String::from("Explorer"),
|
||||||
@ -42,39 +68,9 @@ impl Menu {
|
|||||||
new_list
|
new_list
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_row(&mut self) -> Result<Option<Action>> {
|
fn move_index(&mut self, new_index: usize) -> Result<Option<Action>> {
|
||||||
let i = match self.list_state.selected() {
|
self.list_state.select(Some(new_index));
|
||||||
Some(i) => {
|
match new_index {
|
||||||
if i >= self.items.len() - 1 {
|
|
||||||
i
|
|
||||||
} else {
|
|
||||||
i + 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
self.list_state.select(Some(i));
|
|
||||||
match i {
|
|
||||||
0 => Ok(Some(Action::SetMode(Mode::Explorer))),
|
|
||||||
1 => Ok(Some(Action::SetMode(Mode::Wallet))),
|
|
||||||
2 => Ok(Some(Action::SetMode(Mode::Validator))),
|
|
||||||
_ => Ok(Some(Action::SetMode(Mode::Empty))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn previous_row(&mut self) -> Result<Option<Action>> {
|
|
||||||
let i = match self.list_state.selected() {
|
|
||||||
Some(i) => {
|
|
||||||
if i == 0 {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
i - 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => 0
|
|
||||||
};
|
|
||||||
self.list_state.select(Some(i));
|
|
||||||
match i {
|
|
||||||
0 => Ok(Some(Action::SetMode(Mode::Explorer))),
|
0 => Ok(Some(Action::SetMode(Mode::Explorer))),
|
||||||
1 => Ok(Some(Action::SetMode(Mode::Wallet))),
|
1 => Ok(Some(Action::SetMode(Mode::Wallet))),
|
||||||
2 => Ok(Some(Action::SetMode(Mode::Validator))),
|
2 => Ok(Some(Action::SetMode(Mode::Validator))),
|
||||||
@ -86,17 +82,12 @@ impl Menu {
|
|||||||
impl Component for Menu {
|
impl Component for Menu {
|
||||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||||
match action {
|
match action {
|
||||||
Action::SetActiveScreen(Mode::Menu) => self.is_active = true,
|
Action::SetActiveScreen(Mode::Menu) => self.set_active(),
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
|
||||||
self.command_tx = Some(tx);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||||
if let Some(style) = config.styles.get(&crate::app::Mode::Menu) {
|
if let Some(style) = config.styles.get(&crate::app::Mode::Menu) {
|
||||||
self.palette.with_normal_style(style.get("normal_style").copied());
|
self.palette.with_normal_style(style.get("normal_style").copied());
|
||||||
@ -110,10 +101,10 @@ impl Component for Menu {
|
|||||||
|
|
||||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up | KeyCode::Char('k') if self.is_active => self.previous_row(),
|
KeyCode::Up | KeyCode::Char('k') if self.is_active() => self.prev_row(),
|
||||||
KeyCode::Down | KeyCode::Char('j') if self.is_active => self.next_row(),
|
KeyCode::Down | KeyCode::Char('j') if self.is_active() => self.next_row(),
|
||||||
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active => {
|
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active() => {
|
||||||
self.is_active = false;
|
self.set_inactive();
|
||||||
match self.list_state.selected() {
|
match self.list_state.selected() {
|
||||||
Some(0) => Ok(Some(Action::SetActiveScreen(Mode::Explorer))),
|
Some(0) => Ok(Some(Action::SetActiveScreen(Mode::Explorer))),
|
||||||
Some(1) => Ok(Some(Action::SetActiveScreen(Mode::Wallet))),
|
Some(1) => Ok(Some(Action::SetActiveScreen(Mode::Wallet))),
|
||||||
@ -121,6 +112,7 @@ impl Component for Menu {
|
|||||||
_ => Ok(Some(Action::SetActiveScreen(Mode::Empty))),
|
_ => Ok(Some(Action::SetActiveScreen(Mode::Empty))),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
KeyCode::Char('?') if self.is_active() => self.open_help_popup(),
|
||||||
_ => Ok(None),
|
_ => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,7 +120,7 @@ impl Component for Menu {
|
|||||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||||
let [menu, _] = super::menu_layout(area);
|
let [menu, _] = super::menu_layout(area);
|
||||||
|
|
||||||
let (color, border_type) = self.palette.create_border_style(self.is_active);
|
let (color, border_type) = self.palette.create_border_style(self.is_active());
|
||||||
let block = Block::bordered()
|
let block = Block::bordered()
|
||||||
.border_style(color)
|
.border_style(color)
|
||||||
.border_type(border_type);
|
.border_type(border_type);
|
||||||
|
@ -17,6 +17,8 @@ pub mod explorer;
|
|||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
pub mod validator;
|
pub mod validator;
|
||||||
pub mod empty;
|
pub mod empty;
|
||||||
|
pub mod help;
|
||||||
|
pub mod generic;
|
||||||
|
|
||||||
pub trait Component {
|
pub trait Component {
|
||||||
fn register_network_handler(&mut self, tx: Sender<Action>) -> Result<()> {
|
fn register_network_handler(&mut self, tx: Sender<Action>) -> Result<()> {
|
||||||
|
@ -5,6 +5,7 @@ use subxt::{
|
|||||||
backend::rpc::RpcClient,
|
backend::rpc::RpcClient,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod modes;
|
||||||
mod action;
|
mod action;
|
||||||
mod app;
|
mod app;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
69
src/modes.rs
Normal file
69
src/modes.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use ratatui::layout::Constraint;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum Mode {
|
||||||
|
Menu,
|
||||||
|
Help,
|
||||||
|
Explorer,
|
||||||
|
Wallet,
|
||||||
|
Validator,
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Mode {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Explorer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mode {
|
||||||
|
const MENU_DATA_TITLE: &str = "Help for navigation menu ";
|
||||||
|
const MENU_DATA_HEADERS: &[&str] = &["Hot Key", "Target", "Description"];
|
||||||
|
const MENU_DATA_CONSTRAINTS: &[Constraint] = &[Constraint::Length(10), Constraint::Min(0)];
|
||||||
|
const MENU_DATA_ROWS: &[&[&str]] = &[
|
||||||
|
&["(↑) | k", "Select side menu item above"],
|
||||||
|
&["(↓) | j", "Select side menu item below"],
|
||||||
|
&["(→) | l", "Select next tab in the screen"],
|
||||||
|
&["(←) | h", "Select previous tab in the screen"],
|
||||||
|
&["Enter", "Enter the selected screen"],
|
||||||
|
&["Esc", "Navigate back to the side menu"],
|
||||||
|
];
|
||||||
|
|
||||||
|
const NON_EXISTENT_TITLE: &str = "Non existent";
|
||||||
|
const NON_EXISTENT_DATA_HEADERS: &[&str] = &["Empty header"];
|
||||||
|
const NON_EXISTENT_DATA_CONSTRAINTS: &[Constraint] = &[Constraint::Percentage(100)];
|
||||||
|
const NON_EXISTENT_DATA_ROWS: &[&[&str]] = &[&["Not implemented yet"]];
|
||||||
|
|
||||||
|
pub fn get_help_title(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Menu => &Self::MENU_DATA_TITLE,
|
||||||
|
_ => &Self::NON_EXISTENT_TITLE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_help_headers(&self) -> &'static [&'static str] {
|
||||||
|
match self {
|
||||||
|
Self::Menu => &Self::MENU_DATA_HEADERS,
|
||||||
|
_ => &Self::NON_EXISTENT_DATA_HEADERS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_help_data(&self) -> &'static [&'static [&'static str]] {
|
||||||
|
match self {
|
||||||
|
Self::Menu => &Self::MENU_DATA_ROWS,
|
||||||
|
_ => &Self::NON_EXISTENT_DATA_ROWS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_help_constraints(&self) -> &'static [Constraint] {
|
||||||
|
match self {
|
||||||
|
Self::Menu => &Self::MENU_DATA_CONSTRAINTS,
|
||||||
|
_ => &Self::NON_EXISTENT_DATA_CONSTRAINTS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_help_text(&self) -> &'static str {
|
||||||
|
"(Esc) close | (↑) move up | (↓) move down"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user