From e268993f5e26ba4e0a42bed5f5378f297147844c Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Tue, 13 Mar 2018 18:10:45 +0000 Subject: [PATCH] [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 --- Cargo.toml | 1 + grin/src/lib.rs | 2 +- grin/src/miner.rs | 8 + grin/src/server.rs | 10 + grin/src/types.rs | 49 ++ p2p/src/lib.rs | 2 +- pow/src/lib.rs | 4 +- src/bin/grin.rs | 7 +- src/bin/tui/constants.rs | 57 +++ src/bin/tui/menu.rs | 68 +++ src/bin/tui/mining.rs | 178 +++++++ src/bin/tui/mod.rs | 25 + src/bin/tui/peers.rs | 105 +++++ src/bin/tui/status.rs | 139 ++++++ src/bin/tui/table.rs | 985 +++++++++++++++++++++++++++++++++++++++ src/bin/tui/types.rs | 35 ++ src/bin/tui/ui.rs | 183 ++++++++ src/bin/ui.rs | 394 ---------------- 18 files changed, 1852 insertions(+), 400 deletions(-) create mode 100644 src/bin/tui/constants.rs create mode 100644 src/bin/tui/menu.rs create mode 100644 src/bin/tui/mining.rs create mode 100644 src/bin/tui/mod.rs create mode 100644 src/bin/tui/peers.rs create mode 100644 src/bin/tui/status.rs create mode 100644 src/bin/tui/table.rs create mode 100644 src/bin/tui/types.rs create mode 100644 src/bin/tui/ui.rs delete mode 100644 src/bin/ui.rs diff --git a/Cargo.toml b/Cargo.toml index e0116bb1d..baa66c10e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ grin_core = { path = "./core" } grin_grin = { path = "./grin" } grin_keychain = { path = "./keychain" } grin_p2p = { path = "./p2p"} +grin_pow = { path = "./pow"} grin_util = { path = "./util"} grin_wallet = { path = "./wallet" } blake2-rfc = "~0.2.17" diff --git a/grin/src/lib.rs b/grin/src/lib.rs index 2b86f1e3c..522ed0da1 100644 --- a/grin/src/lib.rs +++ b/grin/src/lib.rs @@ -47,7 +47,7 @@ mod adapters; mod server; mod seed; mod sync; -mod types; +pub mod types; mod miner; pub use server::Server; diff --git a/grin/src/miner.rs b/grin/src/miner.rs index 85610a748..f1ecc6c1d 100644 --- a/grin/src/miner.rs +++ b/grin/src/miner.rs @@ -254,6 +254,11 @@ impl Miner { if sps_total.is_finite() { let mut mining_stats = mining_stats.write().unwrap(); 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; } @@ -359,6 +364,9 @@ impl Miner { if last_hashes_per_sec.is_finite() { let mut mining_stats = mining_stats.write().unwrap(); 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; diff --git a/grin/src/server.rs b/grin/src/server.rs index 44bcfb1ab..570fd9b08 100644 --- a/grin/src/server.rs +++ b/grin/src/server.rs @@ -263,6 +263,15 @@ impl Server { pub fn get_server_stats(&self) -> Result { let mining_stats = self.state_info.mining_stats.read().unwrap().clone(); 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 { peer_count: self.peer_count(), head: self.head(), @@ -270,6 +279,7 @@ impl Server { is_syncing: self.currently_syncing.load(Ordering::Relaxed), awaiting_peers: awaiting_peers, mining_stats: mining_stats, + peer_stats: peer_stats, }) } diff --git a/grin/src/types.rs b/grin/src/types.rs index 130f4d3a5..1cda706f0 100644 --- a/grin/src/types.rs +++ b/grin/src/types.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Server types + use std::convert::From; use std::sync::{Arc, RwLock}; use std::sync::atomic::AtomicBool; @@ -41,6 +43,7 @@ pub enum Error { API(api::Error), /// Error originating from wallet API. Wallet(wallet::Error), + /// Error originating from the cuckoo miner Cuckoo(pow::cuckoo::Error), } @@ -210,6 +213,8 @@ pub struct ServerStats { pub awaiting_peers: bool, /// Handle to current mining stats pub mining_stats: MiningStats, + /// Peer stats + pub peer_stats: Vec, } /// Struct to return relevant information about the mining process @@ -228,6 +233,49 @@ pub struct MiningStats { pub network_difficulty: u64, /// cuckoo size used for mining pub cuckoo_size: u16, + /// Individual device status from Cuckoo-Miner + pub device_stats: Option>>, +} + +/// 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 { @@ -239,6 +287,7 @@ impl Default for MiningStats { block_height: 0, network_difficulty: 0, cuckoo_size: 0, + device_stats: None, } } } diff --git a/p2p/src/lib.rs b/p2p/src/lib.rs index 0e2af039a..bb012a5a9 100644 --- a/p2p/src/lib.rs +++ b/p2p/src/lib.rs @@ -47,7 +47,7 @@ mod peers; mod protocol; mod serv; mod store; -mod types; +pub mod types; pub use serv::{DummyAdapter, Server}; pub use peers::Peers; diff --git a/pow/src/lib.rs b/pow/src/lib.rs index 645116636..68a1cde29 100644 --- a/pow/src/lib.rs +++ b/pow/src/lib.rs @@ -42,7 +42,9 @@ extern crate time; extern crate grin_core as core; 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; pub mod plugin; diff --git a/src/bin/grin.rs b/src/bin/grin.rs index bb86925b4..5af1ccdfe 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -34,7 +34,7 @@ extern crate grin_util as util; extern crate grin_wallet as wallet; mod client; -mod ui; +pub mod tui; use std::thread; use std::sync::Arc; @@ -49,6 +49,7 @@ use config::GlobalConfig; use core::global; use core::core::amount_to_hr_string; use util::{init_logger, LoggingConfig, LOGGER}; +use tui::ui; /// wrap below to allow UI to clean up on stop 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 { let mut wallet_config = global_config.members.unwrap().wallet; - let wallet_seed = - wallet::WalletSeed::from_file(&wallet_config).expect("Failed to read wallet seed file."); + let wallet_seed = wallet::WalletSeed::from_file(&wallet_config) + .expect("Failed to read wallet seed file."); let mut keychain = wallet_seed .derive_keychain("") .expect("Failed to derive keychain from seed file and passphrase."); diff --git a/src/bin/tui/constants.rs b/src/bin/tui/constants.rs new file mode 100644 index 000000000..6f904ac86 --- /dev/null +++ b/src/bin/tui/constants.rs @@ -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 "; diff --git a/src/bin/tui/menu.rs b/src/bin/tui/menu.rs new file mode 100644 index 000000000..de225b695 --- /dev/null +++ b/src/bin/tui/menu.rs @@ -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 { + 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) +} diff --git a/src/bin/tui/mining.rs b/src/bin/tui/mining.rs new file mode 100644 index 000000000..8ca431987 --- /dev/null +++ b/src/bin/tui/mining.rs @@ -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 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 { + let table_view = + TableView::::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| { + t.set_items(flattened_device_stats); + }, + ); + } +} diff --git a/src/bin/tui/mod.rs b/src/bin/tui/mod.rs new file mode 100644 index 000000000..4d5c8eb4e --- /dev/null +++ b/src/bin/tui/mod.rs @@ -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; diff --git a/src/bin/tui/peers.rs b/src/bin/tui/peers.rs new file mode 100644 index 000000000..359bddc17 --- /dev/null +++ b/src/bin/tui/peers.rs @@ -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 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 { + let table_view = + TableView::::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| { + t.set_items(stats.peer_stats.clone()); + }, + ); + } +} diff --git a/src/bin/tui/status.rs b/src/bin/tui/status.rs new file mode 100644 index 000000000..e1b66e35b --- /dev/null +++ b/src/bin/tui/status.rs @@ -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 { + 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); + }); + } +} diff --git a/src/bin/tui/table.rs b/src/bin/tui/table.rs new file mode 100644 index 000000000..b8de142f7 --- /dev/null +++ b/src/bin/tui/table.rs @@ -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: 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 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::::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, H: Eq + Hash + Copy + Clone + 'static> { + enabled: bool, + scrollbase: ScrollBase, + last_size: Vec2, + + column_select: bool, + columns: Vec>, + column_indicies: HashMap, + + focus: usize, + items: Vec, + rows_to_items: Vec, + + on_sort: Option>, + // TODO Pass drawing offsets into the handlers so a popup menu + // can be created easily? + on_submit: Option>, + on_select: Option>, +} + +impl, H: Eq + Hash + Copy + Clone + 'static> TableView { + /// 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, C: FnOnce(TableColumn) -> TableColumn>( + 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 ``. + /// + /// # Example + /// + /// ```norun + /// table.set_on_sort(|siv: &mut Cursive, column: BasicColumn, order: Ordering| { + /// + /// }); + /// ``` + pub fn set_on_sort(&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 ``. + /// + /// Chainable variant. + /// + /// # Example + /// + /// ```norun + /// table.on_sort(|siv: &mut Cursive, column: BasicColumn, order: Ordering| { + /// + /// }); + /// ``` + pub fn on_sort(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 `` 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(&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 `` 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(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(&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(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 { + 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) { + 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) -> 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 { + &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 { + &mut self.items + } + + /// Returns the index of the currently selected item within the underlying + /// storage vector. + pub fn item(&self) -> Option { + 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 { + 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 { + 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, H: Eq + Hash + Copy + Clone + 'static> TableView { + fn draw_columns)>( + &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 + 'static, H: Eq + Hash + Copy + Clone + 'static> View + for TableView { + 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>, Vec<&mut TableColumn>) = + 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 { + column: H, + title: String, + selected: bool, + alignment: HAlign, + order: Ordering, + width: usize, + default_order: Ordering, + requested_width: Option, +} + +enum TableColumnWidth { + Percent(usize), + Absolute(usize), +} + +impl TableColumn { + /// 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!( + "{: 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!("{: format!("{:>width$} ", value, width = self.width), + HAlign::Center => format!("{:^width$} ", value, width = self.width), + }; + printer.print((0, 0), value.as_str()); + } +} diff --git a/src/bin/tui/types.rs b/src/bin/tui/types.rs new file mode 100644 index 000000000..17d64a90f --- /dev/null +++ b/src/bin/tui/types.rs @@ -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; + /// Update according to status update contents + fn update(c: &mut Cursive, stats: &ServerStats); +} diff --git a/src/bin/tui/ui.rs b/src/bin/tui/ui.rs new file mode 100644 index 000000000..04e271059 --- /dev/null +++ b/src/bin/tui/ui.rs @@ -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, + ui_tx: mpsc::Sender, + controller_tx: mpsc::Sender, +} + +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) -> UI { + let (ui_tx, ui_rx) = mpsc::channel::(); + 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, + ui: UI, +} + +pub enum ControllerMessage { + Shutdown, +} + +impl Controller { + /// Create a new controller + pub fn new() -> Result { + let (tx, rx) = mpsc::channel::(); + Ok(Controller { + rx: rx, + ui: UI::new(tx.clone()), + }) + } + /// Run the controller + pub fn run(&mut self, server: Arc) { + 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; + } + } + } +} diff --git a/src/bin/ui.rs b/src/bin/ui.rs deleted file mode 100644 index e81caf872..000000000 --- a/src/bin/ui.rs +++ /dev/null @@ -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, - ui_tx: mpsc::Sender, - controller_tx: mpsc::Sender, -} - -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) -> UI { - let (ui_tx, ui_rx) = mpsc::channel::(); - 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( - " Toggle Basic / Advanced view", - ))) - .child(Panel::new(TextView::new(" 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::>>("basic_status").unwrap(); - //let mut root_stack = s.find_id::("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, - ui: UI, -} - -pub enum ControllerMessage { - Shutdown, -} - -impl Controller { - /// Create a new controller - pub fn new() -> Result { - let (tx, rx) = mpsc::channel::(); - Ok(Controller { - rx: rx, - ui: UI::new(tx.clone()), - }) - } - /// Run the controller - pub fn run(&mut self, server: Arc) { - 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) { - 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(); - } -}