Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 10 additions & 222 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ license = "AGPLv3"

[dependencies]
anyhow = "1.0.100"
config = "0.15.19"
crossterm = "0.29.0"
directories = "6.0.0"
ratatui = "0.30.0"
Expand Down
5 changes: 0 additions & 5 deletions config/config.sample.toml

This file was deleted.

7 changes: 0 additions & 7 deletions config/config.toml

This file was deleted.

122 changes: 87 additions & 35 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later

use crate::bitcoin_config::ConfigEntry as BitcoinEntry;
use crate::components::bitcoin_client::{BitcoinChainInfo, BitcoinClient};
use crate::components::bitcoin_config_view::BitcoinConfigView;
use crate::components::file_explorer::FileExplorer;
use crate::components::p2pool_client::{ChainInfo, P2PoolClient, PeerInfo, SharesResponse};
use crate::components::p2pool_config_view::P2PoolConfigView;
Expand All @@ -20,20 +18,16 @@ use tokio::sync::mpsc;
/// Sidebar items labels
pub const SIDEBAR_ITEMS: &[(&str, CurrentScreen)] = &[
("Home", CurrentScreen::Home),
("Bitcoin Config", CurrentScreen::BitcoinConfig),
("Bitcoin Status", CurrentScreen::BitcoinStatus),
("P2Pool Config", CurrentScreen::P2PoolConfig),
("P2Pool Status", CurrentScreen::P2PoolStatus),
("LN Config", CurrentScreen::LNConfig),
("LN Status", CurrentScreen::LNStatus),
("Shares Market", CurrentScreen::SharesMarket),
("Settings", CurrentScreen::Settings),
];

pub const MAX_SIDEBAR_INDEX: usize = SIDEBAR_ITEMS.len() - 1;

/// Tab labels for the Bitcoin Status view
pub const BITCOIN_STATUS_TABS: &[&str] = &["Chain Info", "System", "Logs", "Peers"];
pub const BITCOIN_STATUS_TABS: &[&str] = &["Chain Info", "Peers"];

pub const MAX_BITCOIN_STATUS_TAB: usize = BITCOIN_STATUS_TABS.len() - 1;

Expand All @@ -45,21 +39,16 @@ pub const MAX_P2POOL_STATUS_TAB: usize = P2POOL_STATUS_TABS.len() - 1;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum CurrentScreen {
Home,
BitcoinConfig,
BitcoinStatus,
P2PoolConfig,
P2PoolStatus,
LNConfig,
LNStatus,
SharesMarket,
FileExplorer,
Settings,
}

/// Identifies which screen (and optionally which field) triggered the file explorer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExplorerTrigger {
BitcoinConfig,
P2PoolConfig,
/// The `usize` is the settings field index (0–`FIELD_COUNT - 1`).
Settings(usize),
Expand All @@ -79,10 +68,6 @@ pub enum AppAction {
FileSelected(PathBuf),
// Closes the explorer without selection
CloseModal,
// Commits an edited value: (entry index, new value)
CommitEdit(usize, String),
// Saves bitcoin config to disk
SaveBitcoinConfig,
/// Commits an edited p2pool config value: (entry index, new value)
CommitP2PoolEdit(usize, String),
/// Saves p2pool config to disk
Expand All @@ -97,14 +82,11 @@ pub struct App {
pub current_screen: CurrentScreen,
pub sidebar_index: usize,
pub explorer_trigger: Option<ExplorerTrigger>,
pub bitcoin_conf_path: Option<PathBuf>,
pub p2pool_conf_path: Option<PathBuf>,
pub explorer: FileExplorer,
pub bitcoin_config_view: BitcoinConfigView,
pub p2pool_config_view: P2PoolConfigView,
pub settings_view: SettingsView,
pub p2pool_config: Option<P2PoolConfig>,
pub bitcoin_data: Vec<BitcoinEntry>,
pub bitcoin_status_tab: usize,
pub bitcoin_chain_info: Option<BitcoinChainInfo>,
pub bitcoin_chain_info_error: Option<String>,
Expand Down Expand Up @@ -150,24 +132,24 @@ impl App {
let (peer_info_tx, peer_info_rx) = mpsc::unbounded_channel();
let (share_info_tx, share_info_rx) = mpsc::unbounded_channel();
let (p2pool_live_tx, p2pool_live_rx) = mpsc::unbounded_channel();
let p2pool_client = P2PoolClient::new();
let p2pool_websocket_client = p2pool_client.websocket_client();

App {
current_screen: CurrentScreen::Home,
sidebar_index: 0,
explorer_trigger: None,
bitcoin_conf_path: None,
p2pool_conf_path: None,
explorer: FileExplorer::new(),
bitcoin_config_view: BitcoinConfigView::new(),
p2pool_config_view: P2PoolConfigView::new(),
settings_view: SettingsView::new(),
p2pool_config: None,
bitcoin_data: Vec::new(),
bitcoin_status_tab: 0,
bitcoin_chain_info: None,
bitcoin_chain_info_error: None,
settings: Settings::default(),
p2pool_client: P2PoolClient::new(),
p2pool_websocket_client: P2PoolWebSocketClient::new(),
p2pool_client,
p2pool_websocket_client,
home_dir: std::env::var("HOME").unwrap_or_default(),
config_dir: crate::settings::config_dir().unwrap_or_default(),
p2pool_status_tab: 0,
Expand Down Expand Up @@ -202,6 +184,40 @@ impl App {
app
}

pub fn set_p2pool_config(&mut self, config: P2PoolConfig) {
self.p2pool_config = Some(config);
self.refresh_p2pool_clients_from_config();
self.clear_p2pool_status_data();
}

pub fn clear_p2pool_config(&mut self) {
self.p2pool_config = None;
self.p2pool_client = P2PoolClient::new();
self.p2pool_websocket_client = self.p2pool_client.websocket_client();
self.clear_p2pool_status_data();
}

pub fn refresh_p2pool_clients_from_config(&mut self) {
if let Some(config) = self.p2pool_config.as_ref() {
self.p2pool_client = P2PoolClient::from_p2pool_config(config);
self.p2pool_websocket_client = self.p2pool_client.websocket_client();
self.p2pool_live_stream_started = false;
}
}

fn clear_p2pool_status_data(&mut self) {
self.chain_info = None;
self.p2pool_chain_info_error = None;
self.share_info = None;
self.p2pool_share_info_error = None;
self.peer_info = None;
self.p2pool_peer_info_error = None;
self.live_shares.clear();
self.live_peer_events.clear();
self.p2pool_live_error = None;
self.p2pool_live_stream_started = false;
}

/// Non-blocking result handler
pub fn poll_chain_info(&mut self) {
while let Ok(result) = self.chain_info_rx.try_recv() {
Expand Down Expand Up @@ -316,12 +332,6 @@ impl App {

// Logic to switch between sidebar items
pub fn toggle_menu(&mut self) {
if self.current_screen == CurrentScreen::BitcoinConfig {
self.bitcoin_config_view.warning_message = None;
self.bitcoin_config_view.save_message = None;
self.bitcoin_config_view.editing = false;
self.bitcoin_config_view.edit_input.clear();
}
if self.current_screen == CurrentScreen::P2PoolConfig {
self.p2pool_config_view.warning_message = None;
self.p2pool_config_view.save_message = None;
Expand All @@ -334,6 +344,11 @@ impl App {
self.fetch_bitcoin_chain_info();
}
if self.current_screen == CurrentScreen::P2PoolStatus {
if self.p2pool_config.is_none() {
self.clear_p2pool_status_data();
return;
}

let chain_client = self.p2pool_client.clone();
let chain_tx = self.chain_info_tx.clone();
let share_client = self.p2pool_client.clone();
Expand Down Expand Up @@ -380,11 +395,11 @@ impl App {
self.bitcoin_chain_info = None;
self.bitcoin_chain_info_error = None;

if self.bitcoin_conf_path.is_none() {
let Some(config) = self.p2pool_config.as_ref() else {
return;
}
};

let client = BitcoinClient::from_config_entries(&self.bitcoin_data);
let client = BitcoinClient::from_p2pool_config(config);
let tx = self.bitcoin_chain_info_tx.clone();

if let Ok(handle) = tokio::runtime::Handle::try_current() {
Expand All @@ -404,6 +419,9 @@ impl Default for App {
#[cfg(test)]
mod tests {
use super::*;
use futures_util::StreamExt;
use tokio::net::TcpListener;
use tokio_tungstenite::accept_async;

#[test]
fn poll_bitcoin_chain_info_updates_state_on_success() {
Expand Down Expand Up @@ -480,9 +498,8 @@ mod tests {
}

#[test]
fn fetch_bitcoin_chain_info_clears_state_without_configured_bitcoin_conf() {
fn fetch_bitcoin_chain_info_clears_state_without_configured_p2pool_config() {
let mut app = App::new();
app.bitcoin_conf_path = None;
app.bitcoin_chain_info = Some(BitcoinChainInfo {
network: "mainnet".to_string(),
block_height: 1,
Expand All @@ -500,4 +517,39 @@ mod tests {
assert!(app.bitcoin_chain_info_error.is_none());
assert!(app.bitcoin_chain_info_rx.try_recv().is_err());
}

#[tokio::test]
async fn set_p2pool_config_refreshes_websocket_client_from_api_section() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let websocket = accept_async(stream).await.unwrap();
let (_, mut read) = websocket.split();

let _ = read.next().await.unwrap().unwrap();
let _ = read.next().await.unwrap().unwrap();
});

let mut config = P2PoolConfig::load(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/p2pool.toml"
))
.unwrap();
config.api.hostname = addr.ip().to_string();
config.api.port = addr.port();

let mut app = App::new();
app.set_p2pool_config(config);
let (tx, mut rx) = mpsc::unbounded_channel();
let client = app.p2pool_websocket_client.clone();
let subscribe_handle = tokio::spawn(async move { client.subscribe_live_events(tx).await });

let event = rx.recv().await.unwrap();
let result = subscribe_handle.await.unwrap();
server.await.unwrap();

assert!(event.is_err());
assert!(result.is_ok());
}
}
Loading
Loading