mirror of
https://github.com/mimblewimble/grin.git
synced 2025-02-01 17:01:09 +03:00
[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:
parent
65c0a4b7b0
commit
e268993f5e
18 changed files with 1852 additions and 400 deletions
|
@ -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"
|
||||
|
|
|
@ -47,7 +47,7 @@ mod adapters;
|
|||
mod server;
|
||||
mod seed;
|
||||
mod sync;
|
||||
mod types;
|
||||
pub mod types;
|
||||
mod miner;
|
||||
|
||||
pub use server::Server;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -263,6 +263,15 @@ impl Server {
|
|||
pub fn get_server_stats(&self) -> Result<ServerStats, Error> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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<PeerStats>,
|
||||
}
|
||||
|
||||
/// 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<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 {
|
||||
|
@ -239,6 +287,7 @@ impl Default for MiningStats {
|
|||
block_height: 0,
|
||||
network_difficulty: 0,
|
||||
cuckoo_size: 0,
|
||||
device_stats: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.");
|
||||
|
|
57
src/bin/tui/constants.rs
Normal file
57
src/bin/tui/constants.rs
Normal 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
68
src/bin/tui/menu.rs
Normal 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
178
src/bin/tui/mining.rs
Normal 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
25
src/bin/tui/mod.rs
Normal 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
105
src/bin/tui/peers.rs
Normal 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
139
src/bin/tui/status.rs
Normal 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
985
src/bin/tui/table.rs
Normal 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
35
src/bin/tui/types.rs
Normal 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
183
src/bin/tui/ui.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
394
src/bin/ui.rs
394
src/bin/ui.rs
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue