[WIP] Needs more TUI (#765)

* first explorations at attempting to integrate more user-friendly status screen/ui

* rustfmt

* adding some logo and color for visual interest

* formatting

* better integration with stdout, cleaner looking startup and shutdown

* rustfmt

* update to framework, and first collection of stats from server

* rustfmt

* commit of basic stat screen, think it's in a good enough state to share

* rustfmt

* fix to automated tests

* fix to automated tests

* grin.toml setting

* grin.toml setting

* grin.toml setting

* merge from upstream

* merge from upstream

* merging

* merging

* formatting

* adding more status screens, beginning to collect peer data

* rustfmt

* beginnings of peer info

* adding tui dir

* rustfmt

* missing table

* create title bar

* Split up UI elements into separate files, create trait for update

* Added basic mining view
This commit is contained in:
Yeastplume 2018-03-13 18:10:45 +00:00 committed by GitHub
parent 65c0a4b7b0
commit e268993f5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1852 additions and 400 deletions

View file

@ -18,6 +18,7 @@ grin_core = { path = "./core" }
grin_grin = { path = "./grin" } grin_grin = { path = "./grin" }
grin_keychain = { path = "./keychain" } grin_keychain = { path = "./keychain" }
grin_p2p = { path = "./p2p"} grin_p2p = { path = "./p2p"}
grin_pow = { path = "./pow"}
grin_util = { path = "./util"} grin_util = { path = "./util"}
grin_wallet = { path = "./wallet" } grin_wallet = { path = "./wallet" }
blake2-rfc = "~0.2.17" blake2-rfc = "~0.2.17"

View file

@ -47,7 +47,7 @@ mod adapters;
mod server; mod server;
mod seed; mod seed;
mod sync; mod sync;
mod types; pub mod types;
mod miner; mod miner;
pub use server::Server; pub use server::Server;

View file

@ -254,6 +254,11 @@ impl Miner {
if sps_total.is_finite() { if sps_total.is_finite() {
let mut mining_stats = mining_stats.write().unwrap(); let mut mining_stats = mining_stats.write().unwrap();
mining_stats.combined_gps = sps_total; mining_stats.combined_gps = sps_total;
let mut device_vec = vec![];
for i in 0..plugin_miner.loaded_plugin_count() {
device_vec.push(job_handle.get_stats(i).unwrap());
}
mining_stats.device_stats = Some(device_vec);
} }
next_stat_output = time::get_time().sec + stat_output_interval; next_stat_output = time::get_time().sec + stat_output_interval;
} }
@ -359,6 +364,9 @@ impl Miner {
if last_hashes_per_sec.is_finite() { if last_hashes_per_sec.is_finite() {
let mut mining_stats = mining_stats.write().unwrap(); let mut mining_stats = mining_stats.write().unwrap();
mining_stats.combined_gps = last_hashes_per_sec; mining_stats.combined_gps = last_hashes_per_sec;
let mut device_vec = vec![];
device_vec.push(plugin_miner.get_stats(0).unwrap());
mining_stats.device_stats = Some(device_vec);
} }
} }
next_stat_check = time::get_time().sec + stat_check_interval; next_stat_check = time::get_time().sec + stat_check_interval;

View file

@ -263,6 +263,15 @@ impl Server {
pub fn get_server_stats(&self) -> Result<ServerStats, Error> { pub fn get_server_stats(&self) -> Result<ServerStats, Error> {
let mining_stats = self.state_info.mining_stats.read().unwrap().clone(); let mining_stats = self.state_info.mining_stats.read().unwrap().clone();
let awaiting_peers = self.state_info.awaiting_peers.load(Ordering::Relaxed); let awaiting_peers = self.state_info.awaiting_peers.load(Ordering::Relaxed);
let peer_stats = self.p2p
.peers
.connected_peers()
.into_iter()
.map(|p| {
let p = p.read().unwrap();
PeerStats::from_peer(&p)
})
.collect();
Ok(ServerStats { Ok(ServerStats {
peer_count: self.peer_count(), peer_count: self.peer_count(),
head: self.head(), head: self.head(),
@ -270,6 +279,7 @@ impl Server {
is_syncing: self.currently_syncing.load(Ordering::Relaxed), is_syncing: self.currently_syncing.load(Ordering::Relaxed),
awaiting_peers: awaiting_peers, awaiting_peers: awaiting_peers,
mining_stats: mining_stats, mining_stats: mining_stats,
peer_stats: peer_stats,
}) })
} }

View file

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! Server types
use std::convert::From; use std::convert::From;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
@ -41,6 +43,7 @@ pub enum Error {
API(api::Error), API(api::Error),
/// Error originating from wallet API. /// Error originating from wallet API.
Wallet(wallet::Error), Wallet(wallet::Error),
/// Error originating from the cuckoo miner
Cuckoo(pow::cuckoo::Error), Cuckoo(pow::cuckoo::Error),
} }
@ -210,6 +213,8 @@ pub struct ServerStats {
pub awaiting_peers: bool, pub awaiting_peers: bool,
/// Handle to current mining stats /// Handle to current mining stats
pub mining_stats: MiningStats, pub mining_stats: MiningStats,
/// Peer stats
pub peer_stats: Vec<PeerStats>,
} }
/// Struct to return relevant information about the mining process /// Struct to return relevant information about the mining process
@ -228,6 +233,49 @@ pub struct MiningStats {
pub network_difficulty: u64, pub network_difficulty: u64,
/// cuckoo size used for mining /// cuckoo size used for mining
pub cuckoo_size: u16, pub cuckoo_size: u16,
/// Individual device status from Cuckoo-Miner
pub device_stats: Option<Vec<Vec<pow::cuckoo_miner::CuckooMinerDeviceStats>>>,
}
/// Struct to return relevant information about peers
#[derive(Clone, Debug)]
pub struct PeerStats {
/// Current state of peer
pub state: String,
/// Address
pub addr: String,
/// version running
pub version: u32,
/// version running
pub total_difficulty: u64,
/// direction
pub direction: String,
}
impl PeerStats {
/// Convert from a peer directly
pub fn from_peer(peer: &p2p::Peer) -> PeerStats {
// State
let mut state = "Disconnected";
if peer.is_connected() {
state = "Connected";
}
if peer.is_banned() {
state = "Banned";
}
let addr = peer.info.addr.to_string();
let direction = match peer.info.direction {
p2p::types::Direction::Inbound => "Inbound",
p2p::types::Direction::Outbound => "Outbound",
};
PeerStats {
state: state.to_string(),
addr: addr,
version: peer.info.version,
total_difficulty: peer.info.total_difficulty.into_num(),
direction: direction.to_string(),
}
}
} }
impl Default for MiningStats { impl Default for MiningStats {
@ -239,6 +287,7 @@ impl Default for MiningStats {
block_height: 0, block_height: 0,
network_difficulty: 0, network_difficulty: 0,
cuckoo_size: 0, cuckoo_size: 0,
device_stats: None,
} }
} }
} }

View file

@ -47,7 +47,7 @@ mod peers;
mod protocol; mod protocol;
mod serv; mod serv;
mod store; mod store;
mod types; pub mod types;
pub use serv::{DummyAdapter, Server}; pub use serv::{DummyAdapter, Server};
pub use peers::Peers; pub use peers::Peers;

View file

@ -42,7 +42,9 @@ extern crate time;
extern crate grin_core as core; extern crate grin_core as core;
extern crate grin_util as util; extern crate grin_util as util;
extern crate cuckoo_miner; // Re-export (mostly for stat collection)
pub extern crate cuckoo_miner as cuckoo_;
pub use cuckoo_ as cuckoo_miner;
mod siphash; mod siphash;
pub mod plugin; pub mod plugin;

View file

@ -34,7 +34,7 @@ extern crate grin_util as util;
extern crate grin_wallet as wallet; extern crate grin_wallet as wallet;
mod client; mod client;
mod ui; pub mod tui;
use std::thread; use std::thread;
use std::sync::Arc; use std::sync::Arc;
@ -49,6 +49,7 @@ use config::GlobalConfig;
use core::global; use core::global;
use core::core::amount_to_hr_string; use core::core::amount_to_hr_string;
use util::{init_logger, LoggingConfig, LOGGER}; use util::{init_logger, LoggingConfig, LOGGER};
use tui::ui;
/// wrap below to allow UI to clean up on stop /// wrap below to allow UI to clean up on stop
fn start_server(config: grin::ServerConfig) { fn start_server(config: grin::ServerConfig) {
@ -398,8 +399,8 @@ fn server_command(server_args: &ArgMatches, global_config: GlobalConfig) {
if let Some(true) = server_config.run_wallet_listener { if let Some(true) = server_config.run_wallet_listener {
let mut wallet_config = global_config.members.unwrap().wallet; let mut wallet_config = global_config.members.unwrap().wallet;
let wallet_seed = let wallet_seed = wallet::WalletSeed::from_file(&wallet_config)
wallet::WalletSeed::from_file(&wallet_config).expect("Failed to read wallet seed file."); .expect("Failed to read wallet seed file.");
let mut keychain = wallet_seed let mut keychain = wallet_seed
.derive_keychain("") .derive_keychain("")
.expect("Failed to derive keychain from seed file and passphrase."); .expect("Failed to derive keychain from seed file and passphrase.");

57
src/bin/tui/constants.rs Normal file
View file

@ -0,0 +1,57 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Identifiers for various TUI elements, because they may be referenced
//! from a few different places
// Basic Status view
pub const VIEW_BASIC_STATUS: &str = "basic_status_view";
// Peer/Sync View
pub const VIEW_PEER_SYNC: &str = "peer_sync_view";
pub const TABLE_PEER_STATUS: &str = "peer_status_table";
// Mining View
pub const VIEW_MINING: &str = "mining_view";
pub const TABLE_MINING_STATUS: &str = "mining_status_table";
// Menu and root elements
pub const ROOT_STACK: &str = "root_stack";
// Logo (not final, to be used somewhere eventually
pub const _WELCOME_LOGO: &str = " GGGGG GGGGGGG
GGGGGGG GGGGGGGGG
GGGGGGGGG GGGG GGGGGGGGGG
GGGGGGGGGGG GGGGGGGG GGGGGGGGGGG
GGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGG
GGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
GGGGGG
GGGGGGG
GGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG
GGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGG
GGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG
GGGGGGGGGGG GGGGGGGG GGGGGGGGGGGG
GGGGGGGGGG GGGGGGGG GGGGGGGGGGG
GGGGGGGG GGGGGGGG GGGGGGGGG
GGGGGGG GGGGGGGG GGGGGGG
GGGG GGGGGGGG GGGG
GG GGGGGGGG GG
GGGGGGGG ";

68
src/bin/tui/menu.rs Normal file
View file

@ -0,0 +1,68 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Main Menu definition
use cursive::Cursive;
use cursive::view::AnyView;
use cursive::align::HAlign;
use cursive::event::{EventResult, Key};
use cursive::views::{BoxView, LinearLayout, OnEventView, SelectView, StackView, TextView};
use cursive::direction::Orientation;
use tui::constants::*;
pub fn create() -> Box<AnyView> {
let mut main_menu = SelectView::new().h_align(HAlign::Left);
main_menu.add_item("Basic Status", VIEW_BASIC_STATUS);
main_menu.add_item("Peers and Sync", VIEW_PEER_SYNC);
main_menu.add_item("Mining", VIEW_MINING);
let change_view = |s: &mut Cursive, v: &str| {
if v == "" {
return;
}
let _ = s.call_on_id(ROOT_STACK, |sv: &mut StackView| {
let pos = sv.find_layer_from_id(v).unwrap();
sv.move_to_front(pos);
});
};
main_menu.set_on_submit(change_view);
let main_menu = OnEventView::new(main_menu)
.on_pre_event_inner('k', |s| {
s.select_up(1);
Some(EventResult::Consumed(None))
})
.on_pre_event_inner('j', |s| {
s.select_down(1);
Some(EventResult::Consumed(None))
})
.on_pre_event_inner(Key::Tab, |s| {
if s.selected_id().unwrap() == s.len() - 1 {
s.set_selection(0);
} else {
s.select_down(1);
}
Some(EventResult::Consumed(None))
});
let main_menu = LinearLayout::new(Orientation::Vertical)
.child(BoxView::with_full_height(main_menu))
.child(TextView::new("------------------"))
.child(TextView::new("Tab/Arrow : Cycle "))
.child(TextView::new("Enter : Select"))
.child(TextView::new("Q : Quit "));
Box::new(main_menu)
}

178
src/bin/tui/mining.rs Normal file
View file

@ -0,0 +1,178 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Mining status view definition
use std::cmp::Ordering;
use cursive::Cursive;
use cursive::view::AnyView;
use cursive::views::{BoxView, Dialog, LinearLayout, TextView};
use cursive::direction::Orientation;
use cursive::traits::*;
use tui::constants::*;
use tui::types::*;
use grin::types::ServerStats;
use tui::pow::cuckoo_miner::CuckooMinerDeviceStats;
use tui::table::{TableView, TableViewItem};
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
enum MiningDeviceColumn {
PluginId,
DeviceId,
DeviceName,
InUse,
ErrorStatus,
LastGraphTime,
GraphsPerSecond,
}
impl MiningDeviceColumn {
fn _as_str(&self) -> &str {
match *self {
MiningDeviceColumn::PluginId => "Plugin ID",
MiningDeviceColumn::DeviceId => "Device ID",
MiningDeviceColumn::DeviceName => "Name",
MiningDeviceColumn::InUse => "In Use",
MiningDeviceColumn::ErrorStatus => "Status",
MiningDeviceColumn::LastGraphTime => "Last Graph Time",
MiningDeviceColumn::GraphsPerSecond => "GPS",
}
}
}
impl TableViewItem<MiningDeviceColumn> for CuckooMinerDeviceStats {
fn to_column(&self, column: MiningDeviceColumn) -> String {
let last_solution_time_secs = self.last_solution_time as f64 / 1000000000.0;
match column {
MiningDeviceColumn::PluginId => String::from("TBD"),
MiningDeviceColumn::DeviceId => self.device_id.clone(),
MiningDeviceColumn::DeviceName => self.device_name.clone(),
MiningDeviceColumn::InUse => match self.in_use {
1 => String::from("Yes"),
_ => String::from("No"),
},
MiningDeviceColumn::ErrorStatus => match self.has_errored {
0 => String::from("OK"),
_ => String::from("Errored"),
},
MiningDeviceColumn::LastGraphTime => {
String::from(format!("{}s", last_solution_time_secs))
}
MiningDeviceColumn::GraphsPerSecond => {
String::from(format!("{:.*}", 4, 1.0 / last_solution_time_secs))
}
}
}
fn cmp(&self, other: &Self, column: MiningDeviceColumn) -> Ordering
where
Self: Sized,
{
let last_solution_time_secs_self = self.last_solution_time as f64 / 1000000000.0;
let gps_self = 1.0 / last_solution_time_secs_self;
let last_solution_time_secs_other = other.last_solution_time as f64 / 1000000000.0;
let gps_other = 1.0 / last_solution_time_secs_other;
match column {
MiningDeviceColumn::PluginId => Ordering::Equal,
MiningDeviceColumn::DeviceId => self.device_id.cmp(&other.device_id),
MiningDeviceColumn::DeviceName => self.device_name.cmp(&other.device_name),
MiningDeviceColumn::InUse => self.in_use.cmp(&other.in_use),
MiningDeviceColumn::ErrorStatus => self.has_errored.cmp(&other.has_errored),
MiningDeviceColumn::LastGraphTime => {
self.last_solution_time.cmp(&other.last_solution_time)
}
MiningDeviceColumn::GraphsPerSecond => gps_self.partial_cmp(&gps_other).unwrap(),
}
}
}
/// Mining status view
pub struct TUIMiningView;
impl TUIStatusListener for TUIMiningView {
/// Create the mining view
fn create() -> Box<AnyView> {
let table_view =
TableView::<CuckooMinerDeviceStats, MiningDeviceColumn>::new()
.column(MiningDeviceColumn::PluginId, "Plugin ID", |c| {
c.width_percent(10)
})
.column(MiningDeviceColumn::DeviceId, "Device ID", |c| {
c.width_percent(10)
})
.column(MiningDeviceColumn::DeviceName, "Device Name", |c| {
c.width_percent(20)
})
.column(MiningDeviceColumn::InUse, "In Use", |c| c.width_percent(10))
.column(MiningDeviceColumn::ErrorStatus, "Status", |c| {
c.width_percent(10)
})
.column(MiningDeviceColumn::LastGraphTime, "Graph Time", |c| {
c.width_percent(10)
})
.column(MiningDeviceColumn::GraphsPerSecond, "GPS", |c| {
c.width_percent(10)
});
let status_view = LinearLayout::new(Orientation::Vertical)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new(" ").with_id("basic_mining_config_status")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new(" ").with_id("basic_mining_status")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new(" ").with_id("basic_network_info")),
);
let mining_view = LinearLayout::new(Orientation::Vertical)
.child(status_view)
.child(BoxView::with_full_screen(
Dialog::around(table_view.with_id(TABLE_MINING_STATUS).min_size((50, 20)))
.title("Mining Devices"),
));
Box::new(mining_view.with_id(VIEW_MINING))
}
/// update
fn update(c: &mut Cursive, stats: &ServerStats) {
let mining_stats = stats.mining_stats.clone();
let device_stats = mining_stats.device_stats;
if device_stats.is_none() {
return;
}
let device_stats = device_stats.unwrap();
let mut flattened_device_stats = vec![];
for p in device_stats.into_iter() {
for d in p.into_iter() {
flattened_device_stats.push(d);
}
}
let _ = c.call_on_id(
TABLE_MINING_STATUS,
|t: &mut TableView<CuckooMinerDeviceStats, MiningDeviceColumn>| {
t.set_items(flattened_device_stats);
},
);
}
}

25
src/bin/tui/mod.rs Normal file
View file

@ -0,0 +1,25 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Grin TUI
extern crate grin_pow as pow;
pub mod ui;
mod table;
mod peers;
mod constants;
mod menu;
mod status;
mod mining;
mod types;

105
src/bin/tui/peers.rs Normal file
View file

@ -0,0 +1,105 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! TUI peer display
use std::cmp::Ordering;
use grin::types::{PeerStats, ServerStats};
use cursive::Cursive;
use cursive::view::AnyView;
use cursive::views::{BoxView, Dialog};
use cursive::traits::*;
use tui::table::{TableView, TableViewItem};
use tui::constants::*;
use tui::types::*;
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
enum PeerColumn {
Address,
State,
TotalDifficulty,
Direction,
Version,
}
impl PeerColumn {
fn _as_str(&self) -> &str {
match *self {
PeerColumn::Address => "Address",
PeerColumn::State => "State",
PeerColumn::Version => "Version",
PeerColumn::TotalDifficulty => "Total Difficulty",
PeerColumn::Direction => "Direction",
}
}
}
impl TableViewItem<PeerColumn> for PeerStats {
fn to_column(&self, column: PeerColumn) -> String {
match column {
PeerColumn::Address => self.addr.clone(),
PeerColumn::State => self.state.clone(),
PeerColumn::TotalDifficulty => self.total_difficulty.to_string(),
PeerColumn::Direction => self.direction.clone(),
PeerColumn::Version => self.version.to_string(),
}
}
fn cmp(&self, other: &Self, column: PeerColumn) -> Ordering
where
Self: Sized,
{
match column {
PeerColumn::Address => self.addr.cmp(&other.addr),
PeerColumn::State => self.state.cmp(&other.state),
PeerColumn::TotalDifficulty => self.total_difficulty.cmp(&other.total_difficulty),
PeerColumn::Direction => self.direction.cmp(&other.direction),
PeerColumn::Version => self.version.cmp(&other.version),
}
}
}
pub struct TUIPeerView;
impl TUIStatusListener for TUIPeerView {
fn create() -> Box<AnyView> {
let table_view =
TableView::<PeerStats, PeerColumn>::new()
.column(PeerColumn::Address, "Address", |c| c.width_percent(20))
.column(PeerColumn::State, "State", |c| c.width_percent(20))
.column(PeerColumn::Direction, "Direction", |c| c.width_percent(20))
.column(PeerColumn::TotalDifficulty, "Total Difficulty", |c| {
c.width_percent(20)
})
.column(PeerColumn::Version, "Version", |c| c.width_percent(20));
let peer_status_view = BoxView::with_full_screen(
Dialog::around(table_view.with_id(TABLE_PEER_STATUS).min_size((50, 20)))
.title("Connected Peers"),
).with_id(VIEW_PEER_SYNC);
Box::new(peer_status_view)
}
fn update(c: &mut Cursive, stats: &ServerStats) {
let _ = c.call_on_id(
TABLE_PEER_STATUS,
|t: &mut TableView<PeerStats, PeerColumn>| {
t.set_items(stats.peer_stats.clone());
},
);
}
}

139
src/bin/tui/status.rs Normal file
View file

@ -0,0 +1,139 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Basic status view definition
use cursive::Cursive;
use cursive::view::AnyView;
use cursive::views::{BoxView, LinearLayout, TextView};
use cursive::direction::Orientation;
use cursive::traits::*;
use tui::constants::*;
use tui::types::*;
use grin::ServerStats;
pub struct TUIStatusView;
impl TUIStatusListener for TUIStatusView {
/// Create basic status view
fn create() -> Box<AnyView> {
let basic_status_view = BoxView::with_full_screen(
LinearLayout::new(Orientation::Vertical)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new("Current Status: "))
.child(TextView::new("Starting").with_id("basic_current_status")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new("Connected Peers: "))
.child(TextView::new("0").with_id("connected_peers")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new("Chain Height: "))
.child(TextView::new(" ").with_id("chain_height")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new("------------------------")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new(" ").with_id("basic_mining_config_status")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new(" ").with_id("basic_mining_status")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new(" ").with_id("basic_network_info")),
), //.child(logo_view)
);
Box::new(basic_status_view.with_id(VIEW_BASIC_STATUS))
}
/// update
fn update(c: &mut Cursive, stats: &ServerStats) {
//find and update here as needed
let basic_status = {
if stats.is_syncing {
if stats.awaiting_peers {
"Waiting for peers".to_string()
} else {
format!("Syncing - Latest header: {}", stats.header_head.height).to_string()
}
} else {
"Running".to_string()
}
};
let basic_mining_config_status = {
if stats.mining_stats.is_enabled {
"Configured as mining node"
} else {
"Configured as validating node only (not mining)"
}
};
let (basic_mining_status, basic_network_info) = {
if stats.mining_stats.is_enabled {
if stats.is_syncing {
(
"Mining Status: Paused while syncing".to_string(),
" ".to_string(),
)
} else if stats.mining_stats.combined_gps == 0.0 {
(
"Mining Status: Starting miner and awating first solution...".to_string(),
" ".to_string(),
)
} else {
(
format!(
"Mining Status: Mining at height {} at {:.*} GPS",
stats.mining_stats.block_height, 4, stats.mining_stats.combined_gps
),
format!(
"Cuckoo {} - Network Difficulty {}",
stats.mining_stats.cuckoo_size,
stats.mining_stats.network_difficulty.to_string()
),
)
}
} else {
(" ".to_string(), " ".to_string())
}
};
c.call_on_id("basic_current_status", |t: &mut TextView| {
t.set_content(basic_status);
});
c.call_on_id("connected_peers", |t: &mut TextView| {
t.set_content(stats.peer_count.to_string());
});
c.call_on_id("chain_height", |t: &mut TextView| {
t.set_content(stats.head.height.to_string());
});
c.call_on_id("basic_mining_config_status", |t: &mut TextView| {
t.set_content(basic_mining_config_status);
});
c.call_on_id("basic_mining_status", |t: &mut TextView| {
t.set_content(basic_mining_status);
});
c.call_on_id("basic_network_info", |t: &mut TextView| {
t.set_content(basic_network_info);
});
}
}

985
src/bin/tui/table.rs Normal file
View file

@ -0,0 +1,985 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Copyright (c) 2015-2017 Ivo Wetzel
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//! Adapted from https://github.com/behnam/rust-cursive-table-view
//! A basic table view implementation for [cursive](https://crates.io/crates/cursive).
#![deny(missing_docs, missing_copy_implementations, trivial_casts, trivial_numeric_casts,
unsafe_code, unused_import_braces, unused_qualifications)]
// Crate Dependencies ---------------------------------------------------------
extern crate cursive;
// STD Dependencies -----------------------------------------------------------
use std::rc::Rc;
use std::hash::Hash;
use std::cmp::{self, Ordering};
use std::collections::HashMap;
// External Dependencies ------------------------------------------------------
use cursive::With;
use cursive::vec::Vec2;
use cursive::align::HAlign;
use cursive::theme::ColorStyle;
use cursive::{Cursive, Printer};
use cursive::direction::Direction;
use cursive::view::{ScrollBase, View};
use cursive::event::{Callback, Event, EventResult, Key};
use cursive::theme::PaletteColor::*;
/// A trait for displaying and sorting items inside a
/// [`TableView`](struct.TableView.html).
pub trait TableViewItem<H>: Clone + Sized
where
H: Eq + Hash + Copy + Clone + 'static,
{
/// Method returning a string representation of the item for the
/// specified column from type `H`.
fn to_column(&self, column: H) -> String;
/// Method comparing two items via their specified column from type `H`.
fn cmp(&self, other: &Self, column: H) -> Ordering
where
Self: Sized;
}
/// View to select an item among a list, supporting multiple columns for sorting.
///
/// # Examples
///
/// ```rust
/// # extern crate cursive;
/// # extern crate cursive_table_view;
/// # use std::cmp::Ordering;
/// # use cursive_table_view::{TableView, TableViewItem};
/// # use cursive::align::HAlign;
/// # fn main() {
/// // Provide a type for the table's columns
/// #[derive(Copy, Clone, PartialEq, Eq, Hash)]
/// enum BasicColumn {
/// Name,
/// Count,
/// Rate
/// }
///
/// // Define the item type
/// #[derive(Clone, Debug)]
/// struct Foo {
/// name: String,
/// count: usize,
/// rate: usize
/// }
///
/// impl TableViewItem<BasicColumn> for Foo {
///
/// fn to_column(&self, column: BasicColumn) -> String {
/// match column {
/// BasicColumn::Name => self.name.to_string(),
/// BasicColumn::Count => format!("{}", self.count),
/// BasicColumn::Rate => format!("{}", self.rate)
/// }
/// }
///
/// fn cmp(&self, other: &Self, column: BasicColumn) -> Ordering where Self: Sized {
/// match column {
/// BasicColumn::Name => self.name.cmp(&other.name),
/// BasicColumn::Count => self.count.cmp(&other.count),
/// BasicColumn::Rate => self.rate.cmp(&other.rate)
/// }
/// }
///
/// }
///
/// // Configure the actual table
/// let table = TableView::<Foo, BasicColumn>::new()
/// .column(BasicColumn::Name, "Name", |c| c.width(20))
/// .column(BasicColumn::Count, "Count", |c| c.align(HAlign::Center))
/// .column(BasicColumn::Rate, "Rate", |c| {
/// c.ordering(Ordering::Greater).align(HAlign::Right).width(20)
/// })
/// .default_column(BasicColumn::Name);
/// # }
/// ```
pub struct TableView<T: TableViewItem<H>, H: Eq + Hash + Copy + Clone + 'static> {
enabled: bool,
scrollbase: ScrollBase,
last_size: Vec2,
column_select: bool,
columns: Vec<TableColumn<H>>,
column_indicies: HashMap<H, usize>,
focus: usize,
items: Vec<T>,
rows_to_items: Vec<usize>,
on_sort: Option<Rc<Fn(&mut Cursive, H, Ordering)>>,
// TODO Pass drawing offsets into the handlers so a popup menu
// can be created easily?
on_submit: Option<Rc<Fn(&mut Cursive, usize, usize)>>,
on_select: Option<Rc<Fn(&mut Cursive, usize, usize)>>,
}
impl<T: TableViewItem<H>, H: Eq + Hash + Copy + Clone + 'static> TableView<T, H> {
/// Creates a new empty `TableView` without any columns.
///
/// A TableView should be accompanied by a enum of type `H` representing
/// the table columns.
pub fn new() -> Self {
Self {
enabled: true,
scrollbase: ScrollBase::new(),
last_size: Vec2::new(0, 0),
column_select: false,
columns: Vec::new(),
column_indicies: HashMap::new(),
focus: 0,
items: Vec::new(),
rows_to_items: Vec::new(),
on_sort: None,
on_submit: None,
on_select: None,
}
}
/// Adds a column for the specified table colum from type `H` along with
/// a title for its visual display.
///
/// The provided callback can be used to further configure the
/// created [`TableColumn`](struct.TableColumn.html).
pub fn column<S: Into<String>, C: FnOnce(TableColumn<H>) -> TableColumn<H>>(
mut self,
column: H,
title: S,
callback: C,
) -> Self {
self.column_indicies.insert(column, self.columns.len());
self.columns
.push(callback(TableColumn::new(column, title.into())));
// Make the first colum the default one
if self.columns.len() == 1 {
self.default_column(column)
} else {
self
}
}
/// Sets the initially active column of the table.
pub fn default_column(mut self, column: H) -> Self {
if self.column_indicies.contains_key(&column) {
for c in &mut self.columns {
c.selected = c.column == column;
if c.selected {
c.order = c.default_order;
} else {
c.order = Ordering::Equal;
}
}
}
self
}
/// Sorts the table using the specified table `column` and the passed
/// `order`.
pub fn sort_by(&mut self, column: H, order: Ordering) {
if self.column_indicies.contains_key(&column) {
for c in &mut self.columns {
c.selected = c.column == column;
if c.selected {
c.order = order;
} else {
c.order = Ordering::Equal;
}
}
}
self.sort_items(column, order);
}
/// Sorts the table using the currently active column and its
/// ordering.
pub fn sort(&mut self) {
if let Some((column, order)) = self.order() {
self.sort_items(column, order);
}
}
/// Returns the currently active column that is used for sorting
/// along with its ordering.
///
/// Might return `None` if there are currently no items in the table
/// and it has not been sorted yet.
pub fn order(&self) -> Option<(H, Ordering)> {
for c in &self.columns {
if c.order != Ordering::Equal {
return Some((c.column, c.order));
}
}
None
}
/// Disables this view.
///
/// A disabled view cannot be selected.
pub fn disable(&mut self) {
self.enabled = false;
}
/// Re-enables this view.
pub fn enable(&mut self) {
self.enabled = true;
}
/// Enable or disable this view.
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
/// Returns `true` if this view is enabled.
pub fn is_enabled(&self) -> bool {
self.enabled
}
/// Sets a callback to be used when a selected column is sorted by
/// pressing `<Enter>`.
///
/// # Example
///
/// ```norun
/// table.set_on_sort(|siv: &mut Cursive, column: BasicColumn, order: Ordering| {
///
/// });
/// ```
pub fn set_on_sort<F>(&mut self, cb: F)
where
F: Fn(&mut Cursive, H, Ordering) + 'static,
{
self.on_sort = Some(Rc::new(move |s, h, o| cb(s, h, o)));
}
/// Sets a callback to be used when a selected column is sorted by
/// pressing `<Enter>`.
///
/// Chainable variant.
///
/// # Example
///
/// ```norun
/// table.on_sort(|siv: &mut Cursive, column: BasicColumn, order: Ordering| {
///
/// });
/// ```
pub fn on_sort<F>(self, cb: F) -> Self
where
F: Fn(&mut Cursive, H, Ordering) + 'static,
{
self.with(|t| t.set_on_sort(cb))
}
/// Sets a callback to be used when `<Enter>` is pressed while an item
/// is selected.
///
/// Both the currently selected row and the index of the corresponding item
/// within the underlying storage vector will be given to the callback.
///
/// # Example
///
/// ```norun
/// table.set_on_submit(|siv: &mut Cursive, row: usize, index: usize| {
///
/// });
/// ```
pub fn set_on_submit<F>(&mut self, cb: F)
where
F: Fn(&mut Cursive, usize, usize) + 'static,
{
self.on_submit = Some(Rc::new(move |s, row, index| cb(s, row, index)));
}
/// Sets a callback to be used when `<Enter>` is pressed while an item
/// is selected.
///
/// Both the currently selected row and the index of the corresponding item
/// within the underlying storage vector will be given to the callback.
///
/// Chainable variant.
///
/// # Example
///
/// ```norun
/// table.on_submit(|siv: &mut Cursive, row: usize, index: usize| {
///
/// });
/// ```
pub fn on_submit<F>(self, cb: F) -> Self
where
F: Fn(&mut Cursive, usize, usize) + 'static,
{
self.with(|t| t.set_on_submit(cb))
}
/// Sets a callback to be used when an item is selected.
///
/// Both the currently selected row and the index of the corresponding item
/// within the underlying storage vector will be given to the callback.
///
/// # Example
///
/// ```norun
/// table.set_on_select(|siv: &mut Cursive, row: usize, index: usize| {
///
/// });
/// ```
pub fn set_on_select<F>(&mut self, cb: F)
where
F: Fn(&mut Cursive, usize, usize) + 'static,
{
self.on_select = Some(Rc::new(move |s, row, index| cb(s, row, index)));
}
/// Sets a callback to be used when an item is selected.
///
/// Both the currently selected row and the index of the corresponding item
/// within the underlying storage vector will be given to the callback.
///
/// Chainable variant.
///
/// # Example
///
/// ```norun
/// table.on_select(|siv: &mut Cursive, row: usize, index: usize| {
///
/// });
/// ```
pub fn on_select<F>(self, cb: F) -> Self
where
F: Fn(&mut Cursive, usize, usize) + 'static,
{
self.with(|t| t.set_on_select(cb))
}
/// Removes all items from this view.
pub fn clear(&mut self) {
self.items.clear();
self.rows_to_items.clear();
self.focus = 0;
}
/// Returns the number of items in this table.
pub fn len(&self) -> usize {
self.items.len()
}
/// Returns `true` if this table has no items.
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
/// Returns the index of the currently selected table row.
pub fn row(&self) -> Option<usize> {
if self.items.is_empty() {
None
} else {
Some(self.focus)
}
}
/// Selects the row at the specified index.
pub fn set_selected_row(&mut self, row_index: usize) {
self.focus = row_index;
self.scrollbase.scroll_to(row_index);
}
/// Selects the row at the specified index.
///
/// Chainable variant.
pub fn selected_row(self, row_index: usize) -> Self {
self.with(|t| t.set_selected_row(row_index))
}
/// Sets the contained items of the table.
///
/// The currently active sort order is preserved and will be applied to all
/// items.
pub fn set_items(&mut self, items: Vec<T>) {
self.items = items;
self.rows_to_items = Vec::with_capacity(self.items.len());
for i in 0..self.items.len() {
self.rows_to_items.push(i);
}
if let Some((column, order)) = self.order() {
self.sort_by(column, order);
}
self.scrollbase
.set_heights(self.last_size.y.saturating_sub(2), self.rows_to_items.len());
self.set_selected_row(0);
}
/// Sets the contained items of the table.
///
/// The order of the items will be preserved even when the table is sorted.
///
/// Chainable variant.
pub fn items(self, items: Vec<T>) -> Self {
self.with(|t| t.set_items(items))
}
/// Returns a immmutable reference to the item at the specified index
/// within the underlying storage vector.
pub fn borrow_item(&mut self, index: usize) -> Option<&T> {
self.items.get(index)
}
/// Returns a mutable reference to the item at the specified index within
/// the underlying storage vector.
pub fn borrow_item_mut(&mut self, index: usize) -> Option<&mut T> {
self.items.get_mut(index)
}
/// Returns a immmutable reference to the items contained within the table.
pub fn borrow_items(&mut self) -> &Vec<T> {
&self.items
}
/// Returns a mutable reference to the items contained within the table.
///
/// Can be used to modify the items in place.
pub fn borrow_items_mut(&mut self) -> &mut Vec<T> {
&mut self.items
}
/// Returns the index of the currently selected item within the underlying
/// storage vector.
pub fn item(&self) -> Option<usize> {
if self.items.is_empty() {
None
} else {
Some(self.rows_to_items[self.focus])
}
}
/// Selects the item at the specified index within the underlying storage
/// vector.
pub fn set_selected_item(&mut self, item_index: usize) {
// TODO optimize the performance for very large item lists
if item_index < self.items.len() {
for (row, item) in self.rows_to_items.iter().enumerate() {
if *item == item_index {
self.focus = row;
self.scrollbase.scroll_to(row);
break;
}
}
}
}
/// Selects the item at the specified index within the underlying storage
/// vector.
///
/// Chainable variant.
pub fn selected_item(self, item_index: usize) -> Self {
self.with(|t| t.set_selected_item(item_index))
}
/// Inserts a new item into the table.
///
/// The currently active sort order is preserved and will be applied to the
/// newly inserted item.
pub fn insert_item(&mut self, item: T) {
self.items.push(item);
self.rows_to_items.push(self.items.len());
self.scrollbase
.set_heights(self.last_size.y.saturating_sub(2), self.rows_to_items.len());
if let Some((column, order)) = self.order() {
self.sort_by(column, order);
}
}
/// Removes the item at the specified index within the underlying storage
/// vector and returns it.
pub fn remove_item(&mut self, item_index: usize) -> Option<T> {
if item_index < self.items.len() {
// Move the selection if the currently selected item gets removed
if let Some(selected_index) = self.item() {
if selected_index == item_index {
self.focus_up(1);
}
}
// Remove the sorted reference to the item
self.rows_to_items.retain(|i| *i != item_index);
// Adjust remaining references
for ref_index in &mut self.rows_to_items {
if *ref_index > item_index {
*ref_index -= 1;
}
}
// Update scroll height to prevent out of index drawing
self.scrollbase
.set_heights(self.last_size.y.saturating_sub(2), self.rows_to_items.len());
// Remove actual item from the underlying storage
Some(self.items.remove(item_index))
} else {
None
}
}
/// Removes all items from the underlying storage and returns them.
pub fn take_items(&mut self) -> Vec<T> {
self.scrollbase
.set_heights(self.last_size.y.saturating_sub(2), 0);
self.set_selected_row(0);
self.rows_to_items.clear();
self.items.drain(0..).collect()
}
}
impl<T: TableViewItem<H>, H: Eq + Hash + Copy + Clone + 'static> TableView<T, H> {
fn draw_columns<C: Fn(&Printer, &TableColumn<H>)>(
&self,
printer: &Printer,
sep: &str,
callback: C,
) {
let mut column_offset = 0;
let column_count = self.columns.len();
for (index, column) in self.columns.iter().enumerate() {
let printer = &printer.sub_printer((column_offset, 0), printer.size, true);
callback(printer, column);
if index < column_count - 1 {
printer.print((column.width + 1, 0), sep);
}
column_offset += column.width + 3;
}
}
fn sort_items(&mut self, column: H, order: Ordering) {
if !self.is_empty() {
let old_item = self.item().unwrap();
let mut rows_to_items = self.rows_to_items.clone();
rows_to_items.sort_by(|a, b| {
if order == Ordering::Less {
self.items[*a].cmp(&self.items[*b], column)
} else {
self.items[*b].cmp(&self.items[*a], column)
}
});
self.rows_to_items = rows_to_items;
self.set_selected_item(old_item);
}
}
fn draw_item(&self, printer: &Printer, i: usize) {
self.draw_columns(printer, "", |printer, column| {
let value = self.items[self.rows_to_items[i]].to_column(column.column);
column.draw_row(printer, value.as_str());
});
}
fn focus_up(&mut self, n: usize) {
self.focus -= cmp::min(self.focus, n);
}
fn focus_down(&mut self, n: usize) {
self.focus = cmp::min(self.focus + n, self.items.len() - 1);
}
fn active_column(&self) -> usize {
self.columns.iter().position(|c| c.selected).unwrap_or(0)
}
fn column_cancel(&mut self) {
self.column_select = false;
for column in &mut self.columns {
column.selected = column.order != Ordering::Equal;
}
}
fn column_next(&mut self) -> bool {
let column = self.active_column();
if column < self.columns.len() - 1 {
self.columns[column].selected = false;
self.columns[column + 1].selected = true;
true
} else {
false
}
}
fn column_prev(&mut self) -> bool {
let column = self.active_column();
if column > 0 {
self.columns[column].selected = false;
self.columns[column - 1].selected = true;
true
} else {
false
}
}
fn column_select(&mut self) {
let next = self.active_column();
let column = self.columns[next].column;
let current = self.columns
.iter()
.position(|c| c.order != Ordering::Equal)
.unwrap_or(0);
let order = if current != next {
self.columns[next].default_order
} else if self.columns[current].order == Ordering::Less {
Ordering::Greater
} else {
Ordering::Less
};
self.sort_by(column, order);
}
}
impl<T: TableViewItem<H> + 'static, H: Eq + Hash + Copy + Clone + 'static> View
for TableView<T, H> {
fn draw(&self, printer: &Printer) {
self.draw_columns(printer, "", |printer, column| {
let color = if column.order != Ordering::Equal || column.selected {
if self.column_select && column.selected && self.enabled && printer.focused {
Highlight
} else {
HighlightInactive
}
} else {
Primary
};
printer.with_color(ColorStyle::from(color), |printer| {
column.draw_header(printer);
});
});
self.draw_columns(
&printer.sub_printer((0, 1), printer.size, true),
"┴─",
|printer, column| {
printer.print_hline((0, 0), column.width + 1, "");
},
);
let printer = &printer.sub_printer((0, 2), printer.size, true);
self.scrollbase.draw(printer, |printer, i| {
let color = if i == self.focus {
if !self.column_select && self.enabled && printer.focused {
Highlight
} else {
HighlightInactive
}
} else {
Primary
};
printer.with_color(ColorStyle::from(color), |printer| {
self.draw_item(printer, i);
});
});
}
fn layout(&mut self, size: Vec2) {
if size == self.last_size {
return;
}
let item_count = self.items.len();
let column_count = self.columns.len();
// Split up all columns into sized / unsized groups
let (mut sized, mut usized): (Vec<&mut TableColumn<H>>, Vec<&mut TableColumn<H>>) =
self.columns
.iter_mut()
.partition(|c| c.requested_width.is_some());
// Subtract one for the seperators between our columns (that's column_count - 1)
let mut available_width = size.x.saturating_sub(column_count.saturating_sub(1) * 3);
// Reduce the with in case we are displaying a scrollbar
if size.y.saturating_sub(1) < item_count {
available_width = available_width.saturating_sub(2);
}
// Calculate widths for all requested columns
let mut remaining_width = available_width;
for column in &mut sized {
column.width = match *column.requested_width.as_ref().unwrap() {
TableColumnWidth::Percent(width) => cmp::min(
(size.x as f32 / 100.0 * width as f32).ceil() as usize,
remaining_width,
),
TableColumnWidth::Absolute(width) => width,
};
remaining_width = remaining_width.saturating_sub(column.width);
}
// Spread the remaining with across the unsized columns
let remaining_columns = usized.len();
for column in &mut usized {
column.width = (remaining_width as f32 / remaining_columns as f32).floor() as usize;
}
self.scrollbase
.set_heights(size.y.saturating_sub(2), item_count);
self.last_size = size;
}
fn take_focus(&mut self, _: Direction) -> bool {
self.enabled && !self.items.is_empty()
}
fn on_event(&mut self, event: Event) -> EventResult {
if !self.enabled {
return EventResult::Ignored;
}
let last_focus = self.focus;
match event {
Event::Key(Key::Right) => {
if self.column_select {
if !self.column_next() {
return EventResult::Ignored;
}
} else {
self.column_select = true;
}
}
Event::Key(Key::Left) => {
if self.column_select {
if !self.column_prev() {
return EventResult::Ignored;
}
} else {
self.column_select = true;
}
}
Event::Key(Key::Up) if self.focus > 0 || self.column_select => {
if self.column_select {
self.column_cancel();
} else {
self.focus_up(1);
}
}
Event::Key(Key::Down) if self.focus + 1 < self.items.len() || self.column_select => {
if self.column_select {
self.column_cancel();
} else {
self.focus_down(1);
}
}
Event::Key(Key::PageUp) => {
self.column_cancel();
self.focus_up(10);
}
Event::Key(Key::PageDown) => {
self.column_cancel();
self.focus_down(10);
}
Event::Key(Key::Home) => {
self.column_cancel();
self.focus = 0;
}
Event::Key(Key::End) => {
self.column_cancel();
self.focus = self.items.len() - 1;
}
Event::Key(Key::Enter) => {
if self.column_select {
self.column_select();
if self.on_sort.is_some() {
let c = &self.columns[self.active_column()];
let column = c.column;
let order = c.order;
let cb = self.on_sort.clone().unwrap();
return EventResult::Consumed(Some(Callback::from_fn(move |s| {
cb(s, column, order)
})));
}
} else if !self.is_empty() && self.on_submit.is_some() {
let cb = self.on_submit.clone().unwrap();
let row = self.row().unwrap();
let index = self.item().unwrap();
return EventResult::Consumed(Some(Callback::from_fn(move |s| {
cb(s, row, index)
})));
}
}
_ => return EventResult::Ignored,
}
let focus = self.focus;
self.scrollbase.scroll_to(focus);
if self.column_select {
EventResult::Consumed(None)
} else if !self.is_empty() && last_focus != focus {
let row = self.row().unwrap();
let index = self.item().unwrap();
EventResult::Consumed(
self.on_select
.clone()
.map(|cb| Callback::from_fn(move |s| cb(s, row, index))),
)
} else {
EventResult::Ignored
}
}
}
/// A type used for the construction of columns in a
/// [`TableView`](struct.TableView.html).
pub struct TableColumn<H: Copy + Clone + 'static> {
column: H,
title: String,
selected: bool,
alignment: HAlign,
order: Ordering,
width: usize,
default_order: Ordering,
requested_width: Option<TableColumnWidth>,
}
enum TableColumnWidth {
Percent(usize),
Absolute(usize),
}
impl<H: Copy + Clone + 'static> TableColumn<H> {
/// Sets the default ordering of the column.
pub fn ordering(mut self, order: Ordering) -> Self {
self.default_order = order;
self
}
/// Sets the horizontal text alignment of the column.
pub fn align(mut self, alignment: HAlign) -> Self {
self.alignment = alignment;
self
}
/// Sets how many characters of width this column will try to occupy.
pub fn width(mut self, width: usize) -> Self {
self.requested_width = Some(TableColumnWidth::Absolute(width));
self
}
/// Sets what percentage of the width of the entire table this column will
/// try to occupy.
pub fn width_percent(mut self, width: usize) -> Self {
self.requested_width = Some(TableColumnWidth::Percent(width));
self
}
fn new(column: H, title: String) -> Self {
Self {
column: column,
title: title,
selected: false,
alignment: HAlign::Left,
order: Ordering::Equal,
width: 0,
default_order: Ordering::Less,
requested_width: None,
}
}
fn draw_header(&self, printer: &Printer) {
let order = match self.order {
Ordering::Less => "^",
Ordering::Greater => "v",
Ordering::Equal => " ",
};
let header = match self.alignment {
HAlign::Left => format!(
"{:<width$} [{}]",
self.title,
order,
width = self.width.saturating_sub(4)
),
HAlign::Right => format!(
"{:>width$} [{}]",
self.title,
order,
width = self.width.saturating_sub(4)
),
HAlign::Center => format!(
"{:^width$} [{}]",
self.title,
order,
width = self.width.saturating_sub(4)
),
};
printer.print((0, 0), header.as_str());
}
fn draw_row(&self, printer: &Printer, value: &str) {
let value = match self.alignment {
HAlign::Left => format!("{:<width$} ", value, width = self.width),
HAlign::Right => format!("{:>width$} ", value, width = self.width),
HAlign::Center => format!("{:^width$} ", value, width = self.width),
};
printer.print((0, 0), value.as_str());
}
}

35
src/bin/tui/types.rs Normal file
View file

@ -0,0 +1,35 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Types specific to the UI module
use cursive::Cursive;
use cursive::view::AnyView;
use grin::types::ServerStats;
/// Main message struct to communicate between the UI and
/// the main process
pub enum UIMessage {
UpdateStatus(ServerStats),
}
/// Trait for a UI element that recieves status update messages
/// and updates itself
pub trait TUIStatusListener {
/// create the view, to return to the main UI controller
fn create() -> Box<AnyView>;
/// Update according to status update contents
fn update(c: &mut Cursive, stats: &ServerStats);
}

183
src/bin/tui/ui.rs Normal file
View file

@ -0,0 +1,183 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Basic TUI to better output the overall system status and status
//! of various subsystems
use std::sync::{mpsc, Arc};
use time;
use cursive::Cursive;
use cursive::theme::{BaseColor, BorderStyle, Color, Theme};
use cursive::theme::PaletteColor::*;
use cursive::theme::Color::*;
use cursive::theme::BaseColor::*;
use cursive::utils::markup::StyledString;
use cursive::views::{LinearLayout, Panel, StackView, TextView};
use cursive::direction::Orientation;
use cursive::traits::*;
use grin::Server;
//use util::LOGGER;
use tui::{menu, mining, peers, status};
use tui::types::*;
use tui::constants::*;
pub struct UI {
cursive: Cursive,
ui_rx: mpsc::Receiver<UIMessage>,
ui_tx: mpsc::Sender<UIMessage>,
controller_tx: mpsc::Sender<ControllerMessage>,
}
fn modify_theme(theme: &mut Theme) {
theme.shadow = false;
theme.borders = BorderStyle::Simple;
theme.palette[Background] = Dark(Black);
theme.palette[Shadow] = Dark(Black);
theme.palette[View] = Dark(Black);
theme.palette[Primary] = Dark(White);
theme.palette[Highlight] = Dark(Cyan);
theme.palette[HighlightInactive] = Dark(Blue);
// also secondary, tertiary, TitlePrimary, TitleSecondary
}
impl UI {
/// Create a new UI
pub fn new(controller_tx: mpsc::Sender<ControllerMessage>) -> UI {
let (ui_tx, ui_rx) = mpsc::channel::<UIMessage>();
let mut grin_ui = UI {
cursive: Cursive::new(),
ui_tx: ui_tx,
ui_rx: ui_rx,
controller_tx: controller_tx,
};
// Create UI objects, etc
let status_view = status::TUIStatusView::create();
let mining_view = mining::TUIMiningView::create();
let peer_view = peers::TUIPeerView::create();
let main_menu = menu::create();
let root_stack = StackView::new()
.layer(mining_view)
.layer(peer_view)
.layer(status_view)
.with_id(ROOT_STACK);
let mut title_string = StyledString::new();
title_string.append(StyledString::styled(
"Grin Version 0.0.1",
Color::Dark(BaseColor::Green),
));
let main_layer = LinearLayout::new(Orientation::Vertical)
.child(Panel::new(TextView::new(title_string)))
.child(
LinearLayout::new(Orientation::Horizontal)
.child(Panel::new(main_menu))
.child(Panel::new(root_stack)),
);
//set theme
let mut theme = grin_ui.cursive.current_theme().clone();
modify_theme(&mut theme);
grin_ui.cursive.set_theme(theme);
grin_ui.cursive.add_layer(main_layer);
// Configure a callback (shutdown, for the first test)
let controller_tx_clone = grin_ui.controller_tx.clone();
grin_ui.cursive.add_global_callback('q', move |_| {
controller_tx_clone
.send(ControllerMessage::Shutdown)
.unwrap();
});
grin_ui.cursive.set_fps(4);
grin_ui
}
/// Step the UI by calling into Cursive's step function, then
/// processing any UI messages
pub fn step(&mut self) -> bool {
if !self.cursive.is_running() {
return false;
}
// Process any pending UI messages
while let Some(message) = self.ui_rx.try_iter().next() {
match message {
UIMessage::UpdateStatus(update) => {
status::TUIStatusView::update(&mut self.cursive, &update);
mining::TUIMiningView::update(&mut self.cursive, &update);
peers::TUIPeerView::update(&mut self.cursive, &update);
}
}
}
// Step the UI
self.cursive.step();
true
}
/// Stop the UI
pub fn stop(&mut self) {
self.cursive.quit();
}
}
pub struct Controller {
rx: mpsc::Receiver<ControllerMessage>,
ui: UI,
}
pub enum ControllerMessage {
Shutdown,
}
impl Controller {
/// Create a new controller
pub fn new() -> Result<Controller, String> {
let (tx, rx) = mpsc::channel::<ControllerMessage>();
Ok(Controller {
rx: rx,
ui: UI::new(tx.clone()),
})
}
/// Run the controller
pub fn run(&mut self, server: Arc<Server>) {
let stat_update_interval = 1;
let mut next_stat_update = time::get_time().sec + stat_update_interval;
while self.ui.step() {
while let Some(message) = self.rx.try_iter().next() {
match message {
ControllerMessage::Shutdown => {
server.stop();
self.ui.stop();
/*self.ui
.ui_tx
.send(UIMessage::UpdateOutput("update".to_string()))
.unwrap();*/
}
}
}
if time::get_time().sec > next_stat_update {
let stats = server.get_server_stats().unwrap();
self.ui.ui_tx.send(UIMessage::UpdateStatus(stats)).unwrap();
next_stat_update = time::get_time().sec + stat_update_interval;
}
}
}
}

View file

@ -1,394 +0,0 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Basic TUI to better output the overall system status and status
//! of various subsystems
use std::sync::{mpsc, Arc};
use time;
use cursive::Cursive;
use cursive::theme::{BaseColor, BorderStyle, Color};
use cursive::theme::PaletteColor::*;
use cursive::theme::Color::*;
use cursive::theme::BaseColor::*;
use cursive::utils::markup::StyledString;
use cursive::align::{HAlign, VAlign};
use cursive::event::Key;
use cursive::views::{BoxView, LayerPosition, LinearLayout, Panel, StackView, TextView};
use cursive::direction::Orientation;
use cursive::traits::*;
use grin::Server;
const WELCOME_LOGO: &str = " GGGGG GGGGGGG
GGGGGGG GGGGGGGGG
GGGGGGGGG GGGG GGGGGGGGGG
GGGGGGGGGGG GGGGGGGG GGGGGGGGGGG
GGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGG
GGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
GGGGGG
GGGGGGG
GGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG
GGGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG
GGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG
GGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGG
GGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG
GGGGGGGGGGG GGGGGGGG GGGGGGGGGGGG
GGGGGGGGGG GGGGGGGG GGGGGGGGGGG
GGGGGGGG GGGGGGGG GGGGGGGGG
GGGGGGG GGGGGGGG GGGGGGG
GGGG GGGGGGGG GGGG
GG GGGGGGGG GG
GGGGGGGG ";
pub struct UI {
cursive: Cursive,
ui_rx: mpsc::Receiver<UIMessage>,
ui_tx: mpsc::Sender<UIMessage>,
controller_tx: mpsc::Sender<ControllerMessage>,
}
pub struct StatusUpdates {
pub basic_status: String,
pub peer_count: String,
pub chain_height: String,
pub basic_mining_config_status: String,
pub basic_mining_status: String,
pub basic_network_info: String,
}
pub enum UIMessage {
UpdateStatus(StatusUpdates),
}
impl UI {
/// Create a new UI
pub fn new(controller_tx: mpsc::Sender<ControllerMessage>) -> UI {
let (ui_tx, ui_rx) = mpsc::channel::<UIMessage>();
let mut grin_ui = UI {
cursive: Cursive::new(),
ui_tx: ui_tx,
ui_rx: ui_rx,
controller_tx: controller_tx,
};
let mut logo_string = StyledString::new();
logo_string.append(StyledString::styled(
WELCOME_LOGO,
Color::Dark(BaseColor::Green),
));
let mut title_string = StyledString::new();
title_string.append(StyledString::styled(
"Grin Version 0.0.1",
Color::Dark(BaseColor::Green),
));
let mut logo_view = TextView::new(logo_string)
.v_align(VAlign::Center)
.h_align(HAlign::Center);
logo_view.set_scrollable(false);
// Create UI objects, etc
let basic_status_view = BoxView::with_full_screen(
LinearLayout::new(Orientation::Horizontal)
.child(BoxView::with_full_screen(logo_view))
.child(BoxView::with_full_screen(
LinearLayout::new(Orientation::Vertical)
.child(TextView::new(title_string))
.child(TextView::new("------------------------"))
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new("Current Status: "))
.child(TextView::new("Starting").with_id("basic_current_status")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new("Connected Peers: "))
.child(TextView::new("0").with_id("connected_peers")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new("Chain Height: "))
.child(TextView::new("").with_id("chain_height")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new("------------------------")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new("").with_id("basic_mining_config_status")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new("").with_id("basic_mining_status")),
)
.child(
LinearLayout::new(Orientation::Horizontal)
.child(TextView::new("").with_id("basic_network_info")),
),
)),
).with_id("basic_status_view");
let advanced_status_view = BoxView::with_full_screen(TextView::new(
"Advanced Status Display will go here and should contain detailed readouts for:
--Latest Blocks
--Sync Info
--Chain Info
--Peer Info
--Mining Info
",
)).with_id("advanced_status");
let root_stack = StackView::new()
.layer(advanced_status_view)
.layer(basic_status_view)
.with_id("root_stack");
/*
let mut basic_button = Button::new("", |s| {
//
});
basic_button.set_label_raw("1 - Basic Status");
let mut advanced_button = Button::new("2 - Advanced Status", |s| {
//
});
//advanced_button.set_label_raw("2 - Advanced Status");
let mut config_button = Button::new("", |s| {
//
});
config_button.set_label_raw("3 - Config");
let mut quit_button = Button::new("", |s| {
//s.quit();
});
quit_button.set_label_raw("Quit");*/
let top_layer = LinearLayout::new(Orientation::Vertical)
.child(Panel::new(root_stack))
.child(
LinearLayout::new(Orientation::Horizontal)
.child(Panel::new(TextView::new(
"<TAB> Toggle Basic / Advanced view",
)))
.child(Panel::new(TextView::new("<Q> Quit"))),
);
/*.child(
LinearLayout::new(Orientation::Horizontal)
.child(Panel::new(basic_button))
.child(Panel::new(advanced_button))
.child(Panel::new(config_button))
.child(Panel::new(quit_button))
);*/
grin_ui.cursive.add_global_callback(Key::Tab, |s| {
//let bas_sta = s.find_id::<Panel<BoxView<TextView>>>("basic_status").unwrap();
//let mut root_stack = s.find_id::<StackView>("root_stack").unwrap();
//root_stack.add_layer(adv_sta);
s.call_on_id("root_stack", |sv: &mut StackView| {
/*if let FunctionalState::BasicStatus = cur_state {
return;
}*/
sv.move_to_front(LayerPosition::FromBack(0));
//sv.add_layer(advanced_status);
/*sv.pop_layer();
sv.add_layer(bas_sta);*/ });
});
//set theme
let mut theme = grin_ui.cursive.current_theme().clone();
theme.shadow = false;
theme.borders = BorderStyle::Simple;
theme.palette[Background] = Dark(Black);
theme.palette[Shadow] = Dark(Black);
theme.palette[View] = Dark(Black);
theme.palette[Primary] = Dark(White);
theme.palette[Highlight] = Dark(Cyan);
theme.palette[HighlightInactive] = Dark(Blue);
// also secondary, tertiary, TitlePrimary, TitleSecondary
grin_ui.cursive.set_theme(theme);
grin_ui.cursive.add_layer(top_layer);
// Configure a callback (shutdown, for the first test)
let controller_tx_clone = grin_ui.controller_tx.clone();
grin_ui.cursive.add_global_callback('q', move |_| {
controller_tx_clone
.send(ControllerMessage::Shutdown)
.unwrap();
});
grin_ui.cursive.set_fps(4);
grin_ui
}
/// Step the UI by calling into Cursive's step function, then
/// processing any UI messages
pub fn step(&mut self) -> bool {
if !self.cursive.is_running() {
return false;
}
// Process any pending UI messages
while let Some(message) = self.ui_rx.try_iter().next() {
match message {
UIMessage::UpdateStatus(update) => {
//find and update here as needed
self.cursive
.call_on_id("basic_current_status", |t: &mut TextView| {
t.set_content(update.basic_status.clone());
});
self.cursive
.call_on_id("connected_peers", |t: &mut TextView| {
t.set_content(update.peer_count.clone());
});
self.cursive.call_on_id("chain_height", |t: &mut TextView| {
t.set_content(update.chain_height.clone());
});
self.cursive
.call_on_id("basic_mining_config_status", |t: &mut TextView| {
t.set_content(update.basic_mining_config_status.clone());
});
self.cursive
.call_on_id("basic_mining_status", |t: &mut TextView| {
t.set_content(update.basic_mining_status.clone());
});
self.cursive
.call_on_id("basic_network_info", |t: &mut TextView| {
t.set_content(update.basic_network_info.clone());
});
}
}
}
// Step the UI
self.cursive.step();
true
}
/// Stop the UI
pub fn stop(&mut self) {
self.cursive.quit();
}
}
pub struct Controller {
rx: mpsc::Receiver<ControllerMessage>,
ui: UI,
}
pub enum ControllerMessage {
Shutdown,
}
impl Controller {
/// Create a new controller
pub fn new() -> Result<Controller, String> {
let (tx, rx) = mpsc::channel::<ControllerMessage>();
Ok(Controller {
rx: rx,
ui: UI::new(tx.clone()),
})
}
/// Run the controller
pub fn run(&mut self, server: Arc<Server>) {
let stat_update_interval = 1;
let mut next_stat_update = time::get_time().sec + stat_update_interval;
while self.ui.step() {
while let Some(message) = self.rx.try_iter().next() {
match message {
ControllerMessage::Shutdown => {
server.stop();
self.ui.stop();
/*self.ui
.ui_tx
.send(UIMessage::UpdateOutput("update".to_string()))
.unwrap();*/
}
}
}
if time::get_time().sec > next_stat_update {
self.update_status(server.clone());
next_stat_update = time::get_time().sec + stat_update_interval;
}
}
}
/// update the UI with server status at given intervals (should be
/// once a second at present
pub fn update_status(&mut self, server: Arc<Server>) {
let stats = server.get_server_stats().unwrap();
let basic_status = {
if stats.is_syncing {
if stats.awaiting_peers {
"Waiting for peers".to_string()
} else {
format!("Syncing - Latest header: {}", stats.header_head.height).to_string()
}
} else {
"Running".to_string()
}
};
let basic_mining_config_status = {
if stats.mining_stats.is_enabled {
"Configured as mining node"
} else {
"Configured as validating node only (not mining)"
}
};
let (basic_mining_status, basic_network_info) = {
if stats.mining_stats.is_enabled {
if stats.is_syncing {
(
"Mining Status: Paused while syncing".to_string(),
"".to_string(),
)
} else if stats.mining_stats.combined_gps == 0.0 {
(
"Mining Status: Starting miner and awaiting first solution...".to_string(),
"".to_string(),
)
} else {
(
format!(
"Mining Status: Mining at height {} at {:.*} GPS",
stats.mining_stats.block_height, 4, stats.mining_stats.combined_gps
),
format!(
"Cuckoo {} - Network Difficulty {}",
stats.mining_stats.cuckoo_size,
stats.mining_stats.network_difficulty.to_string()
),
)
}
} else {
("".to_string(), "".to_string())
}
};
let update = StatusUpdates {
basic_status: basic_status,
peer_count: stats.peer_count.to_string(),
chain_height: stats.head.height.to_string(),
basic_mining_config_status: basic_mining_config_status.to_string(),
basic_mining_status: basic_mining_status,
basic_network_info: basic_network_info,
};
self.ui.ui_tx.send(UIMessage::UpdateStatus(update)).unwrap();
}
}