From 89191e5877cb581e140baa417f49887692ffd979 Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Thu, 7 Dec 2023 09:12:43 +0200 Subject: [PATCH 1/5] Move configuration view into `frontend::configuration` module --- src/frontend.rs | 1 + src/frontend/configuration.rs | 420 +++++++++++++++++++++++++++++++++ src/main.rs | 422 +++------------------------------- 3 files changed, 447 insertions(+), 396 deletions(-) create mode 100644 src/frontend.rs create mode 100644 src/frontend/configuration.rs diff --git a/src/frontend.rs b/src/frontend.rs new file mode 100644 index 00000000..2d148af0 --- /dev/null +++ b/src/frontend.rs @@ -0,0 +1 @@ +pub mod configuration; diff --git a/src/frontend/configuration.rs b/src/frontend/configuration.rs new file mode 100644 index 00000000..260612c1 --- /dev/null +++ b/src/frontend/configuration.rs @@ -0,0 +1,420 @@ +use crate::backend::config::{Farm, RawConfig}; +use crate::{DiskFarm, MaybeValid, MIN_FARM_SIZE}; +use bytesize::ByteSize; +use gtk::prelude::*; +use relm4::component::{AsyncComponent, AsyncComponentParts}; +use relm4::prelude::*; +use relm4::AsyncComponentSender; +use relm4_components::open_dialog::{ + OpenDialog, OpenDialogMsg, OpenDialogResponse, OpenDialogSettings, +}; +use std::path::PathBuf; +use std::str::FromStr; +use subspace_farmer::utils::ss58::parse_ss58_reward_address; +use tracing::{debug, warn}; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum DirectoryKind { + NodePath, + FarmPath(usize), +} + +#[derive(Debug)] +pub enum ConfigurationInput { + RewardAddressChanged(String), + OpenDirectory(DirectoryKind), + DirectorySelected(PathBuf), + FarmSizeChanged { farm_index: usize, size: String }, + Start, + Ignore, +} + +#[derive(Debug)] +pub enum ConfigurationOutput { + StartWithNewConfig(RawConfig), +} + +#[derive(Debug)] +pub struct ConfigurationView { + reward_address: MaybeValid, + node_path: MaybeValid, + farms: Vec, + pending_directory_selection: Option, + open_dialog: Controller, +} + +#[relm4::component(async, pub)] +impl AsyncComponent for ConfigurationView { + type Init = (); + type Input = ConfigurationInput; + type Output = ConfigurationOutput; + type CommandOutput = (); + + view! { + #[root] + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + + gtk::ScrolledWindow { + set_vexpand: true, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + + gtk::ListBox { + gtk::ListBoxRow { + set_activatable: false, + set_margin_bottom: 10, + set_selectable: false, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 10, + + gtk::Label { + add_css_class: "heading", + set_halign: gtk::Align::Start, + set_label: "Rewards address", + }, + + gtk::Entry { + connect_activate[sender] => move |entry| { + sender.input(ConfigurationInput::RewardAddressChanged( + entry.text().into() + )); + }, + connect_changed[sender] => move |entry| { + sender.input(ConfigurationInput::RewardAddressChanged( + entry.text().into() + )); + }, + set_placeholder_text: Some( + "stB4S14whneyomiEa22Fu2PzVoibMB7n5PvBFUwafbCbRkC1K", + ), + #[watch] + set_secondary_icon_name: model.reward_address.icon(), + set_secondary_icon_activatable: false, + set_secondary_icon_sensitive: false, + #[track = "model.reward_address.unknown()"] + set_text: &model.reward_address, + set_tooltip_markup: Some( + "Use Subwallet or polkadot{.js} extension or any other \ + Substrate wallet to create it first (address for any Substrate \ + chain in SS58 format works)" + ), + }, + }, + }, + gtk::ListBoxRow { + set_activatable: false, + set_margin_bottom: 10, + set_selectable: false, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 10, + + gtk::Label { + add_css_class: "heading", + set_halign: gtk::Align::Start, + set_label: "Node path", + }, + + gtk::Box { + add_css_class: "linked", + + gtk::Entry { + set_can_focus: false, + set_editable: false, + set_hexpand: true, + set_placeholder_text: Some( + if cfg!(windows) { + "D:\\subspace-node" + } else { + "/media/subspace-node" + }, + ), + #[watch] + set_secondary_icon_name: model.node_path.icon(), + set_secondary_icon_activatable: false, + set_secondary_icon_sensitive: false, + #[watch] + set_text: model.node_path.display().to_string().as_str(), + set_tooltip_markup: Some( + "Absolute path where node files will be stored, prepare to \ + dedicate at least 100GiB of space for it, good quality SSD \ + recommended" + ), + }, + + gtk::Button { + connect_clicked => ConfigurationInput::OpenDirectory( + DirectoryKind::NodePath + ), + + gtk::Box { + set_spacing: 10, + + gtk::Image { + set_icon_name: Some("folder-new-symbolic"), + }, + + gtk::Label { + set_label: "Select", + }, + }, + }, + }, + }, + }, + // TODO: Support more than one farm + gtk::ListBoxRow { + set_activatable: false, + set_margin_bottom: 10, + set_selectable: false, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 10, + + gtk::Label { + add_css_class: "heading", + set_halign: gtk::Align::Start, + set_label: "Path to farm and its size", + }, + + gtk::Box { + set_spacing: 10, + + gtk::Box { + add_css_class: "linked", + + gtk::Entry { + set_can_focus: false, + set_editable: false, + set_hexpand: true, + set_placeholder_text: Some( + if cfg!(windows) { + "D:\\subspace-farm" + } else { + "/media/subspace-farm" + }, + ), + #[watch] + set_secondary_icon_name: model.farms.get(0).map(|farm| farm.path.icon()).unwrap_or_default(), + set_secondary_icon_activatable: false, + set_secondary_icon_sensitive: false, + #[watch] + set_text: model.farms.get(0).map(|farm| farm.path.display().to_string()).unwrap_or_default().as_str(), + set_tooltip_markup: Some( + "Absolute path where farm files will be stored, any \ + SSD works, high endurance not necessary" + ), + }, + + gtk::Button { + connect_clicked => ConfigurationInput::OpenDirectory( + DirectoryKind::FarmPath(0) + ), + + gtk::Box { + set_spacing: 10, + + gtk::Image { + set_icon_name: Some("folder-new-symbolic"), + }, + + gtk::Label { + set_label: "Select", + }, + }, + }, + }, + + gtk::Entry { + connect_activate[sender] => move |entry| { + sender.input(ConfigurationInput::FarmSizeChanged { + farm_index: 0, + size: entry.text().into() + }); + }, + connect_changed[sender] => move |entry| { + sender.input(ConfigurationInput::FarmSizeChanged { + farm_index: 0, + size: entry.text().into() + }); + }, + set_placeholder_text: Some( + "4T, 2.5TB, 500GiB, etc.", + ), + #[watch] + set_secondary_icon_name: model.farms.get(0).map(|farm| farm.size.icon()).unwrap_or_default(), + set_secondary_icon_activatable: false, + set_secondary_icon_sensitive: false, + #[track = "model.farms.get(0).map(|farm| farm.size.unknown()).unwrap_or_default()"] + set_text: model.farms.get(0).map(|farm| farm.size.as_str()).unwrap_or_default(), + set_tooltip_markup: Some( + "Size of the farm in whichever units you prefer, any \ + amount of space above 2 GB works" + ), + }, + }, + }, + }, + }, + + gtk::Box { + set_halign: gtk::Align::End, + + gtk::Button { + add_css_class: "suggested-action", + connect_clicked => ConfigurationInput::Start, + set_margin_top: 20, + #[watch] + set_sensitive: { + // TODO + model.reward_address.valid() + && model.node_path.valid() + && !model.farms.is_empty() + && model.farms.iter().all(|farm| { + farm.path.valid() && farm.size.valid() + }) + }, + + gtk::Label { + set_label: "Start", + set_margin_all: 10, + }, + }, + }, + }, + }, + } + } + + async fn init( + _init: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let open_dialog = OpenDialog::builder() + .transient_for_native(&root) + .launch(OpenDialogSettings { + folder_mode: true, + accept_label: "Select".to_string(), + ..OpenDialogSettings::default() + }) + .forward(sender.input_sender(), |response| match response { + OpenDialogResponse::Accept(path) => ConfigurationInput::DirectorySelected(path), + OpenDialogResponse::Cancel => ConfigurationInput::Ignore, + }); + + let model = Self { + reward_address: Default::default(), + node_path: Default::default(), + farms: Default::default(), + pending_directory_selection: Default::default(), + open_dialog, + }; + + let widgets = view_output!(); + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + input: Self::Input, + sender: AsyncComponentSender, + _root: &Self::Root, + ) { + self.process_input(input, sender).await; + } +} + +impl ConfigurationView { + async fn process_input( + &mut self, + input: ConfigurationInput, + sender: AsyncComponentSender, + ) { + match input { + ConfigurationInput::RewardAddressChanged(new_reward_address) => { + let new_reward_address = new_reward_address.trim(); + self.reward_address = if parse_ss58_reward_address(new_reward_address).is_ok() { + MaybeValid::Valid(new_reward_address.to_string()) + } else { + MaybeValid::Invalid(new_reward_address.to_string()) + }; + } + ConfigurationInput::OpenDirectory(directory_kind) => { + self.pending_directory_selection.replace(directory_kind); + self.open_dialog.emit(OpenDialogMsg::Open); + } + ConfigurationInput::DirectorySelected(path) => { + match self.pending_directory_selection.take() { + Some(DirectoryKind::NodePath) => { + self.node_path = MaybeValid::Valid(path); + } + Some(DirectoryKind::FarmPath(farm_index)) => { + if let Some(farm) = self.farms.get_mut(farm_index) { + farm.path = MaybeValid::Valid(path); + } else { + self.farms.push(DiskFarm { + path: MaybeValid::Valid(path), + size: Default::default(), + }) + } + } + None => { + warn!( + directory = %path.display(), + "Directory selected, but no pending selection found", + ); + } + } + } + ConfigurationInput::FarmSizeChanged { farm_index, size } => { + let size = if ByteSize::from_str(&size) + .map(|size| size.as_u64() >= MIN_FARM_SIZE) + .unwrap_or_default() + { + MaybeValid::Valid(size) + } else { + MaybeValid::Invalid(size) + }; + if let Some(farm) = self.farms.get_mut(farm_index) { + farm.size = size; + } else { + self.farms.push(DiskFarm { + path: MaybeValid::default(), + size, + }) + } + } + ConfigurationInput::Start => { + let config = RawConfig::V0 { + reward_address: String::clone(&self.reward_address), + node_path: PathBuf::clone(&self.node_path), + farms: self + .farms + .iter() + .map(|farm| Farm { + path: PathBuf::clone(&farm.path), + size: String::clone(&farm.size), + }) + .collect(), + }; + if sender + .output(ConfigurationOutput::StartWithNewConfig(config)) + .is_err() + { + debug!("Failed to send ConfigurationOutput::StartWithNewConfig"); + } + } + ConfigurationInput::Ignore => { + // Ignore + } + } + } +} diff --git a/src/main.rs b/src/main.rs index e6c450a2..d5bb878c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,26 +2,23 @@ #![feature(const_option, trait_alias, try_blocks)] mod backend; +mod frontend; -use crate::backend::config::{Farm, RawConfig}; use crate::backend::farmer::{PlottingKind, PlottingState}; use crate::backend::node::{SyncKind, SyncState}; use crate::backend::{ BackendAction, BackendNotification, FarmerNotification, LoadingStep, NodeNotification, }; -use bytesize::ByteSize; +use crate::frontend::configuration::{ConfigurationOutput, ConfigurationView}; use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use gtk::prelude::*; use relm4::prelude::*; use relm4::{set_global_css, RELM_THREADS}; -use relm4_components::open_dialog::{OpenDialog, *}; use std::ops::Deref; use std::path::PathBuf; -use std::str::FromStr; use std::thread::available_parallelism; use subspace_core_primitives::BlockNumber; -use subspace_farmer::utils::ss58::parse_ss58_reward_address; use subspace_proof_of_space::chia::ChiaTable; use tokio::runtime::Handle; use tracing::warn; @@ -84,27 +81,11 @@ impl MaybeValid { } } -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum DirectoryKind { - NodePath, - FarmPath(usize), -} - -#[derive(Debug)] -enum ConfigurationEvent { - RewardAddressChanged(String), - OpenDirectory(DirectoryKind), - DirectorySelected(PathBuf), - FarmSizeChanged { farm_index: usize, size: String }, - Start, -} - #[derive(Debug)] enum AppInput { BackendNotification(BackendNotification), - Configuration(ConfigurationEvent), + Configuration(ConfigurationOutput), ShowAboutDialog, - Ignore, } #[derive(Debug, Default)] @@ -127,12 +108,7 @@ struct FarmerState { enum View { Loading(String), - Configuration { - reward_address: MaybeValid, - node_path: MaybeValid, - farms: Vec, - pending_directory_selection: Option, - }, + Configuration, Running { node_state: NodeState, farmer_state: FarmerState, @@ -145,7 +121,7 @@ impl View { fn title(&self) -> &'static str { match self { Self::Loading(_) => "Loading", - Self::Configuration { .. } => "Configuration", + Self::Configuration => "Configuration", Self::Running { .. } => "Running", Self::Stopped(_) => "Stopped", Self::Error(_) => "Error", @@ -157,7 +133,7 @@ impl View { struct App { current_view: View, backend_action_sender: mpsc::Sender, - open_dialog: Controller, + configuration_view: AsyncController, menu_popover: gtk::Popover, about_dialog: gtk::AboutDialog, } @@ -226,258 +202,7 @@ impl AsyncComponent for App { set_label: what, }, }, - View::Configuration { reward_address, node_path, farms, .. } => gtk::Box { - set_orientation: gtk::Orientation::Vertical, - - gtk::ScrolledWindow { - set_vexpand: true, - - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - - gtk::ListBox { - gtk::ListBoxRow { - set_activatable: false, - set_margin_bottom: 10, - set_selectable: false, - - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_spacing: 10, - - gtk::Label { - add_css_class: "heading", - set_halign: gtk::Align::Start, - set_label: "Rewards address", - }, - - gtk::Entry { - connect_activate[sender] => move |entry| { - sender.input(AppInput::Configuration( - ConfigurationEvent::RewardAddressChanged(entry.text().into()) - )); - }, - connect_changed[sender] => move |entry| { - sender.input(AppInput::Configuration( - ConfigurationEvent::RewardAddressChanged(entry.text().into()) - )); - }, - set_placeholder_text: Some( - "stB4S14whneyomiEa22Fu2PzVoibMB7n5PvBFUwafbCbRkC1K", - ), - #[watch] - set_secondary_icon_name: reward_address.icon(), - set_secondary_icon_activatable: false, - set_secondary_icon_sensitive: false, - #[track = "reward_address.unknown()"] - set_text: reward_address, - set_tooltip_markup: Some( - "Use Subwallet or polkadot{.js} extension or any other \ - Substrate wallet to create it first (address for any Substrate \ - chain in SS58 format works)" - ), - }, - }, - }, - gtk::ListBoxRow { - set_activatable: false, - set_margin_bottom: 10, - set_selectable: false, - - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_spacing: 10, - - gtk::Label { - add_css_class: "heading", - set_halign: gtk::Align::Start, - set_label: "Node path", - }, - - gtk::Box { - add_css_class: "linked", - - gtk::Entry { - set_can_focus: false, - set_editable: false, - set_hexpand: true, - set_placeholder_text: Some( - if cfg!(windows) { - "D:\\subspace-node" - } else { - "/media/subspace-node" - }, - ), - #[watch] - set_secondary_icon_name: node_path.icon(), - set_secondary_icon_activatable: false, - set_secondary_icon_sensitive: false, - #[watch] - set_text: node_path.display().to_string().as_str(), - set_tooltip_markup: Some( - "Absolute path where node files will be stored, prepare to \ - dedicate at least 100GiB of space for it, good quality SSD \ - recommended" - ), - }, - - gtk::Button { - connect_clicked[sender] => move |_button| { - sender.input(AppInput::Configuration( - ConfigurationEvent::OpenDirectory(DirectoryKind::NodePath) - )); - }, - - gtk::Box { - set_spacing: 10, - - gtk::Image { - set_icon_name: Some("folder-new-symbolic"), - }, - - gtk::Label { - set_label: "Select", - }, - }, - }, - }, - }, - }, - // TODO: Support more than one farm - gtk::ListBoxRow { - set_activatable: false, - set_margin_bottom: 10, - set_selectable: false, - - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_spacing: 10, - - gtk::Label { - add_css_class: "heading", - set_halign: gtk::Align::Start, - set_label: "Path to farm and its size", - }, - - gtk::Box { - set_spacing: 10, - - gtk::Box { - add_css_class: "linked", - - gtk::Entry { - set_can_focus: false, - set_editable: false, - set_hexpand: true, - set_placeholder_text: Some( - if cfg!(windows) { - "D:\\subspace-farm" - } else { - "/media/subspace-farm" - }, - ), - #[watch] - set_secondary_icon_name: farms.get(0).map(|farm| farm.path.icon()).unwrap_or_default(), - set_secondary_icon_activatable: false, - set_secondary_icon_sensitive: false, - #[watch] - set_text: farms.get(0).map(|farm| farm.path.display().to_string()).unwrap_or_default().as_str(), - set_tooltip_markup: Some( - "Absolute path where farm files will be stored, any \ - SSD works, high endurance not necessary" - ), - }, - - gtk::Button { - connect_clicked[sender] => move |_button| { - sender.input(AppInput::Configuration( - ConfigurationEvent::OpenDirectory( - DirectoryKind::FarmPath(0) - ) - )); - }, - - gtk::Box { - set_spacing: 10, - - gtk::Image { - set_icon_name: Some("folder-new-symbolic"), - }, - - gtk::Label { - set_label: "Select", - }, - }, - }, - }, - - gtk::Entry { - connect_activate[sender] => move |entry| { - sender.input(AppInput::Configuration( - ConfigurationEvent::FarmSizeChanged { - farm_index: 0, - size: entry.text().into() - } - )); - }, - connect_changed[sender] => move |entry| { - sender.input(AppInput::Configuration( - ConfigurationEvent::FarmSizeChanged { - farm_index: 0, - size: entry.text().into() - } - )); - }, - set_placeholder_text: Some( - "4T, 2.5TB, 500GiB, etc.", - ), - #[watch] - set_secondary_icon_name: farms.get(0).map(|farm| farm.size.icon()).unwrap_or_default(), - set_secondary_icon_activatable: false, - set_secondary_icon_sensitive: false, - #[track = "farms.get(0).map(|farm| farm.size.unknown()).unwrap_or_default()"] - set_text: farms.get(0).map(|farm| farm.size.as_str()).unwrap_or_default(), - set_tooltip_markup: Some( - "Size of the farm in whichever units you prefer, any \ - amount of space above 2 GB works" - ), - }, - }, - }, - }, - }, - - gtk::Box { - set_halign: gtk::Align::End, - - gtk::Button { - add_css_class: "suggested-action", - connect_clicked[sender] => move |_button| { - sender.input(AppInput::Configuration( - ConfigurationEvent::Start - )); - }, - set_margin_top: 20, - #[watch] - set_sensitive: { - // TODO - reward_address.valid() - && node_path.valid() - && !farms.is_empty() - && farms.iter().all(|farm| { - farm.path.valid() && farm.size.valid() - }) - }, - - gtk::Label { - set_label: "Start", - set_margin_all: 10, - }, - }, - }, - }, - }, - }, + View::Configuration => model.configuration_view.widget().clone(), View::Running { node_state, farmer_state } => gtk::Box { set_orientation: gtk::Orientation::Vertical, @@ -664,19 +389,9 @@ impl AsyncComponent for App { } }); - let open_dialog = OpenDialog::builder() - .transient_for_native(&root) - .launch(OpenDialogSettings { - folder_mode: true, - accept_label: "Select".to_string(), - ..OpenDialogSettings::default() - }) - .forward(sender.input_sender(), |response| match response { - OpenDialogResponse::Accept(path) => { - AppInput::Configuration(ConfigurationEvent::DirectorySelected(path)) - } - OpenDialogResponse::Cancel => AppInput::Ignore, - }); + let configuration_view = ConfigurationView::builder() + .launch(()) + .forward(sender.input_sender(), AppInput::Configuration); let about_dialog = gtk::AboutDialog::builder() .title("About") @@ -701,7 +416,7 @@ impl AsyncComponent for App { let mut model = App { current_view: View::Loading(String::new()), backend_action_sender: action_sender, - open_dialog, + configuration_view, // Hack to initialize a field before this data structure is used menu_popover: gtk::Popover::default(), about_dialog, @@ -724,16 +439,14 @@ impl AsyncComponent for App { AppInput::BackendNotification(notification) => { self.process_backend_notification(notification); } - AppInput::Configuration(event) => { - self.process_configuration_event(event).await; + AppInput::Configuration(configuration_output) => { + self.process_configuration_output(configuration_output) + .await; } AppInput::ShowAboutDialog => { self.menu_popover.hide(); self.about_dialog.show(); } - AppInput::Ignore => { - // Ignore - } } } } @@ -778,22 +491,12 @@ impl App { } BackendNotification::NotConfigured => { // TODO: Welcome screen first - self.current_view = View::Configuration { - reward_address: MaybeValid::Unknown(String::new()), - node_path: MaybeValid::Unknown(PathBuf::new()), - farms: Vec::new(), - pending_directory_selection: None, - }; + self.current_view = View::Configuration; } BackendNotification::ConfigurationIsInvalid { .. } => { // TODO: Toast with configuration error, render old values with corresponding validity status once // notification has that information - self.current_view = View::Configuration { - reward_address: MaybeValid::Unknown(String::new()), - node_path: MaybeValid::Unknown(PathBuf::new()), - farms: Vec::new(), - pending_directory_selection: None, - }; + self.current_view = View::Configuration; } BackendNotification::Running { config, @@ -862,89 +565,16 @@ impl App { self.current_view = View::Loading(s.to_string()); } - async fn process_configuration_event(&mut self, event: ConfigurationEvent) { - if let View::Configuration { - reward_address, - node_path, - farms, - pending_directory_selection, - } = &mut self.current_view - { - match event { - ConfigurationEvent::RewardAddressChanged(new_reward_address) => { - let new_reward_address = new_reward_address.trim(); - *reward_address = if parse_ss58_reward_address(new_reward_address).is_ok() { - MaybeValid::Valid(new_reward_address.to_string()) - } else { - MaybeValid::Invalid(new_reward_address.to_string()) - }; - } - ConfigurationEvent::OpenDirectory(directory_kind) => { - pending_directory_selection.replace(directory_kind); - self.open_dialog.emit(OpenDialogMsg::Open); - } - ConfigurationEvent::DirectorySelected(path) => { - match pending_directory_selection.take() { - Some(DirectoryKind::NodePath) => { - *node_path = MaybeValid::Valid(path); - } - Some(DirectoryKind::FarmPath(farm_index)) => { - if let Some(farm) = farms.get_mut(farm_index) { - farm.path = MaybeValid::Valid(path); - } else { - farms.push(DiskFarm { - path: MaybeValid::Valid(path), - size: Default::default(), - }) - } - } - None => { - warn!( - directory = %path.display(), - "Directory selected, but no pending selection found", - ); - } - } - } - ConfigurationEvent::FarmSizeChanged { farm_index, size } => { - let size = if ByteSize::from_str(&size) - .map(|size| size.as_u64() >= MIN_FARM_SIZE) - .unwrap_or_default() - { - MaybeValid::Valid(size) - } else { - MaybeValid::Invalid(size) - }; - if let Some(farm) = farms.get_mut(farm_index) { - farm.size = size; - } else { - farms.push(DiskFarm { - path: MaybeValid::default(), - size, - }) - } - } - ConfigurationEvent::Start => { - let config = RawConfig::V0 { - reward_address: String::clone(reward_address), - node_path: PathBuf::clone(node_path), - farms: farms - .iter() - .map(|farm| Farm { - path: PathBuf::clone(&farm.path), - size: String::clone(&farm.size), - }) - .collect(), - }; - if let Err(error) = self - .backend_action_sender - .send(BackendAction::NewConfig { config }) - .await - { - self.current_view = View::Error(anyhow::anyhow!( - "Failed to send config to backend: {error}" - )); - } + async fn process_configuration_output(&mut self, configuration_output: ConfigurationOutput) { + match configuration_output { + ConfigurationOutput::StartWithNewConfig(config) => { + if let Err(error) = self + .backend_action_sender + .send(BackendAction::NewConfig { config }) + .await + { + self.current_view = + View::Error(anyhow::anyhow!("Failed to send config to backend: {error}")); } } } From 9dcf1eab5eec9ce53473361957991ac3dd3c7513 Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Thu, 7 Dec 2023 09:31:43 +0200 Subject: [PATCH 2/5] Move running view into `frontend::running` module --- src/frontend.rs | 1 + src/frontend/running.rs | 260 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 227 ++++------------------------------- 3 files changed, 281 insertions(+), 207 deletions(-) create mode 100644 src/frontend/running.rs diff --git a/src/frontend.rs b/src/frontend.rs index 2d148af0..20869bdf 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -1 +1,2 @@ pub mod configuration; +pub mod running; diff --git a/src/frontend/running.rs b/src/frontend/running.rs new file mode 100644 index 00000000..0ca2786c --- /dev/null +++ b/src/frontend/running.rs @@ -0,0 +1,260 @@ +use crate::backend::farmer::{PlottingKind, PlottingState}; +use crate::backend::node::{SyncKind, SyncState}; +use crate::backend::{FarmerNotification, NodeNotification}; +use gtk::prelude::*; +use relm4::component::{AsyncComponent, AsyncComponentParts}; +use relm4::prelude::*; +use relm4::AsyncComponentSender; +use subspace_core_primitives::BlockNumber; +use tracing::warn; + +#[derive(Debug)] +pub enum RunningInput { + Initialize { + best_block_number: BlockNumber, + num_farms: usize, + }, + NodeNotification(NodeNotification), + FarmerNotification(FarmerNotification), +} + +#[derive(Debug, Default)] +struct NodeState { + best_block_number: BlockNumber, + sync_state: Option, +} + +#[derive(Debug, Default)] +struct FarmerState { + /// One entry per farm + plotting_state: Vec>, +} + +#[derive(Debug)] +pub struct RunningView { + node_state: NodeState, + farmer_state: FarmerState, +} + +#[relm4::component(async, pub)] +impl AsyncComponent for RunningView { + type Init = (); + type Input = RunningInput; + type Output = (); + type CommandOutput = (); + + view! { + #[root] + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + + gtk::Box { + set_height_request: 100, + set_orientation: gtk::Orientation::Vertical, + set_spacing: 10, + + gtk::Label { + add_css_class: "heading", + set_halign: gtk::Align::Start, + set_label: "Consensus node", + }, + + // TODO: Match only because `if let Some(x) = y` is not yet supported here: https://github.com/Relm4/Relm4/issues/582 + #[transition = "SlideUpDown"] + match &model.node_state.sync_state { + Some(sync_state) => gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 10, + + gtk::Box { + gtk::Label { + set_halign: gtk::Align::Start, + + #[watch] + set_label: &{ + let kind = match sync_state.kind { + SyncKind::Dsn => "Syncing from DSN", + SyncKind::Regular => "Regular sync", + }; + + format!( + "{} #{}/{}{}", + kind, + model.node_state.best_block_number, + sync_state.target, + sync_state.speed + .map(|speed| format!(", {:.2} blocks/s", speed)) + .unwrap_or_default(), + ) + }, + }, + + gtk::Spinner { + set_margin_start: 5, + start: (), + }, + }, + + gtk::ProgressBar { + #[watch] + set_fraction: model.node_state.best_block_number as f64 / sync_state.target as f64, + }, + }, + None => gtk::Box { + gtk::Label { + #[watch] + set_label: &format!("Synced, best block #{}", model.node_state.best_block_number), + } + }, + }, + }, + + gtk::Separator { + set_margin_all: 10, + }, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 10, + set_valign: gtk::Align::Start, + + gtk::Label { + add_css_class: "heading", + set_halign: gtk::Align::Start, + set_label: "Farmer", + }, + + // TODO: Render all farms, not just the first one + // TODO: Match only because `if let Some(x) = y` is not yet supported here: https://github.com/Relm4/Relm4/issues/582 + #[transition = "SlideUpDown"] + match (&model.farmer_state.plotting_state[0], model.node_state.sync_state.is_none()) { + (Some(plotting_state), true) => gtk::Box { + set_orientation: gtk::Orientation::Vertical, + + gtk::Box { + set_spacing: 10, + + gtk::Label { + set_halign: gtk::Align::Start, + + #[watch] + set_label: &{ + let kind = match plotting_state.kind { + PlottingKind::Initial => "Initial plotting, not farming", + PlottingKind::Replotting => "Replotting, farming", + }; + + format!( + "{} {:.2}%{}", + kind, + plotting_state.progress, + plotting_state.speed + .map(|speed| format!(", {:.2} sectors/h", 3600.0 / speed)) + .unwrap_or_default(), + ) + }, + set_tooltip: "Farming starts after initial plotting is complete", + }, + + gtk::Spinner { + set_margin_start: 5, + start: (), + }, + }, + + gtk::ProgressBar { + #[watch] + set_fraction: plotting_state.progress as f64 / 100.0, + }, + }, + (None, true) => gtk::Box { + gtk::Label { + #[watch] + set_label: "Farming", + } + }, + _ => gtk::Box { + gtk::Label { + #[watch] + set_label: "Waiting for node to sync first", + } + }, + }, + }, + } + } + + async fn init( + _init: Self::Init, + _root: Self::Root, + _sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let model = Self { + node_state: NodeState::default(), + farmer_state: FarmerState::default(), + }; + + let widgets = view_output!(); + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + input: Self::Input, + _sender: AsyncComponentSender, + _root: &Self::Root, + ) { + self.process_input(input).await; + } +} + +impl RunningView { + async fn process_input(&mut self, input: RunningInput) { + match input { + RunningInput::Initialize { + best_block_number, + num_farms, + } => { + self.node_state = NodeState { + best_block_number, + sync_state: None, + }; + self.farmer_state = FarmerState { + plotting_state: vec![None; num_farms], + }; + } + RunningInput::NodeNotification(node_notification) => match node_notification { + NodeNotification::Syncing(sync_state) => { + self.node_state.sync_state = Some(sync_state); + } + NodeNotification::Synced => { + self.node_state.sync_state = None; + } + NodeNotification::BlockImported { number } => { + self.node_state.best_block_number = number; + } + }, + RunningInput::FarmerNotification(farmer_notification) => match farmer_notification { + FarmerNotification::Plotting { farm_index, state } => { + if let Some(plotting_state) = + self.farmer_state.plotting_state.get_mut(farm_index) + { + plotting_state.replace(state); + } else { + warn!(%farm_index, "Unexpected plotting farm index"); + } + } + FarmerNotification::Plotted { farm_index } => { + if let Some(plotting_state) = + self.farmer_state.plotting_state.get_mut(farm_index) + { + plotting_state.take(); + } else { + warn!(%farm_index, "Unexpected plotted farm index"); + } + } + }, + } + } +} diff --git a/src/main.rs b/src/main.rs index d5bb878c..3a712036 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,9 @@ mod backend; mod frontend; -use crate::backend::farmer::{PlottingKind, PlottingState}; -use crate::backend::node::{SyncKind, SyncState}; -use crate::backend::{ - BackendAction, BackendNotification, FarmerNotification, LoadingStep, NodeNotification, -}; +use crate::backend::{BackendAction, BackendNotification, LoadingStep}; use crate::frontend::configuration::{ConfigurationOutput, ConfigurationView}; +use crate::frontend::running::{RunningInput, RunningView}; use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use gtk::prelude::*; @@ -18,10 +15,8 @@ use relm4::{set_global_css, RELM_THREADS}; use std::ops::Deref; use std::path::PathBuf; use std::thread::available_parallelism; -use subspace_core_primitives::BlockNumber; use subspace_proof_of_space::chia::ChiaTable; use tokio::runtime::Handle; -use tracing::warn; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::prelude::*; use tracing_subscriber::EnvFilter; @@ -94,25 +89,10 @@ struct DiskFarm { size: MaybeValid, } -#[derive(Debug, Default)] -struct NodeState { - best_block_number: BlockNumber, - sync_state: Option, -} - -#[derive(Debug, Default)] -struct FarmerState { - /// One entry per farm - plotting_state: Vec>, -} - enum View { Loading(String), Configuration, - Running { - node_state: NodeState, - farmer_state: FarmerState, - }, + Running, Stopped(Option), Error(anyhow::Error), } @@ -122,7 +102,7 @@ impl View { match self { Self::Loading(_) => "Loading", Self::Configuration => "Configuration", - Self::Running { .. } => "Running", + Self::Running => "Running", Self::Stopped(_) => "Stopped", Self::Error(_) => "Error", } @@ -134,6 +114,7 @@ struct App { current_view: View, backend_action_sender: mpsc::Sender, configuration_view: AsyncController, + running_view: AsyncController, menu_popover: gtk::Popover, about_dialog: gtk::AboutDialog, } @@ -203,143 +184,7 @@ impl AsyncComponent for App { }, }, View::Configuration => model.configuration_view.widget().clone(), - View::Running { node_state, farmer_state } => gtk::Box { - set_orientation: gtk::Orientation::Vertical, - - gtk::Box { - set_height_request: 100, - set_orientation: gtk::Orientation::Vertical, - set_spacing: 10, - - gtk::Label { - add_css_class: "heading", - set_halign: gtk::Align::Start, - set_label: "Consensus node", - }, - - // TODO: Match only because `if let Some(x) = y` is not yet supported here: https://github.com/Relm4/Relm4/issues/582 - #[transition = "SlideUpDown"] - match &node_state.sync_state { - Some(sync_state) => gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_spacing: 10, - - gtk::Box { - gtk::Label { - set_halign: gtk::Align::Start, - - #[watch] - set_label: &{ - let kind = match sync_state.kind { - SyncKind::Dsn => "Syncing from DSN", - SyncKind::Regular => "Regular sync", - }; - - format!( - "{} #{}/{}{}", - kind, - node_state.best_block_number, - sync_state.target, - sync_state.speed - .map(|speed| format!(", {:.2} blocks/s", speed)) - .unwrap_or_default(), - ) - }, - }, - - gtk::Spinner { - set_margin_start: 5, - start: (), - }, - }, - - gtk::ProgressBar { - #[watch] - set_fraction: node_state.best_block_number as f64 / sync_state.target as f64, - }, - }, - None => gtk::Box { - gtk::Label { - #[watch] - set_label: &format!("Synced, best block #{}", node_state.best_block_number), - } - }, - }, - }, - - gtk::Separator { - set_margin_all: 10, - }, - - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_spacing: 10, - set_valign: gtk::Align::Start, - - gtk::Label { - add_css_class: "heading", - set_halign: gtk::Align::Start, - set_label: "Farmer", - }, - - // TODO: Render all farms, not just the first one - // TODO: Match only because `if let Some(x) = y` is not yet supported here: https://github.com/Relm4/Relm4/issues/582 - #[transition = "SlideUpDown"] - match (&farmer_state.plotting_state[0], node_state.sync_state.is_none()) { - (Some(plotting_state), true) => gtk::Box { - set_orientation: gtk::Orientation::Vertical, - - gtk::Box { - set_spacing: 10, - - gtk::Label { - set_halign: gtk::Align::Start, - - #[watch] - set_label: &{ - let kind = match plotting_state.kind { - PlottingKind::Initial => "Initial plotting, not farming", - PlottingKind::Replotting => "Replotting, farming", - }; - - format!( - "{} {:.2}%{}", - kind, - plotting_state.progress, - plotting_state.speed - .map(|speed| format!(", {:.2} sectors/h", 3600.0 / speed)) - .unwrap_or_default(), - ) - }, - set_tooltip: "Farming starts after initial plotting is complete", - }, - - gtk::Spinner { - set_margin_start: 5, - start: (), - }, - }, - - gtk::ProgressBar { - #[watch] - set_fraction: plotting_state.progress as f64 / 100.0, - }, - }, - (None, true) => gtk::Box { - gtk::Label { - #[watch] - set_label: "Farming", - } - }, - _ => gtk::Box { - gtk::Label { - #[watch] - set_label: "Waiting for node to sync first", - } - }, - }, - }, - } + View::Running=> model.running_view.widget().clone(), View::Stopped(Some(error)) => { // TODO: Better error handling gtk::Label { @@ -366,7 +211,7 @@ impl AsyncComponent for App { } async fn init( - _init: (), + _init: Self::Init, root: Self::Root, sender: AsyncComponentSender, ) -> AsyncComponentParts { @@ -393,6 +238,8 @@ impl AsyncComponent for App { .launch(()) .forward(sender.input_sender(), AppInput::Configuration); + let running_view = RunningView::builder().launch(()).detach(); + let about_dialog = gtk::AboutDialog::builder() .title("About") .program_name("Space Acres") @@ -413,10 +260,11 @@ impl AsyncComponent for App { gtk::glib::Propagation::Stop }); - let mut model = App { + let mut model = Self { current_view: View::Loading(String::new()), backend_action_sender: action_sender, configuration_view, + running_view, // Hack to initialize a field before this data structure is used menu_popover: gtk::Popover::default(), about_dialog, @@ -503,54 +351,19 @@ impl App { best_block_number, } => { let num_farms = config.farms.len(); - self.current_view = View::Running { - node_state: NodeState { - best_block_number, - sync_state: None, - }, - farmer_state: FarmerState { - plotting_state: vec![None; num_farms], - }, - }; + self.current_view = View::Running; + self.running_view.emit(RunningInput::Initialize { + best_block_number, + num_farms, + }); } BackendNotification::Node(node_notification) => { - if let View::Running { node_state, .. } = &mut self.current_view { - match node_notification { - NodeNotification::Syncing(sync_state) => { - node_state.sync_state = Some(sync_state); - } - NodeNotification::Synced => { - node_state.sync_state = None; - } - NodeNotification::BlockImported { number } => { - node_state.best_block_number = number; - } - } - } + self.running_view + .emit(RunningInput::NodeNotification(node_notification)); } BackendNotification::Farmer(farmer_notification) => { - if let View::Running { farmer_state, .. } = &mut self.current_view { - match farmer_notification { - FarmerNotification::Plotting { farm_index, state } => { - if let Some(plotting_state) = - farmer_state.plotting_state.get_mut(farm_index) - { - plotting_state.replace(state); - } else { - warn!(%farm_index, "Unexpected plotting farm index"); - } - } - FarmerNotification::Plotted { farm_index } => { - if let Some(plotting_state) = - farmer_state.plotting_state.get_mut(farm_index) - { - plotting_state.take(); - } else { - warn!(%farm_index, "Unexpected plotted farm index"); - } - } - } - } + self.running_view + .emit(RunningInput::FarmerNotification(farmer_notification)); } BackendNotification::Stopped { error } => { self.current_view = View::Stopped(error); From 6d1a349d85fb5ebfd2afdc6e4d713b8b9c989f9f Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Thu, 7 Dec 2023 09:34:48 +0200 Subject: [PATCH 3/5] Move configuration-only data structures into `frontend::configuration` module --- src/frontend/configuration.rs | 56 ++++++++++++++++++++++++++++++++++- src/main.rs | 55 ---------------------------------- 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/src/frontend/configuration.rs b/src/frontend/configuration.rs index 260612c1..2a208ae2 100644 --- a/src/frontend/configuration.rs +++ b/src/frontend/configuration.rs @@ -1,5 +1,4 @@ use crate::backend::config::{Farm, RawConfig}; -use crate::{DiskFarm, MaybeValid, MIN_FARM_SIZE}; use bytesize::ByteSize; use gtk::prelude::*; use relm4::component::{AsyncComponent, AsyncComponentParts}; @@ -8,11 +7,15 @@ use relm4::AsyncComponentSender; use relm4_components::open_dialog::{ OpenDialog, OpenDialogMsg, OpenDialogResponse, OpenDialogSettings, }; +use std::ops::Deref; use std::path::PathBuf; use std::str::FromStr; use subspace_farmer::utils::ss58::parse_ss58_reward_address; use tracing::{debug, warn}; +// 2 GB +const MIN_FARM_SIZE: u64 = 1000 * 1000 * 1000 * 2; + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum DirectoryKind { NodePath, @@ -34,6 +37,57 @@ pub enum ConfigurationOutput { StartWithNewConfig(RawConfig), } +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum MaybeValid { + Unknown(T), + Valid(T), + Invalid(T), +} + +impl Default for MaybeValid +where + T: Default, +{ + fn default() -> Self { + Self::Unknown(T::default()) + } +} + +impl Deref for MaybeValid { + type Target = T; + + fn deref(&self) -> &Self::Target { + let (MaybeValid::Unknown(inner) | MaybeValid::Valid(inner) | MaybeValid::Invalid(inner)) = + self; + + inner + } +} + +impl MaybeValid { + fn unknown(&self) -> bool { + matches!(self, MaybeValid::Unknown(_)) + } + + fn valid(&self) -> bool { + matches!(self, MaybeValid::Valid(_)) + } + + fn icon(&self) -> Option<&'static str> { + match self { + MaybeValid::Unknown(_) => None, + MaybeValid::Valid(_) => Some("emblem-ok-symbolic"), + MaybeValid::Invalid(_) => Some("window-close-symbolic"), + } + } +} + +#[derive(Debug, Default)] +struct DiskFarm { + path: MaybeValid, + size: MaybeValid, +} + #[derive(Debug)] pub struct ConfigurationView { reward_address: MaybeValid, diff --git a/src/main.rs b/src/main.rs index 3a712036..778fb35e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,6 @@ use futures::{SinkExt, StreamExt}; use gtk::prelude::*; use relm4::prelude::*; use relm4::{set_global_css, RELM_THREADS}; -use std::ops::Deref; -use std::path::PathBuf; use std::thread::available_parallelism; use subspace_proof_of_space::chia::ChiaTable; use tokio::runtime::Handle; @@ -26,56 +24,9 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; const GLOBAL_CSS: &str = include_str!("../res/app.css"); const ABOUT_IMAGE: &[u8] = include_bytes!("../res/about.png"); -// 2 GB -const MIN_FARM_SIZE: u64 = 1000 * 1000 * 1000 * 2; type PosTable = ChiaTable; -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum MaybeValid { - Unknown(T), - Valid(T), - Invalid(T), -} - -impl Default for MaybeValid -where - T: Default, -{ - fn default() -> Self { - Self::Unknown(T::default()) - } -} - -impl Deref for MaybeValid { - type Target = T; - - fn deref(&self) -> &Self::Target { - let (MaybeValid::Unknown(inner) | MaybeValid::Valid(inner) | MaybeValid::Invalid(inner)) = - self; - - inner - } -} - -impl MaybeValid { - fn unknown(&self) -> bool { - matches!(self, MaybeValid::Unknown(_)) - } - - fn valid(&self) -> bool { - matches!(self, MaybeValid::Valid(_)) - } - - fn icon(&self) -> Option<&'static str> { - match self { - MaybeValid::Unknown(_) => None, - MaybeValid::Valid(_) => Some("emblem-ok-symbolic"), - MaybeValid::Invalid(_) => Some("window-close-symbolic"), - } - } -} - #[derive(Debug)] enum AppInput { BackendNotification(BackendNotification), @@ -83,12 +34,6 @@ enum AppInput { ShowAboutDialog, } -#[derive(Debug, Default)] -struct DiskFarm { - path: MaybeValid, - size: MaybeValid, -} - enum View { Loading(String), Configuration, From bb04cd7e036b049b7cd239f41c1911023218990f Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Thu, 7 Dec 2023 09:47:23 +0200 Subject: [PATCH 4/5] Move loading view into `frontend::loading` module --- src/frontend.rs | 1 + src/frontend/loading.rs | 102 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 72 ++++++---------------------- 3 files changed, 118 insertions(+), 57 deletions(-) create mode 100644 src/frontend/loading.rs diff --git a/src/frontend.rs b/src/frontend.rs index 20869bdf..3814d365 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -1,2 +1,3 @@ pub mod configuration; +pub mod loading; pub mod running; diff --git a/src/frontend/loading.rs b/src/frontend/loading.rs new file mode 100644 index 00000000..768fd107 --- /dev/null +++ b/src/frontend/loading.rs @@ -0,0 +1,102 @@ +use crate::backend::LoadingStep; +use gtk::prelude::*; +use relm4::prelude::*; + +#[derive(Debug)] +pub enum LoadingInput { + BackendLoading(LoadingStep), +} + +#[derive(Debug)] +pub struct LoadingView { + message: String, +} + +#[relm4::component(pub)] +impl Component for LoadingView { + type Init = (); + type Input = LoadingInput; + type Output = (); + type CommandOutput = (); + + view! { + #[root] + gtk::Box { + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + set_vexpand: true, + set_orientation: gtk::Orientation::Vertical, + + gtk::Spinner { + start: (), + set_size_request: (50, 50), + }, + + gtk::Label { + #[watch] + set_label: &model.message, + }, + } + } + + fn init( + _init: Self::Init, + _root: &Self::Root, + _sender: ComponentSender, + ) -> ComponentParts { + let model = Self { + message: String::new(), + }; + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } + + fn update(&mut self, input: Self::Input, _sender: ComponentSender, _root: &Self::Root) { + self.process_input(input); + } +} + +impl LoadingView { + fn process_input(&mut self, input: LoadingInput) { + match input { + LoadingInput::BackendLoading(step) => { + let message = match step { + LoadingStep::LoadingConfiguration => "Loading configuration...", + LoadingStep::ReadingConfiguration => "Reading configuration...", + LoadingStep::ConfigurationReadSuccessfully { .. } => { + "Configuration read successfully" + } + LoadingStep::CheckingConfiguration => "Checking configuration...", + LoadingStep::ConfigurationIsValid => "Configuration is valid", + LoadingStep::DecodingChainSpecification => "Decoding chain specification...", + LoadingStep::DecodedChainSpecificationSuccessfully => { + "Decoded chain specification successfully" + } + LoadingStep::CheckingNodePath => "Checking node path...", + LoadingStep::CreatingNodePath => "Creating node path...", + LoadingStep::NodePathReady => "Node path ready", + LoadingStep::PreparingNetworkingStack => "Preparing networking stack...", + LoadingStep::ReadingNetworkKeypair => "Reading network keypair...", + LoadingStep::GeneratingNetworkKeypair => "Generating network keypair...", + LoadingStep::WritingNetworkKeypair => "Writing network keypair to disk...", + LoadingStep::InstantiatingNetworkingStack => { + "Instantiating networking stack..." + } + LoadingStep::NetworkingStackCreatedSuccessfully => { + "Networking stack created successfully" + } + LoadingStep::CreatingConsensusNode => "Creating consensus node...", + LoadingStep::ConsensusNodeCreatedSuccessfully => { + "Consensus node created successfully" + } + LoadingStep::CreatingFarmer => "Creating farmer...", + LoadingStep::FarmerCreatedSuccessfully => "Farmer created successfully", + }; + + self.message = message.to_string(); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 778fb35e..15894ecb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,9 @@ mod backend; mod frontend; -use crate::backend::{BackendAction, BackendNotification, LoadingStep}; +use crate::backend::{BackendAction, BackendNotification}; use crate::frontend::configuration::{ConfigurationOutput, ConfigurationView}; +use crate::frontend::loading::{LoadingInput, LoadingView}; use crate::frontend::running::{RunningInput, RunningView}; use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; @@ -35,7 +36,7 @@ enum AppInput { } enum View { - Loading(String), + Loading, Configuration, Running, Stopped(Option), @@ -45,7 +46,7 @@ enum View { impl View { fn title(&self) -> &'static str { match self { - Self::Loading(_) => "Loading", + Self::Loading => "Loading", Self::Configuration => "Configuration", Self::Running => "Running", Self::Stopped(_) => "Stopped", @@ -58,6 +59,7 @@ impl View { struct App { current_view: View, backend_action_sender: mpsc::Sender, + loading_view: Controller, configuration_view: AsyncController, running_view: AsyncController, menu_popover: gtk::Popover, @@ -112,22 +114,7 @@ impl AsyncComponent for App { #[transition = "SlideLeftRight"] match &model.current_view { - View::Loading(what) => gtk::Box { - set_halign: gtk::Align::Center, - set_valign: gtk::Align::Center, - set_vexpand: true, - set_orientation: gtk::Orientation::Vertical, - - gtk::Spinner { - start: (), - set_size_request: (50, 50), - }, - - gtk::Label { - #[watch] - set_label: what, - }, - }, + View::Loading => model.loading_view.widget().clone(), View::Configuration => model.configuration_view.widget().clone(), View::Running=> model.running_view.widget().clone(), View::Stopped(Some(error)) => { @@ -165,7 +152,9 @@ impl AsyncComponent for App { // Create backend in dedicated thread tokio::task::spawn_blocking(move || { - Handle::current().block_on(backend::create(action_receiver, notification_sender)); + if true { + Handle::current().block_on(backend::create(action_receiver, notification_sender)); + } }); // Forward backend notifications as application inputs @@ -179,6 +168,8 @@ impl AsyncComponent for App { } }); + let loading_view = LoadingView::builder().launch(()).detach(); + let configuration_view = ConfigurationView::builder() .launch(()) .forward(sender.input_sender(), AppInput::Configuration); @@ -206,8 +197,9 @@ impl AsyncComponent for App { }); let mut model = Self { - current_view: View::Loading(String::new()), + current_view: View::Loading, backend_action_sender: action_sender, + loading_view, configuration_view, running_view, // Hack to initialize a field before this data structure is used @@ -249,38 +241,8 @@ impl App { match notification { // TODO: Render progress BackendNotification::Loading { step, progress: _ } => { - self.set_loading_string(match step { - LoadingStep::LoadingConfiguration => "Loading configuration...", - LoadingStep::ReadingConfiguration => "Reading configuration...", - LoadingStep::ConfigurationReadSuccessfully { .. } => { - "Configuration read successfully" - } - LoadingStep::CheckingConfiguration => "Checking configuration...", - LoadingStep::ConfigurationIsValid => "Configuration is valid", - LoadingStep::DecodingChainSpecification => "Decoding chain specification...", - LoadingStep::DecodedChainSpecificationSuccessfully => { - "Decoded chain specification successfully" - } - LoadingStep::CheckingNodePath => "Checking node path...", - LoadingStep::CreatingNodePath => "Creating node path...", - LoadingStep::NodePathReady => "Node path ready", - LoadingStep::PreparingNetworkingStack => "Preparing networking stack...", - LoadingStep::ReadingNetworkKeypair => "Reading network keypair...", - LoadingStep::GeneratingNetworkKeypair => "Generating network keypair...", - LoadingStep::WritingNetworkKeypair => "Writing network keypair to disk...", - LoadingStep::InstantiatingNetworkingStack => { - "Instantiating networking stack..." - } - LoadingStep::NetworkingStackCreatedSuccessfully => { - "Networking stack created successfully" - } - LoadingStep::CreatingConsensusNode => "Creating consensus node...", - LoadingStep::ConsensusNodeCreatedSuccessfully => { - "Consensus node created successfully" - } - LoadingStep::CreatingFarmer => "Creating farmer...", - LoadingStep::FarmerCreatedSuccessfully => "Farmer created successfully", - }); + self.current_view = View::Loading; + self.loading_view.emit(LoadingInput::BackendLoading(step)); } BackendNotification::NotConfigured => { // TODO: Welcome screen first @@ -319,10 +281,6 @@ impl App { } } - fn set_loading_string(&mut self, s: &'static str) { - self.current_view = View::Loading(s.to_string()); - } - async fn process_configuration_output(&mut self, configuration_output: ConfigurationOutput) { match configuration_output { ConfigurationOutput::StartWithNewConfig(config) => { From 0e8d13a3645aaf70060e0ebd41805626a549899e Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Thu, 7 Dec 2023 09:50:11 +0200 Subject: [PATCH 5/5] Clean up unnecessary imports and make unnecessarily async components synchronous --- src/frontend/configuration.rs | 39 ++++++++++++----------------------- src/frontend/running.rs | 27 +++++++++--------------- src/main.rs | 4 ++-- 3 files changed, 25 insertions(+), 45 deletions(-) diff --git a/src/frontend/configuration.rs b/src/frontend/configuration.rs index 2a208ae2..1c37151f 100644 --- a/src/frontend/configuration.rs +++ b/src/frontend/configuration.rs @@ -1,9 +1,7 @@ use crate::backend::config::{Farm, RawConfig}; use bytesize::ByteSize; use gtk::prelude::*; -use relm4::component::{AsyncComponent, AsyncComponentParts}; use relm4::prelude::*; -use relm4::AsyncComponentSender; use relm4_components::open_dialog::{ OpenDialog, OpenDialogMsg, OpenDialogResponse, OpenDialogSettings, }; @@ -97,8 +95,8 @@ pub struct ConfigurationView { open_dialog: Controller, } -#[relm4::component(async, pub)] -impl AsyncComponent for ConfigurationView { +#[relm4::component(pub)] +impl Component for ConfigurationView { type Init = (); type Input = ConfigurationInput; type Output = ConfigurationOutput; @@ -325,15 +323,13 @@ impl AsyncComponent for ConfigurationView { connect_clicked => ConfigurationInput::Start, set_margin_top: 20, #[watch] - set_sensitive: { - // TODO + set_sensitive: model.reward_address.valid() && model.node_path.valid() && !model.farms.is_empty() && model.farms.iter().all(|farm| { farm.path.valid() && farm.size.valid() - }) - }, + }), gtk::Label { set_label: "Start", @@ -346,13 +342,13 @@ impl AsyncComponent for ConfigurationView { } } - async fn init( + fn init( _init: Self::Init, - root: Self::Root, - sender: AsyncComponentSender, - ) -> AsyncComponentParts { + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { let open_dialog = OpenDialog::builder() - .transient_for_native(&root) + .transient_for_native(root) .launch(OpenDialogSettings { folder_mode: true, accept_label: "Select".to_string(), @@ -373,25 +369,16 @@ impl AsyncComponent for ConfigurationView { let widgets = view_output!(); - AsyncComponentParts { model, widgets } + ComponentParts { model, widgets } } - async fn update( - &mut self, - input: Self::Input, - sender: AsyncComponentSender, - _root: &Self::Root, - ) { - self.process_input(input, sender).await; + fn update(&mut self, input: Self::Input, sender: ComponentSender, _root: &Self::Root) { + self.process_input(input, sender); } } impl ConfigurationView { - async fn process_input( - &mut self, - input: ConfigurationInput, - sender: AsyncComponentSender, - ) { + fn process_input(&mut self, input: ConfigurationInput, sender: ComponentSender) { match input { ConfigurationInput::RewardAddressChanged(new_reward_address) => { let new_reward_address = new_reward_address.trim(); diff --git a/src/frontend/running.rs b/src/frontend/running.rs index 0ca2786c..639ee024 100644 --- a/src/frontend/running.rs +++ b/src/frontend/running.rs @@ -2,9 +2,7 @@ use crate::backend::farmer::{PlottingKind, PlottingState}; use crate::backend::node::{SyncKind, SyncState}; use crate::backend::{FarmerNotification, NodeNotification}; use gtk::prelude::*; -use relm4::component::{AsyncComponent, AsyncComponentParts}; use relm4::prelude::*; -use relm4::AsyncComponentSender; use subspace_core_primitives::BlockNumber; use tracing::warn; @@ -36,8 +34,8 @@ pub struct RunningView { farmer_state: FarmerState, } -#[relm4::component(async, pub)] -impl AsyncComponent for RunningView { +#[relm4::component(pub)] +impl Component for RunningView { type Init = (); type Input = RunningInput; type Output = (); @@ -184,11 +182,11 @@ impl AsyncComponent for RunningView { } } - async fn init( + fn init( _init: Self::Init, - _root: Self::Root, - _sender: AsyncComponentSender, - ) -> AsyncComponentParts { + _root: &Self::Root, + _sender: ComponentSender, + ) -> ComponentParts { let model = Self { node_state: NodeState::default(), farmer_state: FarmerState::default(), @@ -196,21 +194,16 @@ impl AsyncComponent for RunningView { let widgets = view_output!(); - AsyncComponentParts { model, widgets } + ComponentParts { model, widgets } } - async fn update( - &mut self, - input: Self::Input, - _sender: AsyncComponentSender, - _root: &Self::Root, - ) { - self.process_input(input).await; + fn update(&mut self, input: Self::Input, _sender: ComponentSender, _root: &Self::Root) { + self.process_input(input); } } impl RunningView { - async fn process_input(&mut self, input: RunningInput) { + fn process_input(&mut self, input: RunningInput) { match input { RunningInput::Initialize { best_block_number, diff --git a/src/main.rs b/src/main.rs index 15894ecb..e5b01584 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,8 +60,8 @@ struct App { current_view: View, backend_action_sender: mpsc::Sender, loading_view: Controller, - configuration_view: AsyncController, - running_view: AsyncController, + configuration_view: Controller, + running_view: Controller, menu_popover: gtk::Popover, about_dialog: gtk::AboutDialog, }