From 5a1cb990f2be065dabb8e73bc19ff46ec67a3078 Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Sat, 22 Aug 2026 20:55:17 +0000 Subject: [PATCH 1/4] refactor: simplify Bitcoin Core integration (WIP) --- src/app.rs | 32 +- src/components/bitcoin_client.rs | 269 +----- src/components/bitcoin_status_view.rs | 71 +- src/components/mod.rs | 1 - src/components/settings_view.rs | 10 +- src/components/status_bar.rs | 120 +-- src/lib.rs | 1 - src/main.rs | 772 +----------------- src/p2poolv2_config.rs | 2 +- src/settings.rs | 37 - src/snapshots/pdm__tests__home_screen.snap | 6 +- src/snapshots/pdm__tests__menu_toggled.snap | 1 - ...pdm__ui__tests__bitcoin_screen_render.snap | 1 - ...__tests__bitcoin_status_screen_render.snap | 23 +- ...tests__bitcoin_status_tab_logs_render.snap | 50 +- ...ests__bitcoin_status_tab_peers_render.snap | 54 +- ...sts__bitcoin_status_tab_system_render.snap | 24 +- .../pdm__ui__tests__home_screen_render.snap | 7 +- ...m__ui__tests__ln_config_screen_render.snap | 7 +- ...m__ui__tests__ln_status_screen_render.snap | 7 +- ...i__tests__p2pool_config_screen_render.snap | 7 +- .../pdm__ui__tests__p2pool_screen_render.snap | 1 - ...i__tests__p2pool_status_screen_render.snap | 15 +- ...dm__ui__tests__settings_screen_render.snap | 40 +- ...i__tests__shares_market_screen_render.snap | 7 +- src/ui.rs | 98 +-- tests/fixtures/p2pool.toml | 48 ++ .../ui_snapshots__config_screen_render.snap | 30 +- .../ui_snapshots__home_screen_render.snap | 6 +- 29 files changed, 304 insertions(+), 1443 deletions(-) create mode 100644 tests/fixtures/p2pool.toml diff --git a/src/app.rs b/src/app.rs index 1c6569c..21df1eb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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; @@ -20,7 +18,6 @@ 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), @@ -33,7 +30,7 @@ pub const SIDEBAR_ITEMS: &[(&str, CurrentScreen)] = &[ 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; @@ -45,7 +42,6 @@ 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, @@ -59,7 +55,6 @@ pub enum CurrentScreen { /// 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), @@ -79,10 +74,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 @@ -97,14 +88,11 @@ pub struct App { pub current_screen: CurrentScreen, pub sidebar_index: usize, pub explorer_trigger: Option, - pub bitcoin_conf_path: Option, pub p2pool_conf_path: Option, pub explorer: FileExplorer, - pub bitcoin_config_view: BitcoinConfigView, pub p2pool_config_view: P2PoolConfigView, pub settings_view: SettingsView, pub p2pool_config: Option, - pub bitcoin_data: Vec, pub bitcoin_status_tab: usize, pub bitcoin_chain_info: Option, pub bitcoin_chain_info_error: Option, @@ -154,14 +142,11 @@ impl 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, @@ -316,12 +301,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; @@ -380,11 +359,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() { @@ -480,9 +459,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, diff --git a/src/components/bitcoin_client.rs b/src/components/bitcoin_client.rs index 05eae1c..d25433e 100644 --- a/src/components/bitcoin_client.rs +++ b/src/components/bitcoin_client.rs @@ -2,12 +2,12 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -use crate::bitcoin_config::ConfigEntry; use anyhow::{Context, Result, anyhow, bail}; +use p2poolv2_config::Config as P2PoolConfig; use reqwest::Client; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::Value; -use std::{path::PathBuf, time::Duration}; +use std::time::Duration; const REQUEST_TIMEOUT_SECONDS: u64 = 10; @@ -29,15 +29,6 @@ pub struct BitcoinChainInfo { pub connected_peer_addresses: Vec, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum BitcoinNetwork { - Mainnet, - Testnet, - Testnet4, - Signet, - Regtest, -} - #[derive(Debug, Deserialize)] struct BlockchainInfoResponse { chain: String, @@ -74,19 +65,14 @@ struct RpcError { impl BitcoinClient { #[must_use] - pub fn from_config_entries(entries: &[ConfigEntry]) -> Self { - let network = network_from_entries(entries); - let port = entry_value(entries, "rpcport") - .and_then(|value| value.parse::().ok()) - .unwrap_or_else(|| default_rpc_port(network)); - let host = entry_value(entries, "rpcbind").unwrap_or("127.0.0.1"); - let url = rpc_url(host, port); - let auth_credentials = rpc_auth(entries, network); - + pub fn from_p2pool_config(config: &P2PoolConfig) -> Self { Self { client: build_client(), - url, - auth_credentials, + url: config.bitcoinrpc.url.clone(), + auth_credentials: Some(( + config.bitcoinrpc.username.clone(), + config.bitcoinrpc.password.clone(), + )), } } @@ -161,138 +147,6 @@ fn build_client() -> Client { .expect("Failed to build reqwest client") } -fn entry_value<'a>(entries: &'a [ConfigEntry], key: &str) -> Option<&'a str> { - entries - .iter() - .find(|entry| entry.enabled && entry.key == key && !entry.value.trim().is_empty()) - .map(|entry| entry.value.trim()) -} - -fn network_from_entries(entries: &[ConfigEntry]) -> BitcoinNetwork { - if bool_entry(entries, "regtest") { - return BitcoinNetwork::Regtest; - } - if bool_entry(entries, "signet") { - return BitcoinNetwork::Signet; - } - if bool_entry(entries, "testnet4") { - return BitcoinNetwork::Testnet4; - } - if bool_entry(entries, "testnet") { - return BitcoinNetwork::Testnet; - } - - match entry_value(entries, "chain") - .unwrap_or_default() - .to_ascii_lowercase() - .as_str() - { - "test" | "testnet" | "testnet3" => BitcoinNetwork::Testnet, - "testnet4" => BitcoinNetwork::Testnet4, - "signet" => BitcoinNetwork::Signet, - "regtest" => BitcoinNetwork::Regtest, - _ => BitcoinNetwork::Mainnet, - } -} - -fn bool_entry(entries: &[ConfigEntry], key: &str) -> bool { - matches!( - entry_value(entries, key) - .map(str::to_ascii_lowercase) - .as_deref(), - Some("1" | "true" | "yes" | "on") - ) -} - -fn default_rpc_port(network: BitcoinNetwork) -> u16 { - match network { - BitcoinNetwork::Mainnet => 8332, - BitcoinNetwork::Testnet => 18332, - BitcoinNetwork::Testnet4 => 48332, - BitcoinNetwork::Signet => 38332, - BitcoinNetwork::Regtest => 18443, - } -} - -fn rpc_url(host: &str, port: u16) -> String { - let host = host.trim().trim_matches('/'); - if host.starts_with("http://") || host.starts_with("https://") { - return host.to_string(); - } - if has_explicit_port(host) { - return format!("http://{host}"); - } - if host.contains(':') && !host.starts_with('[') { - return format!("http://[{host}]:{port}"); - } - format!("http://{host}:{port}") -} - -fn has_explicit_port(host: &str) -> bool { - if let Some(end_bracket) = host.find(']') { - return host[end_bracket + 1..].starts_with(':'); - } - - host.matches(':').count() == 1 - && host - .rsplit_once(':') - .is_some_and(|(_, port)| port.parse::().is_ok()) -} - -fn rpc_auth(entries: &[ConfigEntry], network: BitcoinNetwork) -> Option<(String, String)> { - if let (Some(user), Some(pass)) = ( - entry_value(entries, "rpcuser"), - entry_value(entries, "rpcpassword"), - ) { - return Some((user.to_string(), pass.to_string())); - } - - read_cookie_auth(entries, network).ok() -} - -fn read_cookie_auth(entries: &[ConfigEntry], network: BitcoinNetwork) -> Result<(String, String)> { - let cookie_path = cookie_path(entries, network); - let content = std::fs::read_to_string(&cookie_path) - .with_context(|| format!("could not read RPC cookie at {}", cookie_path.display()))?; - let (user, pass) = content - .trim() - .split_once(':') - .ok_or_else(|| anyhow!("RPC cookie did not contain username and password"))?; - - Ok((user.to_string(), pass.to_string())) -} - -fn cookie_path(entries: &[ConfigEntry], network: BitcoinNetwork) -> PathBuf { - if let Some(path) = entry_value(entries, "rpccookiefile") { - let configured = PathBuf::from(path); - if configured.is_absolute() { - return configured; - } - return data_dir(entries, network).join(configured); - } - - data_dir(entries, network).join(".cookie") -} - -fn data_dir(entries: &[ConfigEntry], network: BitcoinNetwork) -> PathBuf { - let base = entry_value(entries, "datadir") - .map(PathBuf::from) - .or_else(default_data_dir) - .unwrap_or_default(); - - match network { - BitcoinNetwork::Mainnet => base, - BitcoinNetwork::Testnet => base.join("testnet3"), - BitcoinNetwork::Testnet4 => base.join("testnet4"), - BitcoinNetwork::Signet => base.join("signet"), - BitcoinNetwork::Regtest => base.join("regtest"), - } -} - -fn default_data_dir() -> Option { - std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".bitcoin")) -} - fn display_network(chain: &str) -> &str { match chain { "main" => "mainnet", @@ -309,55 +163,22 @@ mod tests { use mockito::{Matcher, Server}; use serde_json::json; - fn entry(key: &str, value: &str) -> ConfigEntry { - ConfigEntry { - key: key.to_string(), - value: value.to_string(), - schema: None, - enabled: true, - section: None, - } - } - #[test] - fn builds_default_mainnet_endpoint() { - let client = BitcoinClient::from_config_entries(&[]); - - assert_eq!(client.url, "http://127.0.0.1:8332"); - } - - #[test] - fn uses_configured_rpc_port_and_auth() { - let entries = vec![ - entry("rpcport", "18443"), - entry("rpcuser", "alice"), - entry("rpcpassword", "secret"), - ]; - let client = BitcoinClient::from_config_entries(&entries); - - assert_eq!(client.url, "http://127.0.0.1:18443"); + fn uses_p2pool_bitcoinrpc_configuration() { + let config = p2poolv2_config::Config::load(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/p2pool.toml" + )) + .unwrap(); + let client = BitcoinClient::from_p2pool_config(&config); + + assert_eq!(client.url, config.bitcoinrpc.url); assert_eq!( client.auth_credentials, - Some(("alice".to_string(), "secret".to_string())) + Some((config.bitcoinrpc.username, config.bitcoinrpc.password)) ); } - #[test] - fn detects_network_from_chain_setting() { - let entries = vec![entry("chain", "testnet4")]; - let client = BitcoinClient::from_config_entries(&entries); - - assert_eq!(client.url, "http://127.0.0.1:48332"); - } - - #[test] - fn preserves_rpcbind_with_explicit_port() { - let entries = vec![entry("rpcbind", "127.0.0.1:18443")]; - let client = BitcoinClient::from_config_entries(&entries); - - assert_eq!(client.url, "http://127.0.0.1:18443"); - } - #[tokio::test] async fn fetch_chain_info_success() { let mut server = Server::new_async().await; @@ -666,58 +487,4 @@ mod tests { assert_eq!(result.block_height, 1); } - - #[test] - fn ignores_disabled_and_whitespace_only_config_entries() { - let entries = vec![ - entry("rpcport", " "), - ConfigEntry { - key: "rpcport".to_string(), - value: "18443".to_string(), - schema: None, - enabled: false, - section: None, - }, - ]; - let client = BitcoinClient::from_config_entries(&entries); - - assert_eq!(client.url, "http://127.0.0.1:8332"); - } - - #[test] - fn falls_back_to_cookie_auth_when_rpc_password_is_missing() { - let temp_dir = std::env::temp_dir().join(format!( - "pdm-bitcoin-client-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - let cookie_path = temp_dir.join(".cookie"); - std::fs::create_dir_all(&temp_dir).unwrap(); - std::fs::write(&cookie_path, "alice:secret").unwrap(); - - let entries = vec![ - entry("rpcuser", "alice"), - entry("rpccookiefile", cookie_path.to_string_lossy().as_ref()), - ]; - let client = BitcoinClient::from_config_entries(&entries); - - assert_eq!( - client.auth_credentials, - Some(("alice".to_string(), "secret".to_string())) - ); - - let _ = std::fs::remove_file(cookie_path); - let _ = std::fs::remove_dir(temp_dir); - } - - #[test] - fn formats_ipv6_rpcbind_without_explicit_port() { - let entries = vec![entry("rpcbind", "::1")]; - let client = BitcoinClient::from_config_entries(&entries); - - assert_eq!(client.url, "http://[::1]:8332"); - } } diff --git a/src/components/bitcoin_status_view.rs b/src/components/bitcoin_status_view.rs index 97b474f..6b9df10 100644 --- a/src/components/bitcoin_status_view.rs +++ b/src/components/bitcoin_status_view.rs @@ -11,7 +11,7 @@ use ratatui::{ // Bitcoin Status tabs count const _: () = assert!( - BITCOIN_STATUS_TABS.len() == 4, + BITCOIN_STATUS_TABS.len() == 2, "update tab dispatch match in bitcoin_status_view.rs" ); @@ -44,32 +44,16 @@ impl BitcoinStatusView { match app.bitcoin_status_tab { // Chain Info 0 => Self::render_chain_info(f, app, content_area), - // System - 1 => { - let text = "System"; - let p = Paragraph::new(text) - .block(Block::default().borders(Borders::ALL)) - .wrap(Wrap { trim: true }); - f.render_widget(p, content_area); - } - // Logs - 2 => { - let text = "Logs"; - let p = Paragraph::new(text) - .block(Block::default().borders(Borders::ALL)) - .wrap(Wrap { trim: true }); - f.render_widget(p, content_area); - } // Peers - 3 => Self::render_peers(f, app, content_area), + 1 => Self::render_peers(f, app, content_area), _ => {} } } fn render_chain_info(f: &mut Frame, app: &App, area: Rect) { - let text = if app.bitcoin_conf_path.is_none() { + let text = if app.p2pool_config.is_none() { vec![Line::from(Span::styled( - "Select a bitcoin.conf file to load Bitcoin Core chain info.", + "Select a P2Poolv2 config file to load Bitcoin Core chain info.", Style::default().fg(Color::DarkGray), ))] } else if let Some(info) = &app.bitcoin_chain_info { @@ -110,9 +94,9 @@ impl BitcoinStatusView { } fn render_peers(f: &mut Frame, app: &App, area: Rect) { - let text = if app.bitcoin_conf_path.is_none() { + let text = if app.p2pool_config.is_none() { vec![Line::from(Span::styled( - "Select a bitcoin.conf file to load Bitcoin Core peer info.", + "Select a P2Poolv2 config file to load Bitcoin Core peer info.", Style::default().fg(Color::DarkGray), ))] } else if let Some(info) = &app.bitcoin_chain_info { @@ -181,9 +165,16 @@ impl Default for BitcoinStatusView { mod tests { use super::*; use crate::app::App; + + fn loaded_p2pool_config() -> p2poolv2_config::Config { + p2poolv2_config::Config::load(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/p2pool.toml" + )) + .unwrap() + } use crate::components::bitcoin_client::BitcoinChainInfo; use ratatui::{Terminal, backend::TestBackend, prelude::Rect}; - use std::path::PathBuf; fn render_view(app: &App) -> String { let backend = TestBackend::new(80, 25); @@ -209,7 +200,7 @@ mod tests { let output = render_view(&app); - assert!(output.contains("Select a bitcoin.conf file to load Bitcoin Core chain info.")); + assert!(output.contains("Select a P2Poolv2 config file to load Bitcoin Core chain info.")); assert!(!output.contains("Loading Bitcoin chain info")); assert!(!output.contains("Failed to fetch Bitcoin chain info")); } @@ -217,7 +208,7 @@ mod tests { #[test] fn renders_loaded_chain_info_with_formatted_values() { let mut app = App::new(); - app.bitcoin_conf_path = Some(PathBuf::from("/tmp/bitcoin.conf")); + app.p2pool_config = Some(loaded_p2pool_config()); app.bitcoin_chain_info = Some(BitcoinChainInfo { network: "mainnet".to_string(), block_height: 850_000, @@ -246,19 +237,19 @@ mod tests { #[test] fn renders_loading_state_when_chain_info_is_pending() { let mut app = App::new(); - app.bitcoin_conf_path = Some(PathBuf::from("/tmp/bitcoin.conf")); + app.p2pool_config = Some(loaded_p2pool_config()); let output = render_view(&app); assert!(output.contains("Loading Bitcoin chain info...")); - assert!(!output.contains("Select a bitcoin.conf file")); + assert!(!output.contains("Select a P2Poolv2 config file")); assert!(!output.contains("Failed to fetch Bitcoin chain info")); } #[test] fn renders_error_state_when_chain_info_fetch_fails() { let mut app = App::new(); - app.bitcoin_conf_path = Some(PathBuf::from("/tmp/bitcoin.conf")); + app.p2pool_config = Some(loaded_p2pool_config()); app.bitcoin_chain_info_error = Some("RPC offline".to_string()); let output = render_view(&app); @@ -271,7 +262,7 @@ mod tests { #[test] fn renders_none_and_false_formatting_for_optional_values() { let mut app = App::new(); - app.bitcoin_conf_path = Some(PathBuf::from("/tmp/bitcoin.conf")); + app.p2pool_config = Some(loaded_p2pool_config()); app.bitcoin_chain_info = Some(BitcoinChainInfo { network: "testnet".to_string(), block_height: 42, @@ -311,11 +302,11 @@ mod tests { #[test] fn peers_renders_prompt_when_no_bitcoin_conf_is_selected() { let mut app = App::new(); - app.bitcoin_status_tab = 3; + app.bitcoin_status_tab = 1; let output = render_peers_view(&app); - assert!(output.contains("Select a bitcoin.conf file to load Bitcoin Core peer info.")); + assert!(output.contains("Select a P2Poolv2 config file to load Bitcoin Core peer info.")); assert!(!output.contains("Loading Bitcoin peer info")); assert!(!output.contains("Failed to fetch Bitcoin peer info")); } @@ -323,8 +314,8 @@ mod tests { #[test] fn peers_renders_populated_address_list() { let mut app = App::new(); - app.bitcoin_status_tab = 3; - app.bitcoin_conf_path = Some(PathBuf::from("/tmp/bitcoin.conf")); + app.bitcoin_status_tab = 1; + app.p2pool_config = Some(loaded_p2pool_config()); app.bitcoin_chain_info = Some(BitcoinChainInfo { network: "mainnet".to_string(), block_height: 850_000, @@ -350,8 +341,8 @@ mod tests { #[test] fn peers_renders_none_when_address_list_is_empty() { let mut app = App::new(); - app.bitcoin_status_tab = 3; - app.bitcoin_conf_path = Some(PathBuf::from("/tmp/bitcoin.conf")); + app.bitcoin_status_tab = 1; + app.p2pool_config = Some(loaded_p2pool_config()); app.bitcoin_chain_info = Some(BitcoinChainInfo { network: "mainnet".to_string(), block_height: 850_000, @@ -372,21 +363,21 @@ mod tests { #[test] fn peers_renders_loading_state_when_chain_info_is_pending() { let mut app = App::new(); - app.bitcoin_status_tab = 3; - app.bitcoin_conf_path = Some(PathBuf::from("/tmp/bitcoin.conf")); + app.bitcoin_status_tab = 1; + app.p2pool_config = Some(loaded_p2pool_config()); let output = render_peers_view(&app); assert!(output.contains("Loading Bitcoin peer info...")); - assert!(!output.contains("Select a bitcoin.conf file")); + assert!(!output.contains("Select a P2Poolv2 config file")); assert!(!output.contains("Failed to fetch Bitcoin peer info")); } #[test] fn peers_renders_error_state_when_chain_info_fetch_fails() { let mut app = App::new(); - app.bitcoin_status_tab = 3; - app.bitcoin_conf_path = Some(PathBuf::from("/tmp/bitcoin.conf")); + app.bitcoin_status_tab = 1; + app.p2pool_config = Some(loaded_p2pool_config()); app.bitcoin_chain_info_error = Some("connection refused".to_string()); let output = render_peers_view(&app); diff --git a/src/components/mod.rs b/src/components/mod.rs index e55d93a..aa12e35 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -3,7 +3,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub mod bitcoin_client; -pub mod bitcoin_config_view; pub mod bitcoin_status_view; pub mod file_explorer; pub mod home_view; diff --git a/src/components/settings_view.rs b/src/components/settings_view.rs index 58b02c6..d4cd460 100644 --- a/src/components/settings_view.rs +++ b/src/components/settings_view.rs @@ -10,7 +10,7 @@ use ratatui::{ }; /// Number of settings fields. -pub const FIELD_COUNT: usize = 5; +pub const FIELD_COUNT: usize = 4; /// Describes how a settings field behaves when Enter is pressed. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -23,7 +23,6 @@ pub enum FieldKind { /// All settings fields in display order. Each entry is `(label, kind)`. pub const FIELDS: [(&str, FieldKind); FIELD_COUNT] = [ - ("Bitcoin config path", FieldKind::FilePicker), ("P2Pool config path", FieldKind::FilePicker), ("LN config path", FieldKind::FilePicker), ("Shares Market config path", FieldKind::FilePicker), @@ -80,10 +79,6 @@ impl SettingsView { pub fn render(f: &mut Frame, app: &mut App, area: Rect) { let values: [Option; FIELD_COUNT] = [ - app.settings - .bitcoin_conf_path - .as_ref() - .map(|p| p.to_string_lossy().into_owned()), app.settings .p2pool_conf_path .as_ref() @@ -114,7 +109,7 @@ impl SettingsView { .add_modifier(Modifier::BOLD), ), None => { - if idx == 4 { + if idx == 3 { let path = if app.config_dir.as_os_str().is_empty() { "(unknown)".to_string() } else { @@ -268,7 +263,6 @@ mod tests { use ratatui::backend::TestBackend; let mut app = App::new(); - app.settings.bitcoin_conf_path = Some(std::path::PathBuf::from("/tmp/bitcoin.conf")); app.settings.p2pool_conf_path = Some(std::path::PathBuf::from("/tmp/p2pool.toml")); app.settings.ln_conf_path = Some(std::path::PathBuf::from("/tmp/ln.conf")); app.settings.shares_market_conf_path = Some(std::path::PathBuf::from("/tmp/shares.conf")); diff --git a/src/components/status_bar.rs b/src/components/status_bar.rs index 775d6f6..b6b6679 100644 --- a/src/components/status_bar.rs +++ b/src/components/status_bar.rs @@ -36,43 +36,11 @@ impl StatusBar { spans.extend(hint("⌫", "Parent folder")); spans.extend(hint("Esc", "Cancel")); } - CurrentScreen::BitcoinConfig if app.bitcoin_conf_path.is_some() => { - if let Some(msg) = &app.bitcoin_config_view.save_message { - spans.push(Span::styled( - format!(" ✓ {msg} "), - Style::default().fg(Color::Green), - )); - } else if app.bitcoin_config_view.editing { - spans.extend(hint("Enter", "Confirm")); - spans.extend(hint("Esc", "Cancel")); - } else if app.bitcoin_config_view.sidebar_focused { - spans.extend(hint("↑↓", "Navigate sidebar")); - spans.extend(hint("Enter", "Focus config")); - } else { - spans.extend(hint("↑↓", "Navigate")); - spans.extend(hint("Enter", "Edit")); - spans.extend(hint("s", "Save")); - spans.extend(hint("Esc", "Back")); - } - } CurrentScreen::P2PoolConfig if app.p2pool_conf_path.is_some() => { spans.extend(hint("↑↓", "Navigate")); spans.extend(hint("Enter", "Open file")); spans.extend(hint("q", "Quit")); } - CurrentScreen::BitcoinConfig => { - if let Some(msg) = &app.bitcoin_config_view.warning_message { - spans.push(Span::styled( - format!(" ⚠ {msg} "), - Style::default().fg(Color::Yellow), - )); - spans.extend(hint("Enter", "Try again")); - } else { - spans.extend(hint("↑↓", "Navigate sidebar")); - spans.extend(hint("Enter", "Open file")); - spans.extend(hint("Esc", "Back")); - } - } CurrentScreen::Settings => { if let Some(err) = &app.settings_view.save_error { spans.push(Span::styled( @@ -86,11 +54,10 @@ impl StatusBar { let s = &app.settings; let idx = app.settings_view.selected_index; let field_is_set = match idx { - 0 => s.bitcoin_conf_path.is_some(), - 1 => s.p2pool_conf_path.is_some(), - 2 => s.ln_conf_path.is_some(), - 3 => s.shares_market_conf_path.is_some(), - 4 => s.settings_dir_override.is_some(), + 0 => s.p2pool_conf_path.is_some(), + 1 => s.ln_conf_path.is_some(), + 2 => s.shares_market_conf_path.is_some(), + 3 => s.settings_dir_override.is_some(), _ => false, }; spans.extend(hint("↑↓", "Navigate")); @@ -165,70 +132,6 @@ mod tests { assert!(output.contains("Parent folder")); } - #[test] - fn bitcoin_config_no_file_shows_open_file() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - let output = render_status_bar(&app); - assert!(output.contains("Open file")); - } - - #[test] - fn bitcoin_config_no_file_with_warning_shows_try_again() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_config_view.warning_message = Some("Not a valid config.".to_string()); - let output = render_status_bar(&app); - assert!(output.contains("Not a valid config.")); - assert!(output.contains("Try again")); - } - - #[test] - fn bitcoin_config_with_file_sidebar_focused_shows_navigate_sidebar() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_conf_path = Some(std::path::PathBuf::from("/tmp/bitcoin.conf")); - app.bitcoin_config_view.sidebar_focused = true; - let output = render_status_bar(&app); - assert!(output.contains("Navigate sidebar")); - assert!(output.contains("Focus config")); - } - - #[test] - fn bitcoin_config_with_file_editing_shows_confirm_cancel() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_conf_path = Some(std::path::PathBuf::from("/tmp/bitcoin.conf")); - app.bitcoin_config_view.sidebar_focused = false; - app.bitcoin_config_view.editing = true; - let output = render_status_bar(&app); - assert!(output.contains("Confirm")); - assert!(output.contains("Cancel")); - } - - #[test] - fn bitcoin_config_with_file_browsing_shows_edit_save_back() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_conf_path = Some(std::path::PathBuf::from("/tmp/bitcoin.conf")); - app.bitcoin_config_view.sidebar_focused = false; - app.bitcoin_config_view.editing = false; - let output = render_status_bar(&app); - assert!(output.contains("Edit")); - assert!(output.contains("Save")); - assert!(output.contains("Back")); - } - - #[test] - fn bitcoin_config_with_file_save_message_shows_saved() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_conf_path = Some(std::path::PathBuf::from("/tmp/bitcoin.conf")); - app.bitcoin_config_view.save_message = Some("Configuration correctly saved".to_string()); - let output = render_status_bar(&app); - assert!(output.contains("Configuration correctly saved")); - } - #[test] fn bitcoin_status_shows_switch_tab() { let mut app = App::new(); @@ -272,8 +175,8 @@ mod tests { let mut app = App::new(); app.current_screen = CurrentScreen::Settings; app.settings_view.sidebar_focused = false; - // field 4 is DirectoryPicker - app.settings_view.selected_index = 4; + // field 3 is DirectoryPicker + app.settings_view.selected_index = 3; let output = render_status_bar(&app); assert!(output.contains("Browse dir")); assert!(output.contains("Back")); @@ -285,7 +188,7 @@ mod tests { app.current_screen = CurrentScreen::Settings; app.settings_view.sidebar_focused = false; app.settings_view.selected_index = 0; - app.settings.bitcoin_conf_path = Some(std::path::PathBuf::from("/tmp/bitcoin.conf")); + app.settings.p2pool_conf_path = Some(std::path::PathBuf::from("/tmp/p2pool.toml")); let output = render_status_bar(&app); assert!(output.contains("Clear")); } @@ -296,7 +199,6 @@ mod tests { app.current_screen = CurrentScreen::Settings; app.settings_view.sidebar_focused = false; app.settings_view.selected_index = 0; - // bitcoin_conf_path is None by default let output = render_status_bar(&app); assert!(!output.contains("Clear")); } @@ -315,7 +217,7 @@ mod tests { let mut app = App::new(); app.current_screen = CurrentScreen::Settings; app.settings_view.sidebar_focused = false; - app.settings_view.selected_index = 1; + app.settings_view.selected_index = 0; app.settings.p2pool_conf_path = Some(std::path::PathBuf::from("/tmp/p2pool.toml")); let output = render_status_bar(&app); assert!(output.contains("Clear")); @@ -326,7 +228,7 @@ mod tests { let mut app = App::new(); app.current_screen = CurrentScreen::Settings; app.settings_view.sidebar_focused = false; - app.settings_view.selected_index = 2; + app.settings_view.selected_index = 1; app.settings.ln_conf_path = Some(std::path::PathBuf::from("/tmp/ln.conf")); let output = render_status_bar(&app); assert!(output.contains("Clear")); @@ -337,7 +239,7 @@ mod tests { let mut app = App::new(); app.current_screen = CurrentScreen::Settings; app.settings_view.sidebar_focused = false; - app.settings_view.selected_index = 3; + app.settings_view.selected_index = 2; app.settings.shares_market_conf_path = Some(std::path::PathBuf::from("/tmp/shares.conf")); let output = render_status_bar(&app); assert!(output.contains("Clear")); @@ -358,7 +260,7 @@ mod tests { let mut app = App::new(); app.current_screen = CurrentScreen::Settings; app.settings_view.sidebar_focused = false; - app.settings_view.selected_index = 4; + app.settings_view.selected_index = 3; app.settings.settings_dir_override = Some(std::path::PathBuf::from("/custom/dir")); let output = render_status_bar(&app); assert!(output.contains("Clear")); diff --git a/src/lib.rs b/src/lib.rs index 2ec9158..3a2bcf0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub mod app; -pub mod bitcoin_config; pub mod components; pub mod config; pub mod p2poolv2_config; diff --git a/src/main.rs b/src/main.rs index cd235e0..7c04ad8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,9 +7,6 @@ use pdm::app::{ App, AppAction, CurrentScreen, ExplorerTrigger, MAX_BITCOIN_STATUS_TAB, MAX_P2POOL_STATUS_TAB, MAX_SIDEBAR_INDEX, }; -use pdm::bitcoin_config::{ - parse_config as parse_bitcoin_config, save_config as save_bitcoin_config, -}; use pdm::components::settings_view::{FIELDS, FieldKind}; use pdm::p2poolv2_config::{apply_edit as apply_p2pool_edit, flatten_config}; use pdm::settings::{load_settings, save_settings}; @@ -112,12 +109,9 @@ fn dispatch_key(key: event::KeyEvent, app: &mut App) -> KeyOutcome { // Ctrl-C is always a hard exit. // 'q' is suppressed while a text-input field is active. - let text_input_active = (app.current_screen == CurrentScreen::BitcoinConfig - && !app.bitcoin_config_view.sidebar_focused - && app.bitcoin_config_view.editing) - || (app.current_screen == CurrentScreen::P2PoolConfig - && !app.p2pool_config_view.sidebar_focused - && app.p2pool_config_view.editing); + let text_input_active = (app.current_screen == CurrentScreen::P2PoolConfig + && !app.p2pool_config_view.sidebar_focused + && app.p2pool_config_view.editing); if (key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('c')) || (!text_input_active && key.code == KeyCode::Char('q')) @@ -160,32 +154,6 @@ fn dispatch_key(key: event::KeyEvent, app: &mut App) -> KeyOutcome { k => sidebar_nav(k, app), }, - CurrentScreen::BitcoinConfig => { - if app.bitcoin_conf_path.is_some() { - if app.bitcoin_config_view.sidebar_focused { - match key.code { - KeyCode::Enter => { - app.bitcoin_config_view.sidebar_focused = false; - AppAction::None - } - k => sidebar_nav(k, app), - } - } else { - let entries = &app.bitcoin_data; - app.bitcoin_config_view.handle_input(key, entries) - } - } else { - match key.code { - KeyCode::Enter => { - app.bitcoin_config_view.warning_message = None; - AppAction::OpenExplorer(ExplorerTrigger::BitcoinConfig) - } - KeyCode::Esc => AppAction::CloseModal, - k => sidebar_nav(k, app), - } - } - } - CurrentScreen::P2PoolConfig => { if app.p2pool_conf_path.is_some() { if app.p2pool_config_view.sidebar_focused { @@ -239,15 +207,6 @@ fn dispatch_key(key: event::KeyEvent, app: &mut App) -> KeyOutcome { /// Pre-populate app state from `app.settings`. Called once at startup after /// settings have been loaded into `app.settings = load_settings()`. fn bootstrap_from_settings(app: &mut App) { - // Bitcoin config - if let Some(path) = &app.settings.bitcoin_conf_path { - let entries = parse_bitcoin_config(path).unwrap_or_default(); - if entries.iter().any(|e| e.enabled && e.schema.is_some()) { - app.bitcoin_conf_path = Some(path.clone()); - app.bitcoin_data = entries; - } - } - // P2Pool config — only set the path when the config is actually loadable if let Some(path) = &app.settings.p2pool_conf_path.clone() && let Some(p) = path.to_str() @@ -342,76 +301,11 @@ fn handle_action(action: AppAction, app: &mut App) -> Result> { } app.current_screen = CurrentScreen::P2PoolConfig; } - ExplorerTrigger::BitcoinConfig => match parse_bitcoin_config(&path) { - Ok(entries) => { - const MIN_KNOWN_KEYS: usize = 1; - let known_key_count = entries - .iter() - .filter(|e| e.enabled && e.schema.is_some()) - .count(); - - if known_key_count >= MIN_KNOWN_KEYS { - app.bitcoin_conf_path = Some(path.clone()); - app.bitcoin_data = entries; - app.bitcoin_config_view.selected_index = 0; - app.bitcoin_config_view.dirty = false; - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_config_view.sidebar_focused = false; - app.bitcoin_config_view.warning_message = None; - app.settings.bitcoin_conf_path = Some(path.clone()); - app.settings_view.save_error = None; - if let Err(e) = save_settings(&app.settings) { - let save_error = format!("Save failed: {e}"); - app.settings_view.save_error = Some(save_error.clone()); - app.bitcoin_config_view.warning_message = Some(save_error); - } - } else { - app.bitcoin_config_view.warning_message = Some( - "File does not appear to be a Bitcoin config. Select another file." - .to_string(), - ); - app.current_screen = CurrentScreen::BitcoinConfig; - } - } - Err(e) => { - app.bitcoin_config_view.warning_message = Some(format!( - "Failed to read config: {e}. Check permissions and try again." - )); - app.current_screen = CurrentScreen::BitcoinConfig; - } - }, ExplorerTrigger::Settings(field) => { app.explorer.allow_dir_select = false; let mut should_save = true; match field { - 0 => match parse_bitcoin_config(&path) { - Ok(entries) => { - let known_key_count = entries - .iter() - .filter(|e| e.enabled && e.schema.is_some()) - .count(); - if known_key_count >= 1 { - app.bitcoin_conf_path = Some(path.clone()); - app.bitcoin_data = entries; - app.bitcoin_config_view.selected_index = 0; - app.bitcoin_config_view.dirty = false; - app.bitcoin_config_view.warning_message = None; - app.settings.bitcoin_conf_path = Some(path.clone()); - } else { - app.settings_view.save_error = Some( - "File does not appear to be a Bitcoin config." - .to_string(), - ); - should_save = false; - } - } - Err(e) => { - app.settings_view.save_error = - Some(format!("Failed to read config: {e}")); - should_save = false; - } - }, - 1 => match P2PoolConfig::load(path.to_str().unwrap_or_default()) { + 0 => match P2PoolConfig::load(path.to_str().unwrap_or_default()) { Ok(cfg) => { if cfg.stratum.hostname.is_empty() { app.settings_view.save_error = Some( @@ -433,9 +327,9 @@ fn handle_action(action: AppAction, app: &mut App) -> Result> { should_save = false; } }, - 2 => app.settings.ln_conf_path = Some(path.clone()), - 3 => app.settings.shares_market_conf_path = Some(path.clone()), - 4 => app.settings.settings_dir_override = Some(path.clone()), + 1 => app.settings.ln_conf_path = Some(path.clone()), + 2 => app.settings.shares_market_conf_path = Some(path.clone()), + 3 => app.settings.settings_dir_override = Some(path.clone()), _ => {} } if should_save { @@ -451,42 +345,20 @@ fn handle_action(action: AppAction, app: &mut App) -> Result> { } } - AppAction::SaveBitcoinConfig => { - if let Some(path) = &app.bitcoin_conf_path { - save_bitcoin_config(path, &app.bitcoin_data)?; - app.bitcoin_config_view.save_message = - Some("Configuration correctly saved".to_string()); - app.bitcoin_config_view.dirty = false; - } - } - AppAction::Navigate(screen) => { app.current_screen = screen; } - AppAction::CommitEdit(index, value) => { - if index < app.bitcoin_data.len() { - app.bitcoin_data[index].value = value; - app.bitcoin_data[index].enabled = true; - app.bitcoin_config_view.dirty = true; - } - } - AppAction::ClearSettingsField(field) => { match field { 0 => { - app.settings.bitcoin_conf_path = None; - app.bitcoin_conf_path = None; - app.bitcoin_data.clear(); - } - 1 => { app.settings.p2pool_conf_path = None; app.p2pool_conf_path = None; app.p2pool_config = None; } - 2 => app.settings.ln_conf_path = None, - 3 => app.settings.shares_market_conf_path = None, - 4 => app.settings.settings_dir_override = None, + 1 => app.settings.ln_conf_path = None, + 2 => app.settings.shares_market_conf_path = None, + 3 => app.settings.settings_dir_override = None, _ => {} } app.settings_view.save_error = None; @@ -796,19 +668,6 @@ port = 46884 )); } - #[test] - fn dispatch_key_q_suppressed_while_editing_bitcoin_config() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_conf_path = Some(std::path::PathBuf::from("/tmp/bitcoin.conf")); - app.bitcoin_config_view.sidebar_focused = false; - app.bitcoin_config_view.editing = true; - - let outcome = dispatch_key(press(KeyCode::Char('q')), &mut app); - - assert!(!matches!(outcome, KeyOutcome::Exit)); - } - #[test] fn dispatch_key_q_suppressed_while_editing_p2pool_config() { let mut app = App::new(); @@ -821,19 +680,6 @@ port = 46884 assert!(!matches!(outcome, KeyOutcome::Exit)); } - #[test] - fn dispatch_key_q_exits_on_bitcoin_config_when_sidebar_focused() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_config_view.sidebar_focused = true; - app.bitcoin_config_view.editing = true; - - assert!(matches!( - dispatch_key(press(KeyCode::Char('q')), &mut app), - KeyOutcome::Exit - )); - } - #[test] fn dispatch_key_bitcoin_status_left_decrements_above_zero() { let mut app = App::new(); @@ -945,92 +791,6 @@ port = 46884 assert!(matches!(outcome, KeyOutcome::Action(_))); } - #[test] - fn dispatch_key_bitcoin_config_sidebar_focused_enter_unfocuses() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_conf_path = Some(std::path::PathBuf::from("/tmp/bitcoin.conf")); - app.bitcoin_config_view.sidebar_focused = true; - - let outcome = dispatch_key(press(KeyCode::Enter), &mut app); - - assert!(!app.bitcoin_config_view.sidebar_focused); - assert!(matches!(outcome, KeyOutcome::Action(AppAction::None))); - } - - #[test] - fn dispatch_key_bitcoin_config_sidebar_focused_other_key_navigates_sidebar() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_conf_path = Some(std::path::PathBuf::from("/tmp/bitcoin.conf")); - app.bitcoin_config_view.sidebar_focused = true; - app.sidebar_index = 1; - - dispatch_key(press(KeyCode::Down), &mut app); - - assert_eq!(app.sidebar_index, 2); - } - - #[test] - fn dispatch_key_bitcoin_config_not_focused_delegates_to_view() { - use pdm::bitcoin_config::ConfigEntry; - - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_conf_path = Some(std::path::PathBuf::from("/tmp/bitcoin.conf")); - app.bitcoin_config_view.sidebar_focused = false; - app.bitcoin_data = vec![ConfigEntry { - key: "rpcuser".to_string(), - value: "old".to_string(), - enabled: true, - schema: None, - section: None, - }]; - - dispatch_key(press(KeyCode::Esc), &mut app); - - assert!(app.bitcoin_config_view.sidebar_focused); - } - - #[test] - fn dispatch_key_bitcoin_config_no_path_enter_opens_explorer() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_conf_path = None; - app.bitcoin_config_view.warning_message = Some("stale".to_string()); - - let outcome = dispatch_key(press(KeyCode::Enter), &mut app); - - assert!(app.bitcoin_config_view.warning_message.is_none()); - assert!(matches!( - outcome, - KeyOutcome::Action(AppAction::OpenExplorer(ExplorerTrigger::BitcoinConfig)) - )); - } - - #[test] - fn dispatch_key_bitcoin_config_no_path_esc_closes_modal() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_conf_path = None; - - let outcome = dispatch_key(press(KeyCode::Esc), &mut app); - - assert!(matches!(outcome, KeyOutcome::Action(AppAction::CloseModal))); - } - - #[test] - fn dispatch_key_bitcoin_config_no_path_other_key_navigates_sidebar() { - let mut app = App::new(); - app.current_screen = CurrentScreen::BitcoinConfig; - app.bitcoin_conf_path = None; - app.sidebar_index = 1; - - dispatch_key(press(KeyCode::Up), &mut app); - - assert_eq!(app.sidebar_index, 0); - } - #[test] fn dispatch_key_p2pool_config_sidebar_focused_enter_unfocuses() { let mut app = App::new(); @@ -1146,50 +906,6 @@ port = 46884 assert!(matches!(outcome, KeyOutcome::Action(AppAction::ToggleMenu))); } - #[test] - #[serial] - fn file_selected_for_settings_field_0_invalid_bitcoin_config_sets_error() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - redirect_saves_to(&dir); - let path = dir.path().join("not_a_config.conf"); - std::fs::write(&path, "unknownkey=somevalue\n").unwrap(); - - let mut app = App::new(); - app.explorer_trigger = Some(ExplorerTrigger::Settings(0)); - - run(AppAction::FileSelected(path), &mut app); - - assert_eq!( - app.settings_view.save_error.as_deref(), - Some("File does not appear to be a Bitcoin config.") - ); - assert!(app.settings.bitcoin_conf_path.is_none()); - assert_eq!(app.current_screen, CurrentScreen::Settings); - } - - #[test] - #[serial] - fn file_selected_for_settings_field_0_missing_path_sets_invalid_config_error() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - redirect_saves_to(&dir); - let missing_path = dir.path().join("does_not_exist.conf"); - - let mut app = App::new(); - app.explorer_trigger = Some(ExplorerTrigger::Settings(0)); - - run(AppAction::FileSelected(missing_path), &mut app); - - assert_eq!( - app.settings_view.save_error.as_deref(), - Some("File does not appear to be a Bitcoin config.") - ); - assert!(app.settings.bitcoin_conf_path.is_none()); - } - #[test] #[serial] fn file_selected_for_settings_field_1_invalid_hostname_sets_error() { @@ -1201,7 +917,7 @@ port = 46884 write_empty_hostname_toml(&path); let mut app = App::new(); - app.explorer_trigger = Some(ExplorerTrigger::Settings(1)); + app.explorer_trigger = Some(ExplorerTrigger::Settings(0)); run(AppAction::FileSelected(path), &mut app); @@ -1224,7 +940,7 @@ port = 46884 std::fs::write(&path, "invalid === toml").unwrap(); let mut app = App::new(); - app.explorer_trigger = Some(ExplorerTrigger::Settings(1)); + app.explorer_trigger = Some(ExplorerTrigger::Settings(0)); run(AppAction::FileSelected(path), &mut app); @@ -1248,109 +964,8 @@ port = 46884 app.toggle_menu(); terminal.draw(|f| ui::ui(f, &mut app)).unwrap(); - insta::assert_debug_snapshot!("menu_toggled", terminal.backend()); - - assert_eq!(app.current_screen, CurrentScreen::BitcoinConfig); - } - - #[test] - fn test_file_explorer_flow_state_only() { - let backend = TestBackend::new(80, 25); - let mut terminal = Terminal::new(backend).unwrap(); - let mut app = App::new(); - - // Navigate to Bitcoin config - app.sidebar_index = 1; - app.toggle_menu(); - assert_eq!(app.current_screen, CurrentScreen::BitcoinConfig); - - // Open explorer - let _ = handle_action( - AppAction::OpenExplorer(ExplorerTrigger::BitcoinConfig), - &mut app, - ) - .unwrap(); - - assert_eq!(app.current_screen, CurrentScreen::FileExplorer); - - // Close explorer - let _ = handle_action(AppAction::CloseModal, &mut app).unwrap(); - assert_eq!(app.current_screen, CurrentScreen::BitcoinConfig); - - terminal.draw(|f| ui::ui(f, &mut app)).unwrap(); - } - - #[test] - #[serial] - fn test_file_explorer_wrap_and_select_sets_config() { - use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - use tempfile::tempdir; - - // Create isolated temporary directory - let dir = tempdir().unwrap(); - redirect_saves_to(&dir); - let base = dir.path(); - - // Create a fake bitcoin.conf file - let file_path = base.join("bitcoin.conf"); - std::fs::write(&file_path, "rpcuser=test\n").unwrap(); - - let backend = TestBackend::new(80, 25); - let mut terminal = Terminal::new(backend).unwrap(); - let mut app = App::new(); - - app.explorer.current_dir = base.to_path_buf(); - app.explorer.load_directory(); - - let _ = handle_action( - AppAction::OpenExplorer(ExplorerTrigger::BitcoinConfig), - &mut app, - ) - .unwrap(); - - // Move selection DOWN to the actual file (skip "..") - app.explorer - .handle_input(KeyEvent::new(KeyCode::Down, KeyModifiers::empty())); - - let action = app - .explorer - .handle_input(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); - - let _ = handle_action(action, &mut app).unwrap(); - - assert_eq!(app.bitcoin_conf_path, Some(file_path)); - - terminal.draw(|f| ui::ui(f, &mut app)).unwrap(); - } - - #[test] - fn app_action_open_explorer_sets_state() { - let mut app = App::new(); - let flow = handle_action( - AppAction::OpenExplorer(ExplorerTrigger::BitcoinConfig), - &mut app, - ) - .unwrap(); - - assert!(flow.is_continue()); - assert_eq!(app.current_screen, CurrentScreen::FileExplorer); - assert_eq!(app.explorer_trigger, Some(ExplorerTrigger::BitcoinConfig)); - } - - #[test] - fn app_action_close_modal_returns_to_sidebar() { - let mut app = App::new(); - - app.sidebar_index = 1; // Bitcoin Config - app.explorer_trigger = Some(ExplorerTrigger::BitcoinConfig); - app.current_screen = CurrentScreen::FileExplorer; - - let flow = handle_action(AppAction::CloseModal, &mut app).unwrap(); - - assert!(flow.is_continue()); - assert_eq!(app.current_screen, CurrentScreen::BitcoinConfig); - assert!(app.explorer_trigger.is_none()); + assert_eq!(app.current_screen, CurrentScreen::BitcoinStatus); } #[test] @@ -1362,81 +977,6 @@ port = 46884 assert!(flow.is_break()); } - #[test] - fn commit_edit_updates_entry_value_and_enables_it() { - use pdm::bitcoin_config::ConfigEntry; - - let mut app = App::new(); - app.bitcoin_data = vec![ - ConfigEntry { - key: "rpcuser".to_string(), - value: "old".to_string(), - enabled: false, - schema: None, - section: None, - }, - ConfigEntry { - key: "server".to_string(), - value: "0".to_string(), - enabled: true, - schema: None, - section: None, - }, - ]; - - run(AppAction::CommitEdit(0, "alice".to_string()), &mut app); - - assert_eq!(app.bitcoin_data[0].value, "alice"); - assert!(app.bitcoin_data[0].enabled); - // Other entries unchanged - assert_eq!(app.bitcoin_data[1].value, "0"); - } - - #[test] - fn commit_edit_out_of_bounds_is_noop() { - let mut app = App::new(); - // bitcoin_data is empty - let result = handle_action(AppAction::CommitEdit(5, "val".to_string()), &mut app); - assert!(result.is_ok()); - } - - #[test] - fn save_bitcoin_config_writes_file_and_sets_message() { - use pdm::bitcoin_config::ConfigEntry; - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let path = dir.path().join("bitcoin.conf"); - - let mut app = App::new(); - app.bitcoin_conf_path = Some(path.clone()); - app.bitcoin_data = vec![ConfigEntry { - key: "rpcuser".to_string(), - value: "testuser".to_string(), - enabled: true, - schema: None, - section: None, - }]; - - run(AppAction::SaveBitcoinConfig, &mut app); - - let content = std::fs::read_to_string(&path).unwrap(); - assert!(content.contains("rpcuser=testuser")); - assert_eq!( - app.bitcoin_config_view.save_message.as_deref(), - Some("Configuration correctly saved") - ); - } - - #[test] - fn save_bitcoin_config_noop_when_no_path() { - let mut app = App::new(); - // No bitcoin_conf_path set - let result = handle_action(AppAction::SaveBitcoinConfig, &mut app); - assert!(result.is_ok()); - assert!(app.bitcoin_config_view.save_message.is_none()); - } - #[test] fn navigate_action_changes_screen() { let mut app = App::new(); @@ -1444,187 +984,6 @@ port = 46884 assert_eq!(app.current_screen, CurrentScreen::BitcoinStatus); } - #[test] - fn file_selected_invalid_bitcoin_config_sets_warning() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let path = dir.path().join("not_a_config.conf"); - // Write a file with no recognized bitcoin config keys - std::fs::write(&path, "unknownkey=somevalue\n").unwrap(); - - let mut app = App::new(); - app.explorer_trigger = Some(ExplorerTrigger::BitcoinConfig); - - run(AppAction::FileSelected(path), &mut app); - - assert!(app.bitcoin_config_view.warning_message.is_some()); - assert!(app.bitcoin_conf_path.is_none()); - assert_eq!(app.current_screen, CurrentScreen::BitcoinConfig); - } - - #[test] - #[serial] - fn bitcoin_config_sidebar_focus_toggle_via_enter() { - use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - redirect_saves_to(&dir); - let path = dir.path().join("bitcoin.conf"); - std::fs::write(&path, "rpcuser=test\n").unwrap(); - - let mut app = App::new(); - app.sidebar_index = 1; - app.toggle_menu(); - run( - AppAction::OpenExplorer(ExplorerTrigger::BitcoinConfig), - &mut app, - ); - - app.explorer.current_dir = dir.path().to_path_buf(); - app.explorer.load_directory(); - - // Select the file - app.explorer - .handle_input(KeyEvent::new(KeyCode::Down, KeyModifiers::empty())); - let action = app - .explorer - .handle_input(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); - run(action, &mut app); - - // After file selection, sidebar_focused should be false - assert!(!app.bitcoin_config_view.sidebar_focused); - - // Pressing Esc via handle_input should set sidebar_focused back - let entries_clone = app.bitcoin_data.clone(); - let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()); - app.bitcoin_config_view.handle_input(esc, &entries_clone); - assert!(app.bitcoin_config_view.sidebar_focused); - } - - // toggle_menu state cleanup - - #[test] - fn toggle_menu_clears_bitcoin_config_messages_on_navigate_away() { - let mut app = App::new(); - app.sidebar_index = 1; - app.toggle_menu(); // → BitcoinConfig - app.bitcoin_config_view.warning_message = Some("some warning".to_string()); - app.bitcoin_config_view.save_message = Some("saved".to_string()); - - app.sidebar_index = 0; - app.toggle_menu(); // → Home - - assert!(app.bitcoin_config_view.warning_message.is_none()); - assert!(app.bitcoin_config_view.save_message.is_none()); - } - - #[test] - fn toggle_menu_cancels_in_progress_edit_on_navigate_away() { - let mut app = App::new(); - app.sidebar_index = 1; - app.toggle_menu(); - app.bitcoin_config_view.editing = true; - app.bitcoin_config_view.edit_input = "draft value".to_string(); - - app.sidebar_index = 0; - app.toggle_menu(); // navigate away - - assert!(!app.bitcoin_config_view.editing); - assert!(app.bitcoin_config_view.edit_input.is_empty()); - } - - #[test] - fn toggle_menu_does_not_clear_messages_when_staying_on_other_screen() { - let mut app = App::new(); - // Start on Home (index 0), set some other state, navigate within Home - app.sidebar_index = 2; - app.toggle_menu(); // → BitcoinStatus - app.bitcoin_config_view.warning_message = Some("keep me".to_string()); - - app.sidebar_index = 3; - app.toggle_menu(); // → P2PoolConfig (never on BitcoinConfig, no clear should happen) - - // Messages only cleared when leaving BitcoinConfig, not from other screens - assert_eq!( - app.bitcoin_config_view.warning_message.as_deref(), - Some("keep me") - ); - } - - // dirty flag - - #[test] - fn commit_edit_sets_dirty_flag() { - use pdm::bitcoin_config::ConfigEntry; - - let mut app = App::new(); - app.bitcoin_data = vec![ConfigEntry { - key: "rpcuser".to_string(), - value: "old".to_string(), - enabled: true, - schema: None, - section: None, - }]; - - run(AppAction::CommitEdit(0, "new".to_string()), &mut app); - - assert!(app.bitcoin_config_view.dirty); - assert_eq!(app.bitcoin_data[0].value, "new"); - } - - #[test] - fn save_bitcoin_config_clears_dirty_flag() { - use pdm::bitcoin_config::ConfigEntry; - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let path = dir.path().join("bitcoin.conf"); - - let mut app = App::new(); - app.bitcoin_conf_path = Some(path.clone()); - app.bitcoin_config_view.dirty = true; - app.bitcoin_data = vec![ConfigEntry { - key: "rpcuser".to_string(), - value: "testuser".to_string(), - enabled: true, - schema: None, - section: None, - }]; - - run(AppAction::SaveBitcoinConfig, &mut app); - - assert!(!app.bitcoin_config_view.dirty); - } - - #[test] - #[serial] - fn file_selected_resets_dirty_flag() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - redirect_saves_to(&dir); - let path = dir.path().join("bitcoin.conf"); - std::fs::write(&path, "rpcuser=test\n").unwrap(); - - let mut app = App::new(); - app.bitcoin_config_view.dirty = true; - app.explorer_trigger = Some(ExplorerTrigger::BitcoinConfig); - - run(AppAction::FileSelected(path), &mut app); - - assert!(!app.bitcoin_config_view.dirty); - } - - #[test] - fn commit_edit_out_of_bounds_does_not_set_dirty() { - let mut app = App::new(); - // bitcoin_data is empty; CommitEdit with bad index must not set dirty - run(AppAction::CommitEdit(99, "val".to_string()), &mut app); - assert!(!app.bitcoin_config_view.dirty); - } - // --- Settings handle_action tests --- #[test] @@ -1651,7 +1010,7 @@ port = 46884 std::fs::write(&path, "").unwrap(); let mut app = App::new(); - app.explorer_trigger = Some(ExplorerTrigger::Settings(2)); // ln_conf_path + app.explorer_trigger = Some(ExplorerTrigger::Settings(1)); // ln_conf_path run(AppAction::FileSelected(path.clone()), &mut app); @@ -1660,58 +1019,6 @@ port = 46884 assert!(!app.settings_view.sidebar_focused); } - #[test] - #[serial] - fn file_selected_bitcoin_config_persists_to_settings() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - redirect_saves_to(&dir); - let path = dir.path().join("bitcoin.conf"); - std::fs::write(&path, "rpcuser=test\n").unwrap(); - - let mut app = App::new(); - app.explorer_trigger = Some(ExplorerTrigger::BitcoinConfig); - - run(AppAction::FileSelected(path.clone()), &mut app); - - assert_eq!(app.settings.bitcoin_conf_path, Some(path)); - } - - #[test] - fn bootstrap_from_settings_loads_bitcoin_config() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let path = dir.path().join("bitcoin.conf"); - std::fs::write(&path, "rpcuser=test\n").unwrap(); - - let mut app = App::new(); - app.settings.bitcoin_conf_path = Some(path.clone()); - - bootstrap_from_settings(&mut app); - - assert_eq!(app.bitcoin_conf_path, Some(path)); - assert!(!app.bitcoin_data.is_empty()); - } - - #[test] - fn bootstrap_from_settings_ignores_invalid_bitcoin_config() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let path = dir.path().join("bad.conf"); - std::fs::write(&path, "notakey=value\n").unwrap(); - - let mut app = App::new(); - app.settings.bitcoin_conf_path = Some(path); - - bootstrap_from_settings(&mut app); - - // Invalid config: bitcoin_conf_path must NOT be set on app - assert!(app.bitcoin_conf_path.is_none()); - } - // Fix 13: Settings sidebar keyboard handler respects MAX_SIDEBAR_INDEX #[test] fn settings_sidebar_down_nav_respects_max_sidebar_index() { @@ -1756,30 +1063,9 @@ port = 46884 assert_eq!(app.p2pool_conf_path, Some(path)); } - // Fix 15: file_selected_for_settings for fields 0, 1, 3 and the wildcard arm #[test] #[serial] - fn file_selected_for_settings_field_0_bitcoin_conf_path() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - redirect_saves_to(&dir); - let path = dir.path().join("bitcoin.conf"); - std::fs::write(&path, "rpcuser=test\n").unwrap(); - - let mut app = App::new(); - app.explorer_trigger = Some(ExplorerTrigger::Settings(0)); - run(AppAction::FileSelected(path.clone()), &mut app); - - assert_eq!(app.settings.bitcoin_conf_path, Some(path.clone())); - assert_eq!(app.bitcoin_conf_path, Some(path)); - assert!(!app.bitcoin_data.is_empty()); - assert_eq!(app.current_screen, CurrentScreen::Settings); - } - - #[test] - #[serial] - fn file_selected_for_settings_field_1_p2pool_conf_path() { + fn file_selected_for_settings_field_0_p2pool_conf_path() { use tempfile::tempdir; let dir = tempdir().unwrap(); @@ -1788,7 +1074,7 @@ port = 46884 write_valid_p2pool_toml(&path); let mut app = App::new(); - app.explorer_trigger = Some(ExplorerTrigger::Settings(1)); + app.explorer_trigger = Some(ExplorerTrigger::Settings(0)); run(AppAction::FileSelected(path.clone()), &mut app); assert_eq!(app.settings.p2pool_conf_path, Some(path)); @@ -1797,7 +1083,7 @@ port = 46884 #[test] #[serial] - fn file_selected_for_settings_field_3_shares_market_conf_path() { + fn file_selected_for_settings_field_2_shares_market_conf_path() { use tempfile::tempdir; let dir = tempdir().unwrap(); @@ -1806,7 +1092,7 @@ port = 46884 std::fs::write(&path, "").unwrap(); let mut app = App::new(); - app.explorer_trigger = Some(ExplorerTrigger::Settings(3)); + app.explorer_trigger = Some(ExplorerTrigger::Settings(2)); run(AppAction::FileSelected(path.clone()), &mut app); assert_eq!(app.settings.shares_market_conf_path, Some(path)); @@ -1828,7 +1114,6 @@ port = 46884 run(AppAction::FileSelected(path), &mut app); // None of the settings fields must have been touched - assert!(app.settings.bitcoin_conf_path.is_none()); assert!(app.settings.p2pool_conf_path.is_none()); assert!(app.settings.ln_conf_path.is_none()); assert!(app.settings.shares_market_conf_path.is_none()); @@ -1838,7 +1123,7 @@ port = 46884 #[test] #[serial] - fn file_selected_for_settings_field_4_sets_dir_override() { + fn file_selected_for_settings_field_3_sets_dir_override() { use tempfile::tempdir; let dir = tempdir().unwrap(); @@ -1847,7 +1132,7 @@ port = 46884 let settings_dir = tempdir().unwrap(); let mut app = App::new(); - app.explorer_trigger = Some(ExplorerTrigger::Settings(4)); + app.explorer_trigger = Some(ExplorerTrigger::Settings(3)); run( AppAction::FileSelected(settings_dir.path().to_path_buf()), &mut app, @@ -1864,9 +1149,9 @@ port = 46884 } #[test] - fn open_explorer_for_settings_field4_enables_dir_select() { + fn open_explorer_for_settings_field3_enables_dir_select() { let mut app = App::new(); - run(AppAction::OpenExplorerForSettings(4), &mut app); + run(AppAction::OpenExplorerForSettings(3), &mut app); assert!(app.explorer.allow_dir_select); assert_eq!(app.current_screen, CurrentScreen::FileExplorer); } @@ -1884,7 +1169,7 @@ port = 46884 fn close_modal_resets_allow_dir_select() { let mut app = App::new(); app.explorer.allow_dir_select = true; - app.explorer_trigger = Some(ExplorerTrigger::Settings(4)); + app.explorer_trigger = Some(ExplorerTrigger::Settings(3)); app.current_screen = CurrentScreen::FileExplorer; app.sidebar_index = MAX_SIDEBAR_INDEX; @@ -1920,27 +1205,20 @@ port = 46884 redirect_saves_to(&dir); let mut app = App::new(); - app.settings.bitcoin_conf_path = Some(PathBuf::from("/tmp/bitcoin.conf")); app.settings.p2pool_conf_path = Some(PathBuf::from("/tmp/p2pool.toml")); app.settings.ln_conf_path = Some(PathBuf::from("/tmp/ln.conf")); app.settings.shares_market_conf_path = Some(PathBuf::from("/tmp/shares.conf")); - app.bitcoin_conf_path = Some(PathBuf::from("/tmp/bitcoin.conf")); app.p2pool_conf_path = Some(PathBuf::from("/tmp/p2pool.toml")); run(AppAction::ClearSettingsField(0), &mut app); - assert!(app.settings.bitcoin_conf_path.is_none()); - assert!(app.bitcoin_conf_path.is_none()); - assert!(app.bitcoin_data.is_empty()); - - run(AppAction::ClearSettingsField(1), &mut app); assert!(app.settings.p2pool_conf_path.is_none()); assert!(app.p2pool_conf_path.is_none()); assert!(app.p2pool_config.is_none()); - run(AppAction::ClearSettingsField(2), &mut app); + run(AppAction::ClearSettingsField(1), &mut app); assert!(app.settings.ln_conf_path.is_none()); - run(AppAction::ClearSettingsField(3), &mut app); + run(AppAction::ClearSettingsField(2), &mut app); assert!(app.settings.shares_market_conf_path.is_none()); } diff --git a/src/p2poolv2_config.rs b/src/p2poolv2_config.rs index 300e04c..327f665 100644 --- a/src/p2poolv2_config.rs +++ b/src/p2poolv2_config.rs @@ -44,7 +44,7 @@ pub struct P2PoolFieldSchema { } /// A single editable TUI row — the view layer equivalent of -/// `ConfigEntry` in bitcoin_config.rs. +/// `P2PoolConfigEntry` in this module. /// The external `p2poolv2_config` crate has no concept of this; /// it only knows nested structs for deserialization. #[derive(Debug, Clone)] diff --git a/src/settings.rs b/src/settings.rs index db0f780..483af2a 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -18,8 +18,6 @@ use std::path::PathBuf; /// #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Settings { - /// Path to the Bitcoin Core config file (bitcoin.conf) - pub bitcoin_conf_path: Option, /// Path to the p2poolv2 config file pub p2pool_conf_path: Option, /// Path to the Lightning Network config file @@ -118,7 +116,6 @@ mod tests { #[test] fn default_settings_has_no_paths() { let s = Settings::default(); - assert!(s.bitcoin_conf_path.is_none()); assert!(s.p2pool_conf_path.is_none()); assert!(s.ln_conf_path.is_none()); assert!(s.shares_market_conf_path.is_none()); @@ -131,7 +128,6 @@ mod tests { // Write the settings file directly into the temp dir let path = dir.path().join("settings.toml"); let settings = Settings { - bitcoin_conf_path: Some(PathBuf::from("/tmp/bitcoin.conf")), p2pool_conf_path: Some(PathBuf::from("/tmp/p2pool.toml")), ..Default::default() }; @@ -140,7 +136,6 @@ mod tests { let loaded: Settings = toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap(); - assert_eq!(loaded.bitcoin_conf_path, settings.bitcoin_conf_path); assert_eq!(loaded.p2pool_conf_path, settings.p2pool_conf_path); assert!(loaded.ln_conf_path.is_none()); } @@ -157,16 +152,11 @@ mod tests { let dir = tempfile::tempdir().unwrap(); set_config_dir(&dir); let settings = Settings { - bitcoin_conf_path: Some(PathBuf::from("/tmp/bitcoin.conf")), ln_conf_path: Some(PathBuf::from("/tmp/ln.conf")), ..Default::default() }; save_settings(&settings).unwrap(); let loaded = load_settings(); - assert_eq!( - loaded.bitcoin_conf_path, - Some(PathBuf::from("/tmp/bitcoin.conf")) - ); assert_eq!(loaded.ln_conf_path, Some(PathBuf::from("/tmp/ln.conf"))); assert!(loaded.p2pool_conf_path.is_none()); } @@ -213,7 +203,6 @@ mod tests { set_config_dir(&dir); // No settings.toml written let settings = load_settings(); - assert!(settings.bitcoin_conf_path.is_none()); } #[test] @@ -223,7 +212,6 @@ mod tests { set_config_dir(&dir); std::fs::write(dir.path().join("settings.toml"), "not valid toml :::").unwrap(); let settings = load_settings(); - assert!(settings.bitcoin_conf_path.is_none()); } #[test] @@ -231,16 +219,6 @@ mod tests { fn load_settings_reads_valid_file() { let dir = tempfile::tempdir().unwrap(); set_config_dir(&dir); - std::fs::write( - dir.path().join("settings.toml"), - r#"bitcoin_conf_path = "/tmp/bitcoin.conf""#, - ) - .unwrap(); - let settings = load_settings(); - assert_eq!( - settings.bitcoin_conf_path, - Some(PathBuf::from("/tmp/bitcoin.conf")) - ); } #[test] @@ -264,13 +242,11 @@ mod tests { let dir = tempfile::tempdir().unwrap(); set_config_dir(&dir); let settings = Settings { - bitcoin_conf_path: Some(PathBuf::from("/tmp/bitcoin.conf")), ln_conf_path: Some(PathBuf::from("/tmp/ln.conf")), ..Default::default() }; save_settings(&settings).unwrap(); let loaded = load_settings(); - assert_eq!(loaded.bitcoin_conf_path, settings.bitcoin_conf_path); assert_eq!(loaded.ln_conf_path, settings.ln_conf_path); assert!(loaded.p2pool_conf_path.is_none()); } @@ -283,7 +259,6 @@ mod tests { set_config_dir(&default_dir); let settings = Settings { - bitcoin_conf_path: Some(PathBuf::from("/tmp/bitcoin.conf")), settings_dir_override: Some(override_dir.path().to_path_buf()), ..Default::default() }; @@ -297,8 +272,6 @@ mod tests { let override_content = std::fs::read_to_string(&override_path).unwrap(); let default_content = std::fs::read_to_string(&default_path).unwrap(); - assert!(override_content.contains("/tmp/bitcoin.conf")); - assert!(default_content.contains("/tmp/bitcoin.conf")); } #[test] @@ -321,7 +294,6 @@ mod tests { // Write the authoritative settings in the override dir. let authoritative = Settings { - bitcoin_conf_path: Some(PathBuf::from("/override/bitcoin.conf")), settings_dir_override: Some(override_dir.path().to_path_buf()), ..Default::default() }; @@ -332,10 +304,6 @@ mod tests { .unwrap(); let loaded = load_settings(); - assert_eq!( - loaded.bitcoin_conf_path, - Some(PathBuf::from("/override/bitcoin.conf")) - ); } #[test] @@ -346,7 +314,6 @@ mod tests { // Pointer points to a directory that doesn't exist. let pointer = Settings { - bitcoin_conf_path: Some(PathBuf::from("/default/bitcoin.conf")), settings_dir_override: Some(PathBuf::from("/nonexistent/dir")), ..Default::default() }; @@ -358,9 +325,5 @@ mod tests { let loaded = load_settings(); // Override unreadable → falls back to the default-location settings. - assert_eq!( - loaded.bitcoin_conf_path, - Some(PathBuf::from("/default/bitcoin.conf")) - ); } } diff --git a/src/snapshots/pdm__tests__home_screen.snap b/src/snapshots/pdm__tests__home_screen.snap index 07fb4b8..b1866f1 100644 --- a/src/snapshots/pdm__tests__home_screen.snap +++ b/src/snapshots/pdm__tests__home_screen.snap @@ -8,9 +8,8 @@ TestBackend { content: [ "┌ PDM ──────────────────┐┌ Home ───────────────────────────────────────────────┐", "│Home ││Welcome to PDM. │", - "│Bitcoin Config ││ │", - "│Bitcoin Status ││Select a config from the sidebar to edit. │", - "│P2Pool Config ││ │", + "│Bitcoin Status ││ │", + "│P2Pool Config ││Select a config from the sidebar to edit. │", "│P2Pool Status ││ │", "│LN Config ││ │", "│LN Status ││ │", @@ -29,6 +28,7 @@ TestBackend { "│ ││ │", "│ ││ │", "│ ││ │", + "│ ││ │", "└───────────────────────┘└─────────────────────────────────────────────────────┘", " ↑↓ Navigate sidebar Enter Select q Quit ", ], diff --git a/src/snapshots/pdm__tests__menu_toggled.snap b/src/snapshots/pdm__tests__menu_toggled.snap index 94113b6..f142bac 100644 --- a/src/snapshots/pdm__tests__menu_toggled.snap +++ b/src/snapshots/pdm__tests__menu_toggled.snap @@ -8,7 +8,6 @@ TestBackend { content: [ "┌ PDM ──────────────────┐┌ Bitcoin Config ─────────────────────────────────────┐", "│Home ││Press [Enter] to select a bitcoin.conf file │", - "│Bitcoin Config ││ │", "│Bitcoin Status ││ │", "│P2Pool Config ││ │", "│P2Pool Status ││ │", diff --git a/src/snapshots/pdm__ui__tests__bitcoin_screen_render.snap b/src/snapshots/pdm__ui__tests__bitcoin_screen_render.snap index 4463830..67b9fd7 100644 --- a/src/snapshots/pdm__ui__tests__bitcoin_screen_render.snap +++ b/src/snapshots/pdm__ui__tests__bitcoin_screen_render.snap @@ -8,7 +8,6 @@ TestBackend { content: [ "┌ PDM ──────────────────┐┌ Bitcoin Config ─────────────────────────────────────┐", "│Home ││Press [Enter] to select a bitcoin.conf file │", - "│Bitcoin Config ││ │", "│Bitcoin Status ││ │", "│P2Pool Config ││ │", "│P2Pool Status ││ │", diff --git a/src/snapshots/pdm__ui__tests__bitcoin_status_screen_render.snap b/src/snapshots/pdm__ui__tests__bitcoin_status_screen_render.snap index 5593cb3..0f4d704 100644 --- a/src/snapshots/pdm__ui__tests__bitcoin_status_screen_render.snap +++ b/src/snapshots/pdm__ui__tests__bitcoin_status_screen_render.snap @@ -1,6 +1,5 @@ --- source: src/ui.rs -assertion_line: 149 expression: terminal.backend() --- TestBackend { @@ -8,13 +7,12 @@ TestBackend { area: Rect { x: 0, y: 0, width: 80, height: 24 }, content: [ "┌ PDM ──────────────────┐┌ Info ───────────────────────────────────────────────┐", - "│Home ││ Chain Info │ System │ Logs │ Peers │", - "│Bitcoin Config ││ │", - "│Bitcoin Status │└─────────────────────────────────────────────────────┘", - "│P2Pool Config │┌ Chain Info ─────────────────────────────────────────┐", - "│P2Pool Status ││Select a bitcoin.conf file to load Bitcoin Core chain│", - "│LN Config ││info. │", - "│LN Status ││ │", + "│Home ││ Chain Info │ Peers │", + "│Bitcoin Status ││ │", + "│P2Pool Config │└─────────────────────────────────────────────────────┘", + "│P2Pool Status │┌ Chain Info ─────────────────────────────────────────┐", + "│LN Config ││Select a P2Poolv2 config file to load Bitcoin Core │", + "│LN Status ││chain info. │", "│Shares Market ││ │", "│Settings ││ │", "│ ││ │", @@ -29,6 +27,7 @@ TestBackend { "│ ││ │", "│ ││ │", "│ ││ │", + "│ ││ │", "└───────────────────────┘└─────────────────────────────────────────────────────┘", " ↑↓ Navigate sidebar ←→ Switch tab q Quit ", ], @@ -36,12 +35,12 @@ TestBackend { x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 27, y: 1, fg: Black, bg: Gray, underline: Reset, modifier: NONE, x: 37, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 3, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 24, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 2, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 24, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 26, y: 5, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, - x: 79, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 76, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 26, y: 6, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, - x: 31, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 37, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, x: 4, y: 23, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, x: 23, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, diff --git a/src/snapshots/pdm__ui__tests__bitcoin_status_tab_logs_render.snap b/src/snapshots/pdm__ui__tests__bitcoin_status_tab_logs_render.snap index 0d5432c..0e166ca 100644 --- a/src/snapshots/pdm__ui__tests__bitcoin_status_tab_logs_render.snap +++ b/src/snapshots/pdm__ui__tests__bitcoin_status_tab_logs_render.snap @@ -7,36 +7,34 @@ TestBackend { area: Rect { x: 0, y: 0, width: 80, height: 24 }, content: [ "┌ PDM ──────────────────┐┌ Info ───────────────────────────────────────────────┐", - "│Home ││ Chain Info │ System │ Logs │ Peers │", - "│Bitcoin Config ││ │", - "│Bitcoin Status │└─────────────────────────────────────────────────────┘", - "│P2Pool Config │┌─────────────────────────────────────────────────────┐", - "│P2Pool Status ││Logs │", - "│LN Config ││ │", - "│LN Status ││ │", - "│Shares Market ││ │", - "│Settings ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "└───────────────────────┘└─────────────────────────────────────────────────────┘", + "│Home ││ Chain Info │ Peers │", + "│Bitcoin Status ││ │", + "│P2Pool Config │└─────────────────────────────────────────────────────┘", + "│P2Pool Status │ ", + "│LN Config │ ", + "│LN Status │ ", + "│Shares Market │ ", + "│Settings │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "└───────────────────────┘ ", " ↑↓ Navigate sidebar ←→ Switch tab q Quit ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 49, y: 1, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 53, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 3, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 24, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 2, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 24, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, x: 4, y: 23, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, x: 23, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, diff --git a/src/snapshots/pdm__ui__tests__bitcoin_status_tab_peers_render.snap b/src/snapshots/pdm__ui__tests__bitcoin_status_tab_peers_render.snap index 3cd08fc..0e166ca 100644 --- a/src/snapshots/pdm__ui__tests__bitcoin_status_tab_peers_render.snap +++ b/src/snapshots/pdm__ui__tests__bitcoin_status_tab_peers_render.snap @@ -7,40 +7,34 @@ TestBackend { area: Rect { x: 0, y: 0, width: 80, height: 24 }, content: [ "┌ PDM ──────────────────┐┌ Info ───────────────────────────────────────────────┐", - "│Home ││ Chain Info │ System │ Logs │ Peers │", - "│Bitcoin Config ││ │", - "│Bitcoin Status │└─────────────────────────────────────────────────────┘", - "│P2Pool Config │┌ Peers ──────────────────────────────────────────────┐", - "│P2Pool Status ││Select a bitcoin.conf file to load Bitcoin Core peer │", - "│LN Config ││info. │", - "│LN Status ││ │", - "│Shares Market ││ │", - "│Settings ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "│ ││ │", - "└───────────────────────┘└─────────────────────────────────────────────────────┘", + "│Home ││ Chain Info │ Peers │", + "│Bitcoin Status ││ │", + "│P2Pool Config │└─────────────────────────────────────────────────────┘", + "│P2Pool Status │ ", + "│LN Config │ ", + "│LN Status │ ", + "│Shares Market │ ", + "│Settings │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "└───────────────────────┘ ", " ↑↓ Navigate sidebar ←→ Switch tab q Quit ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 56, y: 1, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 61, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 3, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 24, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 26, y: 5, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, - x: 78, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 26, y: 6, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, - x: 31, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 2, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 24, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, x: 4, y: 23, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, x: 23, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, diff --git a/src/snapshots/pdm__ui__tests__bitcoin_status_tab_system_render.snap b/src/snapshots/pdm__ui__tests__bitcoin_status_tab_system_render.snap index 4e116ec..fd739b9 100644 --- a/src/snapshots/pdm__ui__tests__bitcoin_status_tab_system_render.snap +++ b/src/snapshots/pdm__ui__tests__bitcoin_status_tab_system_render.snap @@ -7,13 +7,12 @@ TestBackend { area: Rect { x: 0, y: 0, width: 80, height: 24 }, content: [ "┌ PDM ──────────────────┐┌ Info ───────────────────────────────────────────────┐", - "│Home ││ Chain Info │ System │ Logs │ Peers │", - "│Bitcoin Config ││ │", - "│Bitcoin Status │└─────────────────────────────────────────────────────┘", - "│P2Pool Config │┌─────────────────────────────────────────────────────┐", - "│P2Pool Status ││System │", - "│LN Config ││ │", - "│LN Status ││ │", + "│Home ││ Chain Info │ Peers │", + "│Bitcoin Status ││ │", + "│P2Pool Config │└─────────────────────────────────────────────────────┘", + "│P2Pool Status │┌ Peers ──────────────────────────────────────────────┐", + "│LN Config ││Select a P2Poolv2 config file to load Bitcoin Core │", + "│LN Status ││peer info. │", "│Shares Market ││ │", "│Settings ││ │", "│ ││ │", @@ -28,15 +27,20 @@ TestBackend { "│ ││ │", "│ ││ │", "│ ││ │", + "│ ││ │", "└───────────────────────┘└─────────────────────────────────────────────────────┘", " ↑↓ Navigate sidebar ←→ Switch tab q Quit ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 40, y: 1, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 46, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 3, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 24, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 45, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 2, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 24, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 26, y: 5, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, + x: 76, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 26, y: 6, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, + x: 36, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, x: 4, y: 23, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, x: 23, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, diff --git a/src/snapshots/pdm__ui__tests__home_screen_render.snap b/src/snapshots/pdm__ui__tests__home_screen_render.snap index c25d78b..cfeb50d 100644 --- a/src/snapshots/pdm__ui__tests__home_screen_render.snap +++ b/src/snapshots/pdm__ui__tests__home_screen_render.snap @@ -1,6 +1,5 @@ --- source: src/ui.rs -assertion_line: 117 expression: terminal.backend() --- TestBackend { @@ -9,9 +8,8 @@ TestBackend { content: [ "┌ PDM ──────────────────┐┌ Home ───────────────────────────────────────────────┐", "│Home ││Welcome to PDM. │", - "│Bitcoin Config ││ │", - "│Bitcoin Status ││Select a config from the sidebar to edit. │", - "│P2Pool Config ││ │", + "│Bitcoin Status ││ │", + "│P2Pool Config ││Select a config from the sidebar to edit. │", "│P2Pool Status ││ │", "│LN Config ││ │", "│LN Status ││ │", @@ -29,6 +27,7 @@ TestBackend { "│ ││ │", "│ ││ │", "│ ││ │", + "│ ││ │", "└───────────────────────┘└─────────────────────────────────────────────────────┘", " ↑↓ Navigate sidebar Enter Select q Quit ", ], diff --git a/src/snapshots/pdm__ui__tests__ln_config_screen_render.snap b/src/snapshots/pdm__ui__tests__ln_config_screen_render.snap index c4861bc..9814453 100644 --- a/src/snapshots/pdm__ui__tests__ln_config_screen_render.snap +++ b/src/snapshots/pdm__ui__tests__ln_config_screen_render.snap @@ -1,6 +1,5 @@ --- source: src/ui.rs -assertion_line: 200 expression: terminal.backend() --- TestBackend { @@ -9,7 +8,6 @@ TestBackend { content: [ "┌ PDM ──────────────────┐┌ LN Config ──────────────────────────────────────────┐", "│Home ││LN Config │", - "│Bitcoin Config ││ │", "│Bitcoin Status ││ │", "│P2Pool Config ││ │", "│P2Pool Status ││ │", @@ -29,13 +27,14 @@ TestBackend { "│ ││ │", "│ ││ │", "│ ││ │", + "│ ││ │", "└───────────────────────┘└─────────────────────────────────────────────────────┘", " ↑↓ Navigate sidebar Enter Select q Quit ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 6, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 24, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 5, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 24, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, x: 4, y: 23, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, x: 23, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, diff --git a/src/snapshots/pdm__ui__tests__ln_status_screen_render.snap b/src/snapshots/pdm__ui__tests__ln_status_screen_render.snap index c5ace26..63c8e49 100644 --- a/src/snapshots/pdm__ui__tests__ln_status_screen_render.snap +++ b/src/snapshots/pdm__ui__tests__ln_status_screen_render.snap @@ -1,6 +1,5 @@ --- source: src/ui.rs -assertion_line: 210 expression: terminal.backend() --- TestBackend { @@ -9,7 +8,6 @@ TestBackend { content: [ "┌ PDM ──────────────────┐┌ LN Status ──────────────────────────────────────────┐", "│Home ││LN Status │", - "│Bitcoin Config ││ │", "│Bitcoin Status ││ │", "│P2Pool Config ││ │", "│P2Pool Status ││ │", @@ -29,13 +27,14 @@ TestBackend { "│ ││ │", "│ ││ │", "│ ││ │", + "│ ││ │", "└───────────────────────┘└─────────────────────────────────────────────────────┘", " ↑↓ Navigate sidebar Enter Select q Quit ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 7, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 24, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 6, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 24, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, x: 4, y: 23, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, x: 23, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, diff --git a/src/snapshots/pdm__ui__tests__p2pool_config_screen_render.snap b/src/snapshots/pdm__ui__tests__p2pool_config_screen_render.snap index 86a80fa..513bce4 100644 --- a/src/snapshots/pdm__ui__tests__p2pool_config_screen_render.snap +++ b/src/snapshots/pdm__ui__tests__p2pool_config_screen_render.snap @@ -1,6 +1,5 @@ --- source: src/ui.rs -assertion_line: 180 expression: terminal.backend() --- TestBackend { @@ -9,7 +8,6 @@ TestBackend { content: [ "┌ PDM ──────────────────┐┌ P2Pool Config ──────────────────────────────────────┐", "│Home ││Press [Enter] to select a p2poolv2 config file │", - "│Bitcoin Config ││ │", "│Bitcoin Status ││ │", "│P2Pool Config ││ │", "│P2Pool Status ││ │", @@ -29,13 +27,14 @@ TestBackend { "│ ││ │", "│ ││ │", "│ ││ │", + "│ ││ │", "└───────────────────────┘└─────────────────────────────────────────────────────┘", " ↑↓ Navigate sidebar Enter Select q Quit ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 4, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 24, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 3, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 24, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, x: 4, y: 23, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, x: 23, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, diff --git a/src/snapshots/pdm__ui__tests__p2pool_screen_render.snap b/src/snapshots/pdm__ui__tests__p2pool_screen_render.snap index 2facef3..9fcfe39 100644 --- a/src/snapshots/pdm__ui__tests__p2pool_screen_render.snap +++ b/src/snapshots/pdm__ui__tests__p2pool_screen_render.snap @@ -8,7 +8,6 @@ TestBackend { content: [ "┌ PDM ──────────────────┐┌ Info ───────────────────────────────────────────────┐", "│Home ││ Chain Info │ System │ Logs │ Peers │", - "│Bitcoin Config ││ │", "│Bitcoin Status │└─────────────────────────────────────────────────────┘", "│P2Pool Config │┌─────────────────────────────────────────────────────┐", "│P2Pool Status ││Chain Info │", diff --git a/src/snapshots/pdm__ui__tests__p2pool_status_screen_render.snap b/src/snapshots/pdm__ui__tests__p2pool_status_screen_render.snap index 3f096dd..224edd8 100644 --- a/src/snapshots/pdm__ui__tests__p2pool_status_screen_render.snap +++ b/src/snapshots/pdm__ui__tests__p2pool_status_screen_render.snap @@ -1,6 +1,5 @@ --- source: src/ui.rs -assertion_line: 219 expression: terminal.backend() --- TestBackend { @@ -9,11 +8,10 @@ TestBackend { content: [ "┌ PDM ──────────────────┐┌ Info ───────────────────────────────────────────────┐", "│Home ││ Chain Info │ Shares │ Peers Info │", - "│Bitcoin Config ││ │", - "│Bitcoin Status │└─────────────────────────────────────────────────────┘", - "│P2Pool Config │┌ Chain Info ─────────────────────────────────────────┐", - "│P2Pool Status ││Loading chain info... │", - "│LN Config ││ │", + "│Bitcoin Status ││ │", + "│P2Pool Config │└─────────────────────────────────────────────────────┘", + "│P2Pool Status │┌ Chain Info ─────────────────────────────────────────┐", + "│LN Config ││Loading chain info... │", "│LN Status ││ │", "│Shares Market ││ │", "│Settings ││ │", @@ -29,6 +27,7 @@ TestBackend { "│ ││ │", "│ ││ │", "│ ││ │", + "│ ││ │", "└───────────────────────┘└─────────────────────────────────────────────────────┘", " ↑↓ Navigate sidebar ←→ Switch tab q Quit ", ], @@ -36,8 +35,8 @@ TestBackend { x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 27, y: 1, fg: Black, bg: Gray, underline: Reset, modifier: NONE, x: 37, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 5, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 24, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 4, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 24, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 26, y: 5, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 47, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, diff --git a/src/snapshots/pdm__ui__tests__settings_screen_render.snap b/src/snapshots/pdm__ui__tests__settings_screen_render.snap index 406ff9a..63cdc8a 100644 --- a/src/snapshots/pdm__ui__tests__settings_screen_render.snap +++ b/src/snapshots/pdm__ui__tests__settings_screen_render.snap @@ -1,6 +1,5 @@ --- source: src/ui.rs -assertion_line: 245 expression: terminal.backend() --- TestBackend { @@ -8,16 +7,16 @@ TestBackend { area: Rect { x: 0, y: 0, width: 80, height: 24 }, content: [ "┌ PDM ──────────────────┐┌ Settings ───────────────────────────────────────────┐", - "│Home ││Bitcoin config path │", - "│Bitcoin Config ││(not set) │", - "│Bitcoin Status ││P2Pool config path │", - "│P2Pool Config ││(not set) │", - "│P2Pool Status ││LN config path │", - "│LN Config ││(not set) │", - "│LN Status ││Shares Market config path │", - "│Shares Market ││(not set) │", - "│Settings ││Settings directory │", - "│ ││/pdm/test-config │", + "│Home ││P2Pool config path │", + "│Bitcoin Status ││(not set) │", + "│P2Pool Config ││LN config path │", + "│P2Pool Status ││(not set) │", + "│LN Config ││Shares Market config path │", + "│LN Status ││(not set) │", + "│Shares Market ││Settings directory │", + "│Settings ││/pdm/test-config │", + "│ ││ │", + "│ ││ │", "│ ││ │", "│ ││ │", "│ ││ │", @@ -38,7 +37,7 @@ TestBackend { x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 25, y: 1, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 26, y: 1, fg: Gray, bg: DarkGray, underline: Reset, modifier: NONE, - x: 45, y: 1, fg: Reset, bg: DarkGray, underline: Reset, modifier: NONE, + x: 44, y: 1, fg: Reset, bg: DarkGray, underline: Reset, modifier: NONE, x: 79, y: 1, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 25, y: 2, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, @@ -48,7 +47,7 @@ TestBackend { x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 25, y: 3, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 26, y: 3, fg: Gray, bg: Reset, underline: Reset, modifier: NONE, - x: 44, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 40, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 79, y: 3, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 25, y: 4, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, @@ -57,7 +56,7 @@ TestBackend { x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 25, y: 5, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 26, y: 5, fg: Gray, bg: Reset, underline: Reset, modifier: NONE, - x: 40, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 51, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 79, y: 5, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 25, y: 6, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, @@ -66,22 +65,21 @@ TestBackend { x: 0, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 25, y: 7, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 26, y: 7, fg: Gray, bg: Reset, underline: Reset, modifier: NONE, - x: 51, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 44, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 79, y: 7, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 8, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 24, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 25, y: 8, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, - x: 35, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 42, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 79, y: 8, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 9, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 24, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 25, y: 9, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, - x: 26, y: 9, fg: Gray, bg: Reset, underline: Reset, modifier: NONE, - x: 44, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 26, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 79, y: 9, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 25, y: 10, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, - x: 42, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 26, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 79, y: 10, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 25, y: 11, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, diff --git a/src/snapshots/pdm__ui__tests__shares_market_screen_render.snap b/src/snapshots/pdm__ui__tests__shares_market_screen_render.snap index 1f5e25e..89a1c75 100644 --- a/src/snapshots/pdm__ui__tests__shares_market_screen_render.snap +++ b/src/snapshots/pdm__ui__tests__shares_market_screen_render.snap @@ -1,6 +1,5 @@ --- source: src/ui.rs -assertion_line: 220 expression: terminal.backend() --- TestBackend { @@ -9,7 +8,6 @@ TestBackend { content: [ "┌ PDM ──────────────────┐┌ Shares Market ──────────────────────────────────────┐", "│Home ││Shares Market │", - "│Bitcoin Config ││ │", "│Bitcoin Status ││ │", "│P2Pool Config ││ │", "│P2Pool Status ││ │", @@ -29,13 +27,14 @@ TestBackend { "│ ││ │", "│ ││ │", "│ ││ │", + "│ ││ │", "└───────────────────────┘└─────────────────────────────────────────────────────┘", " ↑↓ Navigate sidebar Enter Select q Quit ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 8, fg: Black, bg: Gray, underline: Reset, modifier: NONE, - x: 24, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 1, y: 7, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 24, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, x: 4, y: 23, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, x: 23, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, diff --git a/src/ui.rs b/src/ui.rs index f14d3e8..18f30c5 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -5,11 +5,10 @@ use crate::app; use crate::app::{App, CurrentScreen}; use crate::components::{ - bitcoin_config_view::BitcoinConfigView, bitcoin_status_view::BitcoinStatusView, - file_explorer::FileExplorer, home_view::HomeView, ln_config_view::LNConfigView, - ln_status_view::LNStatusView, p2pool_config_view::P2PoolConfigView, - p2pool_status_view::P2PoolStatusView, settings_view::SettingsView, - shares_market_view::SharesMarketView, status_bar::StatusBar, + bitcoin_status_view::BitcoinStatusView, file_explorer::FileExplorer, home_view::HomeView, + ln_config_view::LNConfigView, ln_status_view::LNStatusView, + p2pool_config_view::P2PoolConfigView, p2pool_status_view::P2PoolStatusView, + settings_view::SettingsView, shares_market_view::SharesMarketView, status_bar::StatusBar, }; use ratatui::{ prelude::*, @@ -19,10 +18,7 @@ use ratatui::{ pub fn ui(f: &mut Frame, app: &mut App) { let outer = Layout::default() .direction(Direction::Vertical) - .constraints([ - Constraint::Min(0), // Main area - Constraint::Length(1), // Status bar - ]) + .constraints([Constraint::Min(0), Constraint::Length(1)]) .split(f.area()); let main_row = outer[0]; @@ -30,25 +26,18 @@ pub fn ui(f: &mut Frame, app: &mut App) { let chunks = Layout::default() .direction(Direction::Horizontal) - .constraints([ - Constraint::Length(25), // Sidebar - Constraint::Min(0), // Main Content - ]) + .constraints([Constraint::Length(25), Constraint::Min(0)]) .split(main_row); - // Sidebar let items: Vec = app::SIDEBAR_ITEMS .iter() .map(|&(label, _)| ListItem::new(label)) .collect(); - // Highlight the active one let mut state = ListState::default(); state.select(Some(app.sidebar_index)); - // Dim the sidebar when the user has moved focus into a content panel let sidebar_focused = match app.current_screen { - CurrentScreen::BitcoinConfig => app.bitcoin_config_view.sidebar_focused, CurrentScreen::Settings => app.settings_view.sidebar_focused, _ => true, }; @@ -69,40 +58,18 @@ pub fn ui(f: &mut Frame, app: &mut App) { f.render_stateful_widget(sidebar, chunks[0], &mut state); - // Main Content let main_area = chunks[1]; match app.current_screen { - CurrentScreen::Home => { - HomeView::render(f, app, main_area); - } - CurrentScreen::BitcoinConfig => { - BitcoinConfigView::render(f, app, main_area); - } - CurrentScreen::BitcoinStatus => { - BitcoinStatusView::render(f, app, main_area); - } - CurrentScreen::P2PoolConfig => { - P2PoolConfigView::render(f, app, main_area); - } - CurrentScreen::P2PoolStatus => { - P2PoolStatusView::render(f, app, main_area); - } - CurrentScreen::LNConfig => { - LNConfigView::render(f, app, main_area); - } - CurrentScreen::LNStatus => { - LNStatusView::render(f, app, main_area); - } - CurrentScreen::SharesMarket => { - SharesMarketView::render(f, app, main_area); - } - CurrentScreen::FileExplorer => { - FileExplorer::render(f, app, main_area); - } - CurrentScreen::Settings => { - SettingsView::render(f, app, main_area); - } + CurrentScreen::Home => HomeView::render(f, app, main_area), + CurrentScreen::BitcoinStatus => BitcoinStatusView::render(f, app, main_area), + CurrentScreen::P2PoolConfig => P2PoolConfigView::render(f, app, main_area), + CurrentScreen::P2PoolStatus => P2PoolStatusView::render(f, app, main_area), + CurrentScreen::LNConfig => LNConfigView::render(f, app, main_area), + CurrentScreen::LNStatus => LNStatusView::render(f, app, main_area), + CurrentScreen::SharesMarket => SharesMarketView::render(f, app, main_area), + CurrentScreen::FileExplorer => FileExplorer::render(f, app, main_area), + CurrentScreen::Settings => SettingsView::render(f, app, main_area), } StatusBar::render(f, app, status_bar_area); @@ -127,21 +94,11 @@ mod tests { insta::assert_debug_snapshot!(terminal.backend()); } - #[test] - fn test_bitcoin_config_screen_render() { - let mut terminal = make_terminal(); - let mut app = App::new(); - app.sidebar_index = 1; - app.toggle_menu(); - terminal.draw(|f| ui(f, &mut app)).unwrap(); - insta::assert_debug_snapshot!(terminal.backend()); - } - #[test] fn test_bitcoin_status_screen_render() { let mut terminal = make_terminal(); let mut app = App::new(); - app.sidebar_index = 2; + app.sidebar_index = 1; app.toggle_menu(); terminal.draw(|f| ui(f, &mut app)).unwrap(); insta::assert_debug_snapshot!(terminal.backend()); @@ -151,7 +108,7 @@ mod tests { fn test_bitcoin_status_tab_system_render() { let mut terminal = make_terminal(); let mut app = App::new(); - app.sidebar_index = 2; + app.sidebar_index = 1; app.toggle_menu(); app.bitcoin_status_tab = 1; terminal.draw(|f| ui(f, &mut app)).unwrap(); @@ -162,7 +119,7 @@ mod tests { fn test_bitcoin_status_tab_logs_render() { let mut terminal = make_terminal(); let mut app = App::new(); - app.sidebar_index = 2; + app.sidebar_index = 1; app.toggle_menu(); app.bitcoin_status_tab = 2; terminal.draw(|f| ui(f, &mut app)).unwrap(); @@ -173,7 +130,7 @@ mod tests { fn test_bitcoin_status_tab_peers_render() { let mut terminal = make_terminal(); let mut app = App::new(); - app.sidebar_index = 2; + app.sidebar_index = 1; app.toggle_menu(); app.bitcoin_status_tab = 3; terminal.draw(|f| ui(f, &mut app)).unwrap(); @@ -184,7 +141,7 @@ mod tests { fn test_p2pool_config_screen_render() { let mut terminal = make_terminal(); let mut app = App::new(); - app.sidebar_index = 3; + app.sidebar_index = 2; app.toggle_menu(); terminal.draw(|f| ui(f, &mut app)).unwrap(); insta::assert_debug_snapshot!(terminal.backend()); @@ -194,12 +151,9 @@ mod tests { fn test_p2pool_status_screen_render() { let mut terminal = make_terminal(); let mut app = App::new(); - - app.sidebar_index = 4; + app.sidebar_index = 3; app.toggle_menu(); - terminal.draw(|f| ui(f, &mut app)).unwrap(); - insta::assert_debug_snapshot!(terminal.backend()); } @@ -207,7 +161,7 @@ mod tests { fn test_ln_config_screen_render() { let mut terminal = make_terminal(); let mut app = App::new(); - app.sidebar_index = 5; + app.sidebar_index = 4; app.toggle_menu(); terminal.draw(|f| ui(f, &mut app)).unwrap(); insta::assert_debug_snapshot!(terminal.backend()); @@ -217,7 +171,7 @@ mod tests { fn test_ln_status_screen_render() { let mut terminal = make_terminal(); let mut app = App::new(); - app.sidebar_index = 6; + app.sidebar_index = 5; app.toggle_menu(); terminal.draw(|f| ui(f, &mut app)).unwrap(); insta::assert_debug_snapshot!(terminal.backend()); @@ -227,7 +181,7 @@ mod tests { fn test_shares_market_screen_render() { let mut terminal = make_terminal(); let mut app = App::new(); - app.sidebar_index = 7; + app.sidebar_index = 6; app.toggle_menu(); terminal.draw(|f| ui(f, &mut app)).unwrap(); insta::assert_debug_snapshot!(terminal.backend()); @@ -236,12 +190,10 @@ mod tests { #[test] #[serial_test::serial] fn test_settings_screen_render() { - // Fix PDM_CONFIG_DIR so field 4 renders a deterministic path across platforms. - // SAFETY: serialised by #[serial] — no concurrent mutation of PDM_CONFIG_DIR. unsafe { std::env::set_var("PDM_CONFIG_DIR", "/pdm/test-config") }; let mut terminal = make_terminal(); let mut app = App::new(); - app.sidebar_index = 8; // Settings + app.sidebar_index = 7; app.toggle_menu(); terminal.draw(|f| ui(f, &mut app)).unwrap(); unsafe { std::env::remove_var("PDM_CONFIG_DIR") }; diff --git a/tests/fixtures/p2pool.toml b/tests/fixtures/p2pool.toml new file mode 100644 index 0000000..5b5304b --- /dev/null +++ b/tests/fixtures/p2pool.toml @@ -0,0 +1,48 @@ +[network] +listen_address = "/ip4/127.0.0.1/tcp/6884" +dial_peers = [] +max_pending_incoming = 10 +max_pending_outgoing = 10 +max_established_incoming = 50 +max_established_outgoing = 50 +max_established_per_peer = 1 +max_workbase_per_second = 10 +max_userworkbase_per_second = 10 +max_miningshare_per_second = 100 +max_inventory_per_second = 100 +max_transaction_per_second = 100 +max_requests_per_second = 100 +dial_timeout_secs = 30 + +[store] +path = "./store.db" +background_task_frequency_hours = 24 +pplns_ttl_days = 7 + +[stratum] +hostname = "pool.example.com" +port = 3333 +start_difficulty = 10000 +minimum_difficulty = 100 +solo_address = "tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk" +bootstrap_address = "tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk" +zmqpubhashblock = "tcp://127.0.0.1:28332" +network = "signet" +version_mask = "1fffe000" +difficulty_multiplier = 1.0 +pool_signature = "P2Poolv2" + +[bitcoinrpc] +url = "http://127.0.0.1:38332" +username = "p2pool" +password = "p2pool" + +[logging] +file = "./logs/p2pool.log" +console = true +level = "info" +stats_dir = "./logs/stats" + +[api] +hostname = "127.0.0.1" +port = 46884 diff --git a/tests/snapshots/ui_snapshots__config_screen_render.snap b/tests/snapshots/ui_snapshots__config_screen_render.snap index 288335a..ce4b250 100644 --- a/tests/snapshots/ui_snapshots__config_screen_render.snap +++ b/tests/snapshots/ui_snapshots__config_screen_render.snap @@ -6,14 +6,13 @@ TestBackend { buffer: Buffer { area: Rect { x: 0, y: 0, width: 80, height: 25 }, content: [ - "┌ PDM ──────────────────┐┌ Bitcoin Config ─────────────────────────────────────┐", - "│Home ││Press [Enter] to select a bitcoin.conf file │", - "│Bitcoin Config ││ │", + "┌ PDM ──────────────────┐┌ Info ───────────────────────────────────────────────┐", + "│Home ││ Chain Info │ Peers │", "│Bitcoin Status ││ │", - "│P2Pool Config ││ │", - "│P2Pool Status ││ │", - "│LN Config ││ │", - "│LN Status ││ │", + "│P2Pool Config │└─────────────────────────────────────────────────────┘", + "│P2Pool Status │┌ Chain Info ─────────────────────────────────────────┐", + "│LN Config ││Select a P2Poolv2 config file to load Bitcoin Core │", + "│LN Status ││chain info. │", "│Shares Market ││ │", "│Settings ││ │", "│ ││ │", @@ -29,20 +28,27 @@ TestBackend { "│ ││ │", "│ ││ │", "│ ││ │", + "│ ││ │", "└───────────────────────┘└─────────────────────────────────────────────────────┘", - " ↑↓ Navigate sidebar Enter Open file Esc Back ", + " ↑↓ Navigate sidebar ←→ Switch tab q Quit ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 27, y: 1, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 37, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 1, y: 2, fg: Black, bg: Gray, underline: Reset, modifier: NONE, x: 24, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 26, y: 5, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, + x: 76, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 26, y: 6, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, + x: 37, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 24, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, x: 4, y: 24, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, x: 23, y: 24, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, - x: 30, y: 24, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, - x: 42, y: 24, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, - x: 47, y: 24, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, - x: 54, y: 24, fg: Reset, bg: Black, underline: Reset, modifier: NONE, + x: 27, y: 24, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, + x: 40, y: 24, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, + x: 43, y: 24, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, + x: 50, y: 24, fg: Reset, bg: Black, underline: Reset, modifier: NONE, ] }, scrollback: Buffer { diff --git a/tests/snapshots/ui_snapshots__home_screen_render.snap b/tests/snapshots/ui_snapshots__home_screen_render.snap index 7349b4c..bca39c2 100644 --- a/tests/snapshots/ui_snapshots__home_screen_render.snap +++ b/tests/snapshots/ui_snapshots__home_screen_render.snap @@ -8,9 +8,8 @@ TestBackend { content: [ "┌ PDM ──────────────────┐┌ Home ───────────────────────────────────────────────┐", "│Home ││Welcome to PDM. │", - "│Bitcoin Config ││ │", - "│Bitcoin Status ││Select a config from the sidebar to edit. │", - "│P2Pool Config ││ │", + "│Bitcoin Status ││ │", + "│P2Pool Config ││Select a config from the sidebar to edit. │", "│P2Pool Status ││ │", "│LN Config ││ │", "│LN Status ││ │", @@ -29,6 +28,7 @@ TestBackend { "│ ││ │", "│ ││ │", "│ ││ │", + "│ ││ │", "└───────────────────────┘└─────────────────────────────────────────────────────┘", " ↑↓ Navigate sidebar Enter Select q Quit ", ], From 7d420f7525e1e77504880c6f50cfe02c78ee3452 Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Sun, 23 Aug 2026 14:11:37 +0000 Subject: [PATCH 2/4] wip: bitcoin config removal + test fixes --- src/bitcoin_config.rs | 1997 ------------------------- src/components/bitcoin_config_view.rs | 705 --------- src/components/bitcoin_status_view.rs | 2 +- src/main.rs | 10 +- src/ui.rs | 22 - 5 files changed, 6 insertions(+), 2730 deletions(-) delete mode 100644 src/bitcoin_config.rs delete mode 100644 src/components/bitcoin_config_view.rs diff --git a/src/bitcoin_config.rs b/src/bitcoin_config.rs deleted file mode 100644 index ff8fc9c..0000000 --- a/src/bitcoin_config.rs +++ /dev/null @@ -1,1997 +0,0 @@ -// SPDX-FileCopyrightText: 2024 PDM Authors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -use anyhow::Result; -use config::{Config, File, FileFormat}; -use std::{ - collections::{HashMap, HashSet}, - path::Path, -}; - -#[allow(dead_code)] -/// Core Config -#[derive(Debug, Clone)] -pub struct Core { - // Data directory and storage - pub datadir: Option, - pub blocksdir: Option, - pub pid: Option, - pub debuglogfile: Option, - pub settings: Option, - pub includeconf: Option, - pub loadblock: Option, - - // Indexing - pub txindex: Option, - pub blockfilterindex: Option, - pub coinstatsindex: Option, - - // Pruning - pub prune: Option, - - // Memory and performance - pub dbcache: Option, - pub maxmempool: Option, - pub maxorphantx: Option, - pub mempoolexpiry: Option, - pub par: Option, - pub blockreconstructionextratxn: Option, - - // Behavior - pub blocksonly: Option, - pub persistmempool: Option, - pub reindex: Option, - pub reindex_chainstate: Option, - pub sysperms: Option, - - // Daemon mode - pub daemon: Option, - pub daemonwait: Option, - - // Notification commands - pub alertnotify: Option, - pub blocknotify: Option, - pub startupnotify: Option, - - // Validation - pub assumevalid: Option, -} - -#[allow(dead_code)] -/// Network Config -#[derive(Debug, Clone)] -pub struct Network { - // Chain selection - pub chain: Option, - pub testnet: Option, - pub regtest: Option, - pub signet: Option, - pub signetchallenge: Option, - pub signetseednode: Option, - - // Listening and binding - pub listen: Option, - pub bind: Option, - pub whitebind: Option, - pub port: Option, - - // Connection limits - pub maxconnections: Option, - pub maxreceivebuffer: Option, - pub maxsendbuffer: Option, - pub maxuploadtarget: Option, - pub timeout: Option, - pub maxtimeadjustment: Option, - pub bantime: Option, - - // Peer discovery - pub discover: Option, - pub dns: Option, - pub dnsseed: Option, - pub fixedseeds: Option, - pub forcednsseed: Option, - pub seednode: Option, - pub addnode: Option, - pub connect: Option, - - // Network selection - pub onlynet: Option, - pub networkactive: Option, - - // Proxy settings - pub proxy: Option, - pub proxyrandomize: Option, - - // Tor settings - pub onion: Option, - pub listenonion: Option, - pub torcontrol: Option, - pub torpassword: Option, - - // I2P settings - pub i2psam: Option, - pub i2pacceptincoming: Option, - - // CJDNS - pub cjdnsreachable: Option, - - // Peer permissions - pub whitelist: Option, - pub peerblockfilters: Option, - pub peerbloomfilters: Option, - pub permitbaremultisig: Option, - - // External IP - pub externalip: Option, - - // UPnP - pub upnp: Option, - - // ASN mapping - pub asmap: Option, -} - -#[allow(dead_code)] -/// RPC Config -#[derive(Debug, Clone)] -pub struct RPC { - // Server enable - pub server: Option, - - // Authentication - pub rpcuser: Option, - pub rpcpassword: Option, - pub rpcauth: Option, - pub rpccookiefile: Option, - - // Connection - pub rpcport: Option, - pub rpcbind: Option, - pub rpcallowip: Option, - - // Performance - pub rpcthreads: Option, - - // Serialization - pub rpcserialversion: Option, - - // Whitelist - pub rpcwhitelist: Option, - pub rpcwhitelistdefault: Option, - - // REST interface - pub rest: Option, -} - -#[allow(dead_code)] -/// Wallet related config -#[derive(Debug, Clone)] -pub struct Wallet { - // Enable/disable - pub disablewallet: Option, - - // Wallet paths - pub wallet: Option, - pub walletdir: Option, - - // Address types - pub addresstype: Option, - pub changetype: Option, - - // Fee settings - pub fallbackfee: Option, - pub discardfee: Option, - pub mintxfee: Option, - pub paytxfee: Option, - pub consolidatefeerate: Option, - pub maxapsfee: Option, - - // Transaction behavior - pub txconfirmtarget: Option, - pub spendzeroconfchange: Option, - pub walletrbf: Option, - pub avoidpartialspends: Option, - - // Key management - pub keypool: Option, - - // External signer - pub signer: Option, - - // Broadcast - pub walletbroadcast: Option, - - // Notifications - pub walletnotify: Option, -} - -#[allow(dead_code)] -/// Debugging related config -#[derive(Debug, Clone)] -pub struct Debugging { - // Debug categories - pub debug: Option, - pub debugexclude: Option, - - // Logging options - pub logips: Option, - pub logsourcelocations: Option, - pub logthreadnames: Option, - pub logtimestamps: Option, - pub shrinkdebugfile: Option, - pub printtoconsole: Option, - - // User agent - pub uacomment: Option, - - // Fee limits - pub maxtxfee: Option, -} - -#[allow(dead_code)] -/// Mining related config -#[derive(Debug, Clone)] -pub struct Mining { - // Block creation - pub blockmaxweight: Option, - pub blockmintxfee: Option, -} - -#[allow(dead_code)] -/// Relay related config -#[derive(Debug, Clone)] -pub struct Relay { - // Relay fees - pub minrelaytxfee: Option, - - // Data carrier (OP_RETURN) - pub datacarrier: Option, - pub datacarriersize: Option, - - // Sigops - pub bytespersigop: Option, - - // Whitelist relay - pub whitelistforcerelay: Option, - pub whitelistrelay: Option, -} - -#[allow(dead_code)] -/// ZMQ related config -#[derive(Debug, Clone)] -pub struct ZMQ { - // Hash notifications - pub zmqpubhashblock: Option, - pub zmqpubhashtx: Option, - - // Raw data notifications - pub zmqpubrawblock: Option, - pub zmqpubrawtx: Option, - - // Sequence notifications - pub zmqpubsequence: Option, -} - -#[allow(dead_code)] -#[derive(Debug, Clone)] -pub struct BitcoinConfig { - pub core: Core, - pub network: Network, - pub rpc: RPC, - pub wallet: Wallet, - pub debugging: Debugging, - pub mining: Mining, - pub relay: Relay, - pub zmq: ZMQ, -} - -/// Type of a configuration option value -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ConfigType { - Bool, - Int, - Float, - String, - Path, - Address, -} - -impl std::fmt::Display for ConfigType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ConfigType::Bool => write!(f, "boolean"), - ConfigType::Int => write!(f, "integer"), - ConfigType::Float => write!(f, "float"), - ConfigType::String => write!(f, "string"), - ConfigType::Path => write!(f, "path"), - ConfigType::Address => write!(f, "address"), - } - } -} - -/// Category of a configuration option -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ConfigCategory { - Core, - Network, - RPC, - Wallet, - Debugging, - Mining, - Relay, - ZMQ, -} - -/// Schema for a single configuration option -#[derive(Debug, Clone)] -pub struct ConfigSchema { - pub key: String, - pub default: String, - pub config_type: ConfigType, - pub category: ConfigCategory, - pub description: String, -} - -impl ConfigSchema { - #[must_use] - pub fn new( - key: &str, - default: &str, - config_type: ConfigType, - category: ConfigCategory, - description: &str, - ) -> Self { - Self { - key: key.to_string(), - default: default.to_string(), - config_type, - category, - description: description.to_string(), - } - } -} - -/// A parsed configuration entry -#[derive(Debug, Clone)] -pub struct ConfigEntry { - pub key: String, - pub value: String, - pub schema: Option, - pub enabled: bool, - pub section: Option, -} - -/// Returns the default schema for all known bitcoin.conf options -#[must_use] -#[allow(clippy::too_many_lines)] -pub fn get_default_schema() -> Vec { - vec![ - // Core options - ConfigSchema::new( - "datadir", - "", - ConfigType::Path, - ConfigCategory::Core, - "Specify data directory", - ), - ConfigSchema::new( - "blocksdir", - "", - ConfigType::Path, - ConfigCategory::Core, - "Specify blocks directory", - ), - ConfigSchema::new( - "pid", - "", - ConfigType::Path, - ConfigCategory::Core, - "Specify pid file", - ), - ConfigSchema::new( - "debuglogfile", - "", - ConfigType::Path, - ConfigCategory::Core, - "Specify debug log file", - ), - ConfigSchema::new( - "settings", - "", - ConfigType::Path, - ConfigCategory::Core, - "Specify settings file", - ), - ConfigSchema::new( - "includeconf", - "", - ConfigType::Path, - ConfigCategory::Core, - "Include additional config file", - ), - ConfigSchema::new( - "loadblock", - "", - ConfigType::Path, - ConfigCategory::Core, - "Import blocks from external file", - ), - ConfigSchema::new( - "txindex", - "0", - ConfigType::Bool, - ConfigCategory::Core, - "Maintain full transaction index", - ), - ConfigSchema::new( - "blockfilterindex", - "", - ConfigType::String, - ConfigCategory::Core, - "Maintain compact block filter index", - ), - ConfigSchema::new( - "coinstatsindex", - "0", - ConfigType::Bool, - ConfigCategory::Core, - "Maintain coinstats index", - ), - ConfigSchema::new( - "prune", - "0", - ConfigType::Int, - ConfigCategory::Core, - "Reduce storage by pruning old blocks", - ), - ConfigSchema::new( - "dbcache", - "450", - ConfigType::Int, - ConfigCategory::Core, - "Database cache size in MiB", - ), - ConfigSchema::new( - "maxmempool", - "300", - ConfigType::Int, - ConfigCategory::Core, - "Maximum mempool size in MiB", - ), - ConfigSchema::new( - "maxorphantx", - "100", - ConfigType::Int, - ConfigCategory::Core, - "Maximum orphan transactions", - ), - ConfigSchema::new( - "mempoolexpiry", - "336", - ConfigType::Int, - ConfigCategory::Core, - "Mempool expiry in hours", - ), - ConfigSchema::new( - "par", - "0", - ConfigType::Int, - ConfigCategory::Core, - "Script verification threads", - ), - ConfigSchema::new( - "blockreconstructionextratxn", - "100", - ConfigType::Int, - ConfigCategory::Core, - "Extra transactions for block reconstruction", - ), - ConfigSchema::new( - "blocksonly", - "0", - ConfigType::Bool, - ConfigCategory::Core, - "Reject transactions from network peers", - ), - ConfigSchema::new( - "persistmempool", - "1", - ConfigType::Bool, - ConfigCategory::Core, - "Save mempool on shutdown", - ), - ConfigSchema::new( - "reindex", - "0", - ConfigType::Bool, - ConfigCategory::Core, - "Rebuild chain state and block index", - ), - ConfigSchema::new( - "reindex-chainstate", - "0", - ConfigType::Bool, - ConfigCategory::Core, - "Rebuild chain state from blocks", - ), - ConfigSchema::new( - "sysperms", - "0", - ConfigType::Bool, - ConfigCategory::Core, - "Create files with system default permissions", - ), - ConfigSchema::new( - "daemon", - "0", - ConfigType::Bool, - ConfigCategory::Core, - "Run in background as daemon", - ), - ConfigSchema::new( - "daemonwait", - "0", - ConfigType::Bool, - ConfigCategory::Core, - "Wait for initialization before backgrounding", - ), - ConfigSchema::new( - "alertnotify", - "", - ConfigType::String, - ConfigCategory::Core, - "Command to execute on alert", - ), - ConfigSchema::new( - "blocknotify", - "", - ConfigType::String, - ConfigCategory::Core, - "Command to execute on new block", - ), - ConfigSchema::new( - "startupnotify", - "", - ConfigType::String, - ConfigCategory::Core, - "Command to execute on startup", - ), - ConfigSchema::new( - "assumevalid", - "", - ConfigType::String, - ConfigCategory::Core, - "Assume blocks are valid up to this hash", - ), - // Network options - ConfigSchema::new( - "chain", - "main", - ConfigType::String, - ConfigCategory::Network, - "Chain to use (main, test, signet, regtest)", - ), - ConfigSchema::new( - "testnet", - "0", - ConfigType::Bool, - ConfigCategory::Network, - "Use testnet", - ), - ConfigSchema::new( - "regtest", - "0", - ConfigType::Bool, - ConfigCategory::Network, - "Use regtest", - ), - ConfigSchema::new( - "signet", - "0", - ConfigType::Bool, - ConfigCategory::Network, - "Use signet", - ), - ConfigSchema::new( - "signetchallenge", - "", - ConfigType::String, - ConfigCategory::Network, - "Signet challenge script", - ), - ConfigSchema::new( - "signetseednode", - "", - ConfigType::String, - ConfigCategory::Network, - "Signet seed node", - ), - ConfigSchema::new( - "listen", - "1", - ConfigType::Bool, - ConfigCategory::Network, - "Accept incoming connections", - ), - ConfigSchema::new( - "bind", - "", - ConfigType::Address, - ConfigCategory::Network, - "Bind to address", - ), - ConfigSchema::new( - "whitebind", - "", - ConfigType::Address, - ConfigCategory::Network, - "Bind with whitelist permissions", - ), - ConfigSchema::new( - "port", - "8333", - ConfigType::Int, - ConfigCategory::Network, - "Listen on port", - ), - ConfigSchema::new( - "maxconnections", - "125", - ConfigType::Int, - ConfigCategory::Network, - "Maximum peer connections", - ), - ConfigSchema::new( - "maxreceivebuffer", - "5000", - ConfigType::Int, - ConfigCategory::Network, - "Maximum receive buffer per connection", - ), - ConfigSchema::new( - "maxsendbuffer", - "1000", - ConfigType::Int, - ConfigCategory::Network, - "Maximum send buffer per connection", - ), - ConfigSchema::new( - "maxuploadtarget", - "0", - ConfigType::Int, - ConfigCategory::Network, - "Maximum upload target in MiB per day", - ), - ConfigSchema::new( - "timeout", - "5000", - ConfigType::Int, - ConfigCategory::Network, - "Connection timeout in milliseconds", - ), - ConfigSchema::new( - "maxtimeadjustment", - "4200", - ConfigType::Int, - ConfigCategory::Network, - "Maximum time adjustment in seconds", - ), - ConfigSchema::new( - "bantime", - "86400", - ConfigType::Int, - ConfigCategory::Network, - "Ban duration in seconds", - ), - ConfigSchema::new( - "discover", - "1", - ConfigType::Bool, - ConfigCategory::Network, - "Discover own IP address", - ), - ConfigSchema::new( - "dns", - "1", - ConfigType::Bool, - ConfigCategory::Network, - "Allow DNS lookups", - ), - ConfigSchema::new( - "dnsseed", - "1", - ConfigType::Bool, - ConfigCategory::Network, - "Query DNS seeds", - ), - ConfigSchema::new( - "fixedseeds", - "1", - ConfigType::Bool, - ConfigCategory::Network, - "Use fixed seeds if DNS fails", - ), - ConfigSchema::new( - "forcednsseed", - "0", - ConfigType::Bool, - ConfigCategory::Network, - "Always query DNS seeds", - ), - ConfigSchema::new( - "seednode", - "", - ConfigType::Address, - ConfigCategory::Network, - "Connect to seed node for addresses", - ), - ConfigSchema::new( - "addnode", - "", - ConfigType::Address, - ConfigCategory::Network, - "Add node to connect to", - ), - ConfigSchema::new( - "connect", - "", - ConfigType::Address, - ConfigCategory::Network, - "Connect only to specified node", - ), - ConfigSchema::new( - "onlynet", - "", - ConfigType::String, - ConfigCategory::Network, - "Only connect to network type", - ), - ConfigSchema::new( - "networkactive", - "1", - ConfigType::Bool, - ConfigCategory::Network, - "Enable network activity", - ), - ConfigSchema::new( - "proxy", - "", - ConfigType::Address, - ConfigCategory::Network, - "SOCKS5 proxy", - ), - ConfigSchema::new( - "proxyrandomize", - "1", - ConfigType::Bool, - ConfigCategory::Network, - "Randomize proxy credentials", - ), - ConfigSchema::new( - "onion", - "", - ConfigType::Address, - ConfigCategory::Network, - "SOCKS5 proxy for Tor", - ), - ConfigSchema::new( - "listenonion", - "1", - ConfigType::Bool, - ConfigCategory::Network, - "Create Tor onion service", - ), - ConfigSchema::new( - "torcontrol", - "127.0.0.1:9051", - ConfigType::Address, - ConfigCategory::Network, - "Tor control port", - ), - ConfigSchema::new( - "torpassword", - "", - ConfigType::String, - ConfigCategory::Network, - "Tor control password", - ), - ConfigSchema::new( - "i2psam", - "", - ConfigType::Address, - ConfigCategory::Network, - "I2P SAM proxy", - ), - ConfigSchema::new( - "i2pacceptincoming", - "1", - ConfigType::Bool, - ConfigCategory::Network, - "Accept incoming I2P connections", - ), - ConfigSchema::new( - "cjdnsreachable", - "0", - ConfigType::Bool, - ConfigCategory::Network, - "CJDNS reachable", - ), - ConfigSchema::new( - "whitelist", - "", - ConfigType::String, - ConfigCategory::Network, - "Whitelist peers", - ), - ConfigSchema::new( - "peerblockfilters", - "0", - ConfigType::Bool, - ConfigCategory::Network, - "Serve compact block filters", - ), - ConfigSchema::new( - "peerbloomfilters", - "0", - ConfigType::Bool, - ConfigCategory::Network, - "Support bloom filters", - ), - ConfigSchema::new( - "permitbaremultisig", - "1", - ConfigType::Bool, - ConfigCategory::Network, - "Relay bare multisig", - ), - ConfigSchema::new( - "externalip", - "", - ConfigType::Address, - ConfigCategory::Network, - "Specify external IP", - ), - ConfigSchema::new( - "upnp", - "0", - ConfigType::Bool, - ConfigCategory::Network, - "Use UPnP for port mapping", - ), - ConfigSchema::new( - "asmap", - "", - ConfigType::Path, - ConfigCategory::Network, - "ASN mapping file", - ), - // RPC options - ConfigSchema::new( - "server", - "0", - ConfigType::Bool, - ConfigCategory::RPC, - "Accept RPC commands", - ), - ConfigSchema::new( - "rpcuser", - "", - ConfigType::String, - ConfigCategory::RPC, - "RPC username", - ), - ConfigSchema::new( - "rpcpassword", - "", - ConfigType::String, - ConfigCategory::RPC, - "RPC password", - ), - ConfigSchema::new( - "rpcauth", - "", - ConfigType::String, - ConfigCategory::RPC, - "RPC auth credentials", - ), - ConfigSchema::new( - "rpccookiefile", - "", - ConfigType::Path, - ConfigCategory::RPC, - "RPC cookie file location", - ), - ConfigSchema::new( - "rpcport", - "8332", - ConfigType::Int, - ConfigCategory::RPC, - "RPC port", - ), - ConfigSchema::new( - "rpcbind", - "", - ConfigType::Address, - ConfigCategory::RPC, - "RPC bind address", - ), - ConfigSchema::new( - "rpcallowip", - "", - ConfigType::String, - ConfigCategory::RPC, - "Allow RPC from IP", - ), - ConfigSchema::new( - "rpcthreads", - "4", - ConfigType::Int, - ConfigCategory::RPC, - "RPC worker threads", - ), - ConfigSchema::new( - "rpcserialversion", - "1", - ConfigType::Int, - ConfigCategory::RPC, - "RPC serialization version", - ), - ConfigSchema::new( - "rpcwhitelist", - "", - ConfigType::String, - ConfigCategory::RPC, - "RPC method whitelist", - ), - ConfigSchema::new( - "rpcwhitelistdefault", - "1", - ConfigType::Bool, - ConfigCategory::RPC, - "Default RPC whitelist behavior", - ), - ConfigSchema::new( - "rest", - "0", - ConfigType::Bool, - ConfigCategory::RPC, - "Enable REST interface", - ), - // Wallet options - ConfigSchema::new( - "disablewallet", - "0", - ConfigType::Bool, - ConfigCategory::Wallet, - "Disable wallet", - ), - ConfigSchema::new( - "wallet", - "", - ConfigType::Path, - ConfigCategory::Wallet, - "Wallet to load", - ), - ConfigSchema::new( - "walletdir", - "", - ConfigType::Path, - ConfigCategory::Wallet, - "Wallet directory", - ), - ConfigSchema::new( - "addresstype", - "bech32", - ConfigType::String, - ConfigCategory::Wallet, - "Default address type", - ), - ConfigSchema::new( - "changetype", - "", - ConfigType::String, - ConfigCategory::Wallet, - "Change address type", - ), - ConfigSchema::new( - "fallbackfee", - "0.00", - ConfigType::Float, - ConfigCategory::Wallet, - "Fallback fee rate", - ), - ConfigSchema::new( - "discardfee", - "0.0001", - ConfigType::Float, - ConfigCategory::Wallet, - "Discard fee threshold", - ), - ConfigSchema::new( - "mintxfee", - "0.00001", - ConfigType::Float, - ConfigCategory::Wallet, - "Minimum transaction fee", - ), - ConfigSchema::new( - "paytxfee", - "0.00", - ConfigType::Float, - ConfigCategory::Wallet, - "Transaction fee rate", - ), - ConfigSchema::new( - "consolidatefeerate", - "0.0001", - ConfigType::Float, - ConfigCategory::Wallet, - "Consolidation fee rate", - ), - ConfigSchema::new( - "maxapsfee", - "0.00", - ConfigType::Float, - ConfigCategory::Wallet, - "Max fee for partial spend avoidance", - ), - ConfigSchema::new( - "txconfirmtarget", - "6", - ConfigType::Int, - ConfigCategory::Wallet, - "Confirmation target blocks", - ), - ConfigSchema::new( - "spendzeroconfchange", - "1", - ConfigType::Bool, - ConfigCategory::Wallet, - "Spend unconfirmed change", - ), - ConfigSchema::new( - "walletrbf", - "0", - ConfigType::Bool, - ConfigCategory::Wallet, - "Enable wallet RBF", - ), - ConfigSchema::new( - "avoidpartialspends", - "0", - ConfigType::Bool, - ConfigCategory::Wallet, - "Avoid partial spends", - ), - ConfigSchema::new( - "keypool", - "1000", - ConfigType::Int, - ConfigCategory::Wallet, - "Keypool size", - ), - ConfigSchema::new( - "signer", - "", - ConfigType::String, - ConfigCategory::Wallet, - "External signer command", - ), - ConfigSchema::new( - "walletbroadcast", - "1", - ConfigType::Bool, - ConfigCategory::Wallet, - "Broadcast wallet transactions", - ), - ConfigSchema::new( - "walletnotify", - "", - ConfigType::String, - ConfigCategory::Wallet, - "Command on wallet transaction", - ), - // Debugging options - ConfigSchema::new( - "debug", - "", - ConfigType::String, - ConfigCategory::Debugging, - "Debug categories", - ), - ConfigSchema::new( - "debugexclude", - "", - ConfigType::String, - ConfigCategory::Debugging, - "Exclude debug categories", - ), - ConfigSchema::new( - "logips", - "0", - ConfigType::Bool, - ConfigCategory::Debugging, - "Log IP addresses", - ), - ConfigSchema::new( - "logsourcelocations", - "0", - ConfigType::Bool, - ConfigCategory::Debugging, - "Log source locations", - ), - ConfigSchema::new( - "logthreadnames", - "0", - ConfigType::Bool, - ConfigCategory::Debugging, - "Log thread names", - ), - ConfigSchema::new( - "logtimestamps", - "1", - ConfigType::Bool, - ConfigCategory::Debugging, - "Log timestamps", - ), - ConfigSchema::new( - "shrinkdebugfile", - "1", - ConfigType::Bool, - ConfigCategory::Debugging, - "Shrink debug.log on startup", - ), - ConfigSchema::new( - "printtoconsole", - "0", - ConfigType::Bool, - ConfigCategory::Debugging, - "Print to console", - ), - ConfigSchema::new( - "uacomment", - "", - ConfigType::String, - ConfigCategory::Debugging, - "User agent comment", - ), - ConfigSchema::new( - "maxtxfee", - "0.10", - ConfigType::Float, - ConfigCategory::Debugging, - "Maximum transaction fee", - ), - // Mining options - ConfigSchema::new( - "blockmaxweight", - "3996000", - ConfigType::Int, - ConfigCategory::Mining, - "Maximum block weight", - ), - ConfigSchema::new( - "blockmintxfee", - "0.00001", - ConfigType::Float, - ConfigCategory::Mining, - "Minimum block transaction fee", - ), - // Relay options - ConfigSchema::new( - "minrelaytxfee", - "0.00001", - ConfigType::Float, - ConfigCategory::Relay, - "Minimum relay fee", - ), - ConfigSchema::new( - "datacarrier", - "1", - ConfigType::Bool, - ConfigCategory::Relay, - "Relay OP_RETURN transactions", - ), - ConfigSchema::new( - "datacarriersize", - "83", - ConfigType::Int, - ConfigCategory::Relay, - "Maximum OP_RETURN size", - ), - ConfigSchema::new( - "bytespersigop", - "20", - ConfigType::Int, - ConfigCategory::Relay, - "Bytes per sigop", - ), - ConfigSchema::new( - "whitelistforcerelay", - "0", - ConfigType::Bool, - ConfigCategory::Relay, - "Force relay from whitelist", - ), - ConfigSchema::new( - "whitelistrelay", - "1", - ConfigType::Bool, - ConfigCategory::Relay, - "Relay from whitelist", - ), - // ZMQ options - ConfigSchema::new( - "zmqpubhashblock", - "", - ConfigType::Address, - ConfigCategory::ZMQ, - "ZMQ hash block publisher", - ), - ConfigSchema::new( - "zmqpubhashtx", - "", - ConfigType::Address, - ConfigCategory::ZMQ, - "ZMQ hash tx publisher", - ), - ConfigSchema::new( - "zmqpubrawblock", - "", - ConfigType::Address, - ConfigCategory::ZMQ, - "ZMQ raw block publisher", - ), - ConfigSchema::new( - "zmqpubrawtx", - "", - ConfigType::Address, - ConfigCategory::ZMQ, - "ZMQ raw tx publisher", - ), - ConfigSchema::new( - "zmqpubsequence", - "", - ConfigType::Address, - ConfigCategory::ZMQ, - "ZMQ sequence publisher", - ), - ] -} - -/// Parse bitcoin.conf file -/// -/// # Errors -/// Returns an error if the file cannot be read or the config library fails to build. -/// On a parse failure the function returns schema defaults rather than an error. -#[allow(clippy::too_many_lines)] // Sequential key-mapping logic; refactoring adds no clarity -pub fn parse_config(path: &Path) -> Result> { - let schema_list = get_default_schema(); - let mut entries = Vec::new(); - let mut found_keys: HashSet = HashSet::new(); - let mut builder = Config::builder(); - - if path.exists() { - builder = builder.add_source(File::from(path).format(FileFormat::Ini)); - } - - let Ok(config) = builder.build() else { - // Return schema defaults if config can't be parsed - for schema in schema_list { - entries.push(ConfigEntry { - key: schema.key.clone(), - value: schema.default.clone(), - schema: Some(schema), - enabled: false, - section: None, - }); - } - return Ok(entries); - }; - - // Maps key name -> section it was first seen in (None = top-level) - let mut config_keys: HashMap> = HashMap::new(); - let sections = vec!["", "main", "test", "signet", "regtest"]; - - // Collect all keys from all sections, preserving which section each key came from - for section in §ions { - if let Ok(table) = if section.is_empty() { - config.get_table("") - } else { - config.get_table(section) - } { - for key in table.keys() { - let actual_key = if key.contains('.') { - key.split('.').next_back().unwrap_or(key).to_string() - } else { - key.clone() - }; - let key_section = if section.is_empty() { - None - } else { - Some((*section).to_string()) - }; - config_keys.entry(actual_key).or_insert(key_section); - } - } - } - - // Process known schema options - for schema in &schema_list { - let key = &schema.key; - let mut value = schema.default.clone(); - let mut enabled = false; - let mut entry_section: Option = None; - - 'find_section: for section in §ions { - let lookup_key = if section.is_empty() { - key.clone() - } else { - format!("{section}.{key}") - }; - - let resolved = if let Ok(val) = config.get_string(&lookup_key) { - Some(val) - } else if let Ok(val) = config.get_bool(&lookup_key) { - Some(if val { - "1".to_string() - } else { - "0".to_string() - }) - } else if let Ok(val) = config.get_int(&lookup_key) { - Some(val.to_string()) - } else if let Ok(val) = config.get_float(&lookup_key) { - Some(val.to_string()) - } else { - None - }; - - if let Some(v) = resolved { - value = v; - enabled = true; - found_keys.insert(key.clone()); - entry_section = if section.is_empty() { - None - } else { - Some((*section).to_string()) - }; - break 'find_section; - } - } - - entries.push(ConfigEntry { - key: key.clone(), - value, - schema: Some(schema.clone()), - enabled, - section: entry_section, - }); - } - - // Add unknown config keys (not in schema) - for (config_key, key_section) in &config_keys { - if !found_keys.contains(config_key) { - let lookup_key = match key_section { - None => config_key.clone(), - Some(s) => format!("{s}.{config_key}"), - }; - - let value = if let Ok(val) = config.get_string(&lookup_key) { - val - } else if let Ok(val) = config.get_bool(&lookup_key) { - if val { - "1".to_string() - } else { - "0".to_string() - } - } else if let Ok(val) = config.get_int(&lookup_key) { - val.to_string() - } else if let Ok(val) = config.get_float(&lookup_key) { - val.to_string() - } else { - String::new() - }; - - entries.push(ConfigEntry { - key: config_key.clone(), - value, - schema: None, - enabled: true, - section: key_section.clone(), - }); - } - } - - Ok(entries) -} - -/// Writes enabled entries back to the config file -/// -/// # Errors -/// Returns an error if the file cannot be created or written. -pub fn save_config(path: &Path, entries: &[ConfigEntry]) -> Result<()> { - use std::collections::BTreeMap; - use std::io::Write; - - let mut file = std::fs::File::create(path)?; - let mut sectioned: BTreeMap> = BTreeMap::new(); - - for entry in entries { - if !entry.enabled { - continue; - } - match &entry.section { - None => writeln!(file, "{}={}", entry.key, entry.value)?, - Some(s) => sectioned.entry(s.clone()).or_default().push(entry), - } - } - - // Write each named section - for (section, section_entries) in §ioned { - writeln!(file, "\n[{section}]")?; - for entry in section_entries { - writeln!(file, "{}={}", entry.key, entry.value)?; - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Write; - - fn create_temp_config(content: &str) -> (tempfile::TempDir, std::path::PathBuf) { - let dir = tempfile::tempdir().unwrap(); - let file_path = dir.path().join("bitcoin.conf"); - let mut file = std::fs::File::create(&file_path).unwrap(); - file.write_all(content.as_bytes()).unwrap(); - (dir, file_path) - } - - // Tests for get_default_schema() - - #[test] - fn get_default_schema_returns_non_empty_list() { - let schema = get_default_schema(); - assert!(!schema.is_empty()); - } - - #[test] - fn get_default_schema_contains_core_options() { - let schema = get_default_schema(); - let keys: Vec<&str> = schema.iter().map(|s| s.key.as_str()).collect(); - - assert!(keys.contains(&"datadir")); - assert!(keys.contains(&"txindex")); - assert!(keys.contains(&"prune")); - assert!(keys.contains(&"dbcache")); - } - - #[test] - fn get_default_schema_contains_network_options() { - let schema = get_default_schema(); - let keys: Vec<&str> = schema.iter().map(|s| s.key.as_str()).collect(); - - assert!(keys.contains(&"testnet")); - assert!(keys.contains(&"regtest")); - assert!(keys.contains(&"listen")); - assert!(keys.contains(&"port")); - assert!(keys.contains(&"maxconnections")); - } - - #[test] - fn get_default_schema_contains_rpc_options() { - let schema = get_default_schema(); - let keys: Vec<&str> = schema.iter().map(|s| s.key.as_str()).collect(); - - assert!(keys.contains(&"server")); - assert!(keys.contains(&"rpcuser")); - assert!(keys.contains(&"rpcpassword")); - assert!(keys.contains(&"rpcport")); - } - - #[test] - fn get_default_schema_contains_zmq_options() { - let schema = get_default_schema(); - let keys: Vec<&str> = schema.iter().map(|s| s.key.as_str()).collect(); - - assert!(keys.contains(&"zmqpubhashblock")); - assert!(keys.contains(&"zmqpubhashtx")); - assert!(keys.contains(&"zmqpubrawblock")); - assert!(keys.contains(&"zmqpubrawtx")); - assert!(keys.contains(&"zmqpubsequence")); - } - - #[test] - fn get_default_schema_has_correct_categories() { - let schema = get_default_schema(); - - let txindex = schema.iter().find(|s| s.key == "txindex").unwrap(); - assert_eq!(txindex.category, ConfigCategory::Core); - - let testnet = schema.iter().find(|s| s.key == "testnet").unwrap(); - assert_eq!(testnet.category, ConfigCategory::Network); - - let server = schema.iter().find(|s| s.key == "server").unwrap(); - assert_eq!(server.category, ConfigCategory::RPC); - - let disablewallet = schema.iter().find(|s| s.key == "disablewallet").unwrap(); - assert_eq!(disablewallet.category, ConfigCategory::Wallet); - } - - #[test] - fn get_default_schema_has_correct_types() { - let schema = get_default_schema(); - - let txindex = schema.iter().find(|s| s.key == "txindex").unwrap(); - assert_eq!(txindex.config_type, ConfigType::Bool); - - let dbcache = schema.iter().find(|s| s.key == "dbcache").unwrap(); - assert_eq!(dbcache.config_type, ConfigType::Int); - - let fallbackfee = schema.iter().find(|s| s.key == "fallbackfee").unwrap(); - assert_eq!(fallbackfee.config_type, ConfigType::Float); - - let datadir = schema.iter().find(|s| s.key == "datadir").unwrap(); - assert_eq!(datadir.config_type, ConfigType::Path); - - let rpcbind = schema.iter().find(|s| s.key == "rpcbind").unwrap(); - assert_eq!(rpcbind.config_type, ConfigType::Address); - } - - // Tests for ConfigSchema::new() - - #[test] - fn config_schema_new_creates_correct_schema() { - let schema = ConfigSchema::new( - "testkey", - "testdefault", - ConfigType::String, - ConfigCategory::Core, - "Test description", - ); - - assert_eq!(schema.key, "testkey"); - assert_eq!(schema.default, "testdefault"); - assert_eq!(schema.config_type, ConfigType::String); - assert_eq!(schema.category, ConfigCategory::Core); - assert_eq!(schema.description, "Test description"); - } - - // Tests for parse_config() - - #[test] - fn parse_config_non_existent_file_returns_defaults() { - let path = Path::new("/non/existent/path/bitcoin.conf"); - let entries = parse_config(path).unwrap(); - - assert!(!entries.is_empty()); - - // All entries should have schema and be disabled - for entry in &entries { - assert!(entry.schema.is_some()); - assert!(!entry.enabled); - } - } - - #[test] - fn parse_config_malformed_ini_returns_schema_defaults() { - // An unclosed section bracket causes the config crate's INI parser to - // return Err, triggering the `let Ok(config) = ... else { return Ok(entries) }` - // fallback path in parse_config. - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("bitcoin.conf"); - std::fs::write(&path, b"[unclosed\n").unwrap(); - - let entries = parse_config(&path).unwrap(); - - // Must return schema-populated defaults, all disabled - assert!(!entries.is_empty()); - let disabled_with_schema = entries - .iter() - .filter(|e| e.schema.is_some() && !e.enabled) - .count(); - // If the parser actually fails, ALL schema entries are disabled defaults. - assert!(disabled_with_schema > 0 || entries.iter().any(|e| e.schema.is_some())); - } - - #[test] - fn parse_config_empty_file_returns_defaults() { - let (_dir, path) = create_temp_config(""); - let entries = parse_config(&path).unwrap(); - - assert!(!entries.is_empty()); - - // All entries should be disabled (not set in config) - let enabled_count = entries.iter().filter(|e| e.enabled).count(); - assert_eq!(enabled_count, 0); - } - - #[test] - fn parse_config_parses_bool_values() { - let (_dir, path) = create_temp_config("txindex=1\nserver=0\n"); - let entries = parse_config(&path).unwrap(); - - let txindex = entries.iter().find(|e| e.key == "txindex").unwrap(); - assert_eq!(txindex.value, "1"); - assert!(txindex.enabled); - - let server = entries.iter().find(|e| e.key == "server").unwrap(); - assert_eq!(server.value, "0"); - assert!(server.enabled); - } - - #[test] - fn parse_config_parses_int_values() { - let (_dir, path) = create_temp_config("dbcache=1000\nport=8334\n"); - let entries = parse_config(&path).unwrap(); - - let dbcache = entries.iter().find(|e| e.key == "dbcache").unwrap(); - assert_eq!(dbcache.value, "1000"); - assert!(dbcache.enabled); - - let port = entries.iter().find(|e| e.key == "port").unwrap(); - assert_eq!(port.value, "8334"); - assert!(port.enabled); - } - - #[test] - fn parse_config_parses_string_values() { - let (_dir, path) = create_temp_config("rpcuser=myuser\nrpcpassword=mypassword\n"); - let entries = parse_config(&path).unwrap(); - - let rpcuser = entries.iter().find(|e| e.key == "rpcuser").unwrap(); - assert_eq!(rpcuser.value, "myuser"); - assert!(rpcuser.enabled); - - let rpcpassword = entries.iter().find(|e| e.key == "rpcpassword").unwrap(); - assert_eq!(rpcpassword.value, "mypassword"); - assert!(rpcpassword.enabled); - } - - #[test] - fn parse_config_parses_path_values() { - let (_dir, path) = create_temp_config("datadir=/home/user/.bitcoin\n"); - let entries = parse_config(&path).unwrap(); - - let datadir = entries.iter().find(|e| e.key == "datadir").unwrap(); - assert_eq!(datadir.value, "/home/user/.bitcoin"); - assert!(datadir.enabled); - } - - #[test] - fn parse_config_parses_address_values() { - let (_dir, path) = create_temp_config("zmqpubhashblock=tcp://127.0.0.1:28332\n"); - let entries = parse_config(&path).unwrap(); - - let zmq = entries.iter().find(|e| e.key == "zmqpubhashblock").unwrap(); - assert_eq!(zmq.value, "tcp://127.0.0.1:28332"); - assert!(zmq.enabled); - } - - #[test] - fn parse_config_handles_unknown_keys() { - // Use a section to ensure the config crate parses the key properly - let (_dir, path) = create_temp_config("[main]\nunknownkey=unknownvalue\n"); - let entries = parse_config(&path).unwrap(); - - let unknown = entries.iter().find(|e| e.key == "unknownkey"); - assert!( - unknown.is_some(), - "Unknown key should be present in entries" - ); - - let unknown = unknown.unwrap(); - assert_eq!(unknown.value, "unknownvalue"); - assert!(unknown.enabled); - assert!(unknown.schema.is_none()); - } - - #[test] - fn parse_config_handles_section_values() { - let content = r#" -[main] -rpcport=8332 - -[test] -rpcport=18332 -"#; - let (_dir, path) = create_temp_config(content); - let entries = parse_config(&path).unwrap(); - - // Should find rpcport with first matching section value - let rpcport = entries.iter().find(|e| e.key == "rpcport").unwrap(); - assert!(rpcport.enabled); - } - - #[test] - fn parse_config_preserves_schema_info() { - let (_dir, path) = create_temp_config("txindex=1\n"); - let entries = parse_config(&path).unwrap(); - - let txindex = entries.iter().find(|e| e.key == "txindex").unwrap(); - assert!(txindex.schema.is_some()); - - let schema = txindex.schema.as_ref().unwrap(); - assert_eq!(schema.config_type, ConfigType::Bool); - assert_eq!(schema.category, ConfigCategory::Core); - assert!(!schema.description.is_empty()); - } - - #[test] - fn parse_config_uses_defaults_for_unset_options() { - let (_dir, path) = create_temp_config("txindex=1\n"); - let entries = parse_config(&path).unwrap(); - - // dbcache should have default value since not set - let dbcache = entries.iter().find(|e| e.key == "dbcache").unwrap(); - assert_eq!(dbcache.value, "450"); // default value - assert!(!dbcache.enabled); - } - - #[test] - fn parse_config_handles_comments() { - let content = r#" -# This is a comment -txindex=1 -# Another comment -server=1 -"#; - let (_dir, path) = create_temp_config(content); - let entries = parse_config(&path).unwrap(); - - let txindex = entries.iter().find(|e| e.key == "txindex").unwrap(); - assert_eq!(txindex.value, "1"); - assert!(txindex.enabled); - - let server = entries.iter().find(|e| e.key == "server").unwrap(); - assert_eq!(server.value, "1"); - assert!(server.enabled); - } - - #[test] - fn parse_config_handles_full_config() { - let content = r#" -# Bitcoin Core configuration - -# Network -testnet=0 -listen=1 -port=8333 -maxconnections=125 - -# RPC -server=1 -rpcuser=bitcoinrpc -rpcpassword=secretpassword -rpcport=8332 -rpcallowip=127.0.0.1 - -# Wallet -disablewallet=0 -fallbackfee=0.0002 - -# ZMQ -zmqpubhashblock=tcp://127.0.0.1:28332 -zmqpubhashtx=tcp://127.0.0.1:28333 -"#; - let (_dir, path) = create_temp_config(content); - let entries = parse_config(&path).unwrap(); - - // Verify various entries - let testnet = entries.iter().find(|e| e.key == "testnet").unwrap(); - assert_eq!(testnet.value, "0"); - assert!(testnet.enabled); - - let rpcuser = entries.iter().find(|e| e.key == "rpcuser").unwrap(); - assert_eq!(rpcuser.value, "bitcoinrpc"); - - let zmq = entries.iter().find(|e| e.key == "zmqpubhashblock").unwrap(); - assert_eq!(zmq.value, "tcp://127.0.0.1:28332"); - } - - // Tests for ConfigType and ConfigCategory enums - - #[test] - fn config_type_is_copy() { - let t1 = ConfigType::Bool; - let t2 = t1; // Copy - assert_eq!(t1, t2); - } - - #[test] - fn config_category_is_copy() { - let c1 = ConfigCategory::Core; - let c2 = c1; // Copy - assert_eq!(c1, c2); - } - - #[test] - fn config_entry_clone_works() { - let entry = ConfigEntry { - key: "test".to_string(), - value: "value".to_string(), - schema: None, - enabled: true, - section: None, - }; - let cloned = entry.clone(); - assert_eq!(entry.key, cloned.key); - assert_eq!(entry.value, cloned.value); - assert_eq!(entry.enabled, cloned.enabled); - } - - #[test] - fn config_schema_clone_works() { - let schema = ConfigSchema::new( - "test", - "default", - ConfigType::String, - ConfigCategory::Core, - "description", - ); - let cloned = schema.clone(); - assert_eq!(schema.key, cloned.key); - assert_eq!(schema.default, cloned.default); - assert_eq!(schema.config_type, cloned.config_type); - assert_eq!(schema.category, cloned.category); - assert_eq!(schema.description, cloned.description); - } - - // Tests for save_config() - - #[test] - fn save_config_writes_only_enabled_entries() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("out.conf"); - - let entries = vec![ - ConfigEntry { - key: "rpcuser".to_string(), - value: "alice".to_string(), - enabled: true, - schema: None, - section: None, - }, - ConfigEntry { - key: "rpcport".to_string(), - value: "8332".to_string(), - enabled: false, - schema: None, - section: None, - }, - ConfigEntry { - key: "server".to_string(), - value: "1".to_string(), - enabled: true, - schema: None, - section: None, - }, - ]; - - save_config(&path, &entries).unwrap(); - - let content = std::fs::read_to_string(&path).unwrap(); - assert!(content.contains("rpcuser=alice\n")); - assert!(content.contains("server=1\n")); - assert!(!content.contains("rpcport")); - } - - #[test] - fn save_config_empty_entries_creates_empty_file() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("empty.conf"); - - save_config(&path, &[]).unwrap(); - - let content = std::fs::read_to_string(&path).unwrap(); - assert!(content.is_empty()); - } - - #[test] - fn save_config_roundtrip_with_parse() { - let (_dir, path) = create_temp_config("rpcuser=bob\nserver=1\n"); - - let entries = parse_config(&path).unwrap(); - save_config(&path, &entries).unwrap(); - - let reparsed = parse_config(&path).unwrap(); - let enabled: Vec<_> = reparsed.iter().filter(|e| e.enabled).collect(); - - assert!( - enabled - .iter() - .any(|e| e.key == "rpcuser" && e.value == "bob") - ); - assert!(enabled.iter().any(|e| e.key == "server" && e.value == "1")); - - // No disabled entry should have been promoted to enabled by the round-trip - let originally_disabled_count = entries.iter().filter(|e| !e.enabled).count(); - let after_disabled_count = reparsed.iter().filter(|e| !e.enabled).count(); - assert_eq!( - originally_disabled_count, after_disabled_count, - "round-trip must not enable previously-disabled entries" - ); - - // No extra enabled entries should appear - let originally_enabled_count = entries.iter().filter(|e| e.enabled).count(); - assert_eq!( - enabled.len(), - originally_enabled_count, - "round-trip must not introduce extra enabled entries" - ); - } - - #[test] - fn save_config_preserves_sections() { - // A config with keys in different sections - let (_dir, path) = create_temp_config("[main]\nrpcuser=alice\n\n[test]\nrpcport=18332\n"); - - let entries = parse_config(&path).unwrap(); - - // Verify sections were captured during parse - let main_entry = entries.iter().find(|e| e.key == "rpcuser" && e.enabled); - let test_entry = entries.iter().find(|e| e.key == "rpcport" && e.enabled); - assert!(main_entry.is_some(), "rpcuser should be parsed"); - assert!(test_entry.is_some(), "rpcport should be parsed"); - assert_eq!(main_entry.unwrap().section.as_deref(), Some("main")); - assert_eq!(test_entry.unwrap().section.as_deref(), Some("test")); - - // Save then re-parse - save_config(&path, &entries).unwrap(); - let saved = std::fs::read_to_string(&path).unwrap(); - assert!(saved.contains("[main]"), "expected [main] section header"); - assert!(saved.contains("[test]"), "expected [test] section header"); - - let reparsed = parse_config(&path).unwrap(); - let rpcuser = reparsed - .iter() - .find(|e| e.key == "rpcuser" && e.enabled) - .unwrap(); - let rpcport = reparsed - .iter() - .find(|e| e.key == "rpcport" && e.enabled) - .unwrap(); - assert_eq!(rpcuser.value, "alice"); - assert_eq!(rpcport.value, "18332"); - assert_eq!(rpcuser.section.as_deref(), Some("main")); - assert_eq!(rpcport.section.as_deref(), Some("test")); - } - - #[test] - fn save_config_sections_written_after_top_level() { - // Entries with mixed sections: top-level first, then named sections - let entries = vec![ - ConfigEntry { - key: "daemon".to_string(), - value: "1".to_string(), - enabled: true, - schema: None, - section: None, - }, - ConfigEntry { - key: "rpcport".to_string(), - value: "18332".to_string(), - enabled: true, - schema: None, - section: Some("test".to_string()), - }, - ]; - - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("bitcoin.conf"); - save_config(&path, &entries).unwrap(); - - let content = std::fs::read_to_string(&path).unwrap(); - let daemon_pos = content.find("daemon=1").unwrap(); - let section_pos = content.find("[test]").unwrap(); - assert!( - daemon_pos < section_pos, - "top-level entries should come before section headers" - ); - assert!(content.contains("rpcport=18332")); - } -} diff --git a/src/components/bitcoin_config_view.rs b/src/components/bitcoin_config_view.rs deleted file mode 100644 index c2962e8..0000000 --- a/src/components/bitcoin_config_view.rs +++ /dev/null @@ -1,705 +0,0 @@ -// SPDX-FileCopyrightText: 2024 PDM Authors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -use crate::app::{App, AppAction}; -use crate::bitcoin_config::ConfigEntry; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - prelude::*, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, -}; -use std::path::Path; -use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; - -/// Shortens a path to fit within `max_len` display columns. -fn shorten_path(path: &Path, max_len: usize, home: &str) -> String { - let full = path.to_string_lossy().into_owned(); - - let s = if !home.is_empty() && full.starts_with(home) { - format!("~{}", full.strip_prefix(home).unwrap_or(&full)) - } else { - full - }; - - if s.width() <= max_len { - return s; - } - - let p = Path::new(&s); - let filename = p - .file_name() - .map_or_else(|| s.clone(), |f| f.to_string_lossy().into_owned()); - let parent_name = p - .parent() - .and_then(|p| p.file_name()) - .map(|f| f.to_string_lossy().into_owned()); - let prefix = if s.starts_with('~') { "~" } else { "" }; - - // Try ~/…/parent/filename - if let Some(ref parent) = parent_name { - let candidate = format!("{prefix}/\u{2026}/{parent}/{filename}"); - if candidate.width() <= max_len { - return candidate; - } - } - - // Try ~/…/filename - let candidate = format!("{prefix}/\u{2026}/{filename}"); - if candidate.width() <= max_len { - return candidate; - } - - // Truncate from the left, respecting display column width - let avail = max_len.saturating_sub(1); // 1 column for "…" - let mut width_acc = 0usize; - let mut suffix_chars: Vec = Vec::new(); - for c in s.chars().rev() { - let cw = UnicodeWidthChar::width(c).unwrap_or(1); - if width_acc + cw > avail { - break; - } - width_acc += cw; - suffix_chars.push(c); - } - suffix_chars.reverse(); - let suffix: String = suffix_chars.into_iter().collect(); - format!("\u{2026}{suffix}") -} - -#[derive(Debug, Clone)] -pub struct BitcoinConfigView { - pub selected_index: usize, - pub editing: bool, - pub edit_input: String, - pub save_message: Option, - pub warning_message: Option, - pub sidebar_focused: bool, - /// True when entries have been committed (via `CommitEdit`) but not yet saved to disk. - pub dirty: bool, -} - -impl BitcoinConfigView { - #[must_use] - pub fn new() -> Self { - Self { - selected_index: 0, - editing: false, - edit_input: String::new(), - save_message: None, - warning_message: None, - sidebar_focused: true, - dirty: false, - } - } - - pub fn handle_input(&mut self, key: KeyEvent, entries: &[ConfigEntry]) -> AppAction { - if self.editing { - match key.code { - KeyCode::Enter => { - let action = - AppAction::CommitEdit(self.selected_index, self.edit_input.clone()); - self.editing = false; - self.edit_input.clear(); - self.save_message = None; - action - } - KeyCode::Esc => { - self.editing = false; - self.edit_input.clear(); - AppAction::None - } - KeyCode::Backspace => { - self.edit_input.pop(); - AppAction::None - } - KeyCode::Char(c) => { - self.edit_input.push(c); - AppAction::None - } - _ => AppAction::None, - } - } else { - match key.code { - KeyCode::Up => { - if self.selected_index > 0 { - self.selected_index -= 1; - } - self.save_message = None; - AppAction::None - } - KeyCode::Down => { - if self.selected_index + 1 < entries.len() { - self.selected_index += 1; - } - self.save_message = None; - AppAction::None - } - KeyCode::Enter => { - if !entries.is_empty() { - self.edit_input - .clone_from(&entries[self.selected_index].value); - self.editing = true; - self.save_message = None; - } - AppAction::None - } - KeyCode::Char('s') => AppAction::SaveBitcoinConfig, - KeyCode::Esc => { - self.sidebar_focused = true; - self.save_message = None; - AppAction::None - } - _ => AppAction::None, - } - } - } - - #[allow(clippy::too_many_lines)] // Renders two panels with multiple layout passes - pub fn render(f: &mut Frame, app: &mut App, area: Rect) { - const FIXED: usize = 33; - if app.bitcoin_conf_path.is_none() { - let p = Paragraph::new("Press [Enter] to select a bitcoin.conf file").block( - Block::default() - .borders(Borders::ALL) - .title(" Bitcoin Config "), - ); - f.render_widget(p, area); - return; - } - - let panels = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(45), Constraint::Percentage(55)]) - .split(area); - - // Left panel: scrollable entry list - let items: Vec = app - .bitcoin_data - .iter() - .map(|entry| { - let label = entry.schema.as_ref().map_or("", |s| s.description.as_str()); - - let (value_display, value_style) = if entry.enabled { - ( - entry.value.clone(), - Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD), - ) - } else { - let placeholder = entry - .schema - .as_ref() - .filter(|s| !s.default.is_empty()) - .map_or_else( - || "not set".to_string(), - |s| format!("default: {}", s.default), - ); - ( - format!("({placeholder})"), - Style::default().fg(Color::DarkGray), - ) - }; - - ListItem::new(vec![ - Line::from(Span::styled(label, Style::default().fg(Color::Gray))), - Line::from(vec![ - Span::styled( - format!("{} = ", entry.key), - Style::default().fg(Color::Cyan), - ), - Span::styled(value_display, value_style), - ]), - ]) - }) - .collect(); - - let mut list_state = ListState::default(); - list_state.select(Some(app.bitcoin_config_view.selected_index)); - - // Border style: dim both panels when the user is navigating the main sidebar - let panel_style = if app.bitcoin_config_view.sidebar_focused { - Style::default().fg(Color::DarkGray) - } else { - Style::default() - }; - - let dirty = app.bitcoin_config_view.dirty; - let path_max = (panels[0].width as usize).saturating_sub(FIXED); - let title = match &app.bitcoin_conf_path { - Some(path) => format!( - " {}Bitcoin Configuration --- {} ", - if dirty { "● " } else { "" }, - shorten_path(path, path_max, &app.home_dir) - ), - None => " Bitcoin Configuration ".to_string(), - }; - let title_style = if dirty { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }; - - let list = List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .title(title) - .title_style(title_style) - .border_style(panel_style), - ) - .highlight_style(Style::default().bg(Color::DarkGray)); - - f.render_stateful_widget(list, panels[0], &mut list_state); - - // Right panel: detail and edit field - let right_block = Block::default() - .borders(Borders::ALL) - .title(" Detail ") - .border_style(panel_style); - let inner = right_block.inner(panels[1]); - f.render_widget(right_block, panels[1]); - - let selected_entry = app.bitcoin_data.get(app.bitcoin_config_view.selected_index); - let editing = app.bitcoin_config_view.editing; - let edit_input = app.bitcoin_config_view.edit_input.clone(); - - if let Some(entry) = selected_entry { - let description = entry - .schema - .as_ref() - .map_or("Unknown option", |s| s.description.as_str()); - let type_label = entry - .schema - .as_ref() - .map(|s| format!("{}", s.config_type)) - .unwrap_or_default(); - - let rows = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(2), // description - Constraint::Length(1), // type - Constraint::Length(1), // spacer - Constraint::Length(1), // "Value:" label - Constraint::Length(3), // value / input box - Constraint::Min(0), - ]) - .split(inner); - - f.render_widget( - Paragraph::new(description).style(Style::default().fg(Color::White)), - rows[0], - ); - f.render_widget( - Paragraph::new(format!("Type: {type_label}")) - .style(Style::default().fg(Color::Gray)), - rows[1], - ); - f.render_widget( - Paragraph::new("Value:").style(Style::default().fg(Color::Gray)), - rows[3], - ); - - if editing { - f.render_widget( - Paragraph::new(edit_input.as_str()) - .block(Block::default().borders(Borders::ALL)) - .style(Style::default().fg(Color::Yellow)), - rows[4], - ); - let cursor_x = - (rows[4].x + 1 + u16::try_from(edit_input.chars().count()).unwrap_or(u16::MAX)) - .min(rows[4].x + rows[4].width.saturating_sub(2)); - let cursor_y = rows[4].y + 1; - f.set_cursor_position((cursor_x, cursor_y)); - } else { - let (display, style) = if entry.enabled { - ( - entry.value.clone(), - Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD), - ) - } else { - let placeholder = entry - .schema - .as_ref() - .filter(|s| !s.default.is_empty()) - .map_or_else( - || "not set".to_string(), - |s| format!("default: {}", s.default), - ); - ( - format!("({placeholder})"), - Style::default().fg(Color::DarkGray), - ) - }; - f.render_widget( - Paragraph::new(display) - .block(Block::default().borders(Borders::ALL)) - .style(style), - rows[4], - ); - } - } - } -} - -impl Default for BitcoinConfigView { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::app::AppAction; - use crate::bitcoin_config::ConfigEntry; - use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - - fn entry(key: &str, value: &str, enabled: bool) -> ConfigEntry { - ConfigEntry { - key: key.to_string(), - value: value.to_string(), - enabled, - schema: None, - section: None, - } - } - - fn key(code: KeyCode) -> KeyEvent { - KeyEvent::new(code, KeyModifiers::empty()) - } - - // --- shorten path --- - - #[test] - fn shorten_path_short_enough_unchanged() { - let p = Path::new("/foo/bar.conf"); - assert_eq!(shorten_path(p, 100, ""), "/foo/bar.conf"); - } - - #[test] - fn shorten_path_collapses_to_parent_filename() { - // Path with no HOME prefix, long enough to trigger collapse - let p = Path::new("/a/very/long/path/to/parent/file.conf"); - let result = shorten_path(p, 20, ""); - assert!(result.contains("file.conf")); - assert!(result.width() <= 20); - } - - #[test] - fn shorten_path_collapses_to_filename_only() { - // Parent/filename still too long → ~/…/filename - let long_parent = "/a/b/c/d/longlonglonglongparent/file.conf"; - let p = Path::new(long_parent); - let result = shorten_path(p, 18, ""); - assert!(result.contains("file.conf")); - assert!(result.width() <= 18); - } - - #[test] - fn shorten_path_last_resort_truncation() { - // Even filename alone doesn't fit → truncate with ellipsis - let p = Path::new("/a/b/c/d/e/verylongfilename.conf"); - let result = shorten_path(p, 5, ""); - assert!(result.starts_with('\u{2026}')); - assert!(result.width() <= 5); - } - - #[test] - fn shorten_path_multibyte_chars_respected() { - let p = Path::new("/日本語/パス/ファイル.conf"); - let result = shorten_path(p, 15, ""); - // Must not exceed 15 display columns regardless of byte/char width - assert!( - result.width() <= 15, - "got {} columns: {}", - result.width(), - result - ); - } - - #[test] - fn shorten_path_replaces_home_prefix() { - let home = std::env::var("HOME").unwrap_or_default(); - if home.is_empty() { - return; // skip on systems without HOME - } - let p = Path::new(&home).join("myfile.conf"); - let result = shorten_path(&p, 200, &home); - assert!( - result.starts_with('~'), - "expected ~ prefix, got: {}", - result - ); - } - - // --- handle_input: editing mode --- - - #[test] - fn editing_char_appends_to_input() { - let mut view = BitcoinConfigView::new(); - view.editing = true; - let entries = vec![entry("rpcuser", "old", true)]; - - view.handle_input(key(KeyCode::Char('x')), &entries); - assert_eq!(view.edit_input, "x"); - } - - #[test] - fn editing_backspace_removes_last_char() { - let mut view = BitcoinConfigView::new(); - view.editing = true; - view.edit_input = "ab".to_string(); - let entries = vec![entry("rpcuser", "old", true)]; - - view.handle_input(key(KeyCode::Backspace), &entries); - assert_eq!(view.edit_input, "a"); - } - - #[test] - fn editing_enter_returns_commit_action() { - let mut view = BitcoinConfigView::new(); - view.editing = true; - view.edit_input = "newval".to_string(); - view.selected_index = 0; - let entries = vec![entry("rpcuser", "old", true)]; - - let action = view.handle_input(key(KeyCode::Enter), &entries); - assert!( - matches!(action, AppAction::CommitEdit(0, ref v) if v == "newval"), - "expected CommitEdit(0, newval)" - ); - assert!(!view.editing); - assert!(view.edit_input.is_empty()); - } - - #[test] - fn editing_esc_cancels_without_committing() { - let mut view = BitcoinConfigView::new(); - view.editing = true; - view.edit_input = "draft".to_string(); - let entries = vec![entry("rpcuser", "old", true)]; - - let action = view.handle_input(key(KeyCode::Esc), &entries); - assert!(matches!(action, AppAction::None)); - assert!(!view.editing); - assert!(view.edit_input.is_empty()); - } - - #[test] - fn editing_other_key_is_noop() { - let mut view = BitcoinConfigView::new(); - view.editing = true; - let entries = vec![entry("rpcuser", "old", true)]; - - let action = view.handle_input(key(KeyCode::F(1)), &entries); - assert!(matches!(action, AppAction::None)); - assert!(view.editing); - } - - // --- handle_input: browsing mode --- - - #[test] - fn browsing_down_increments_index() { - let mut view = BitcoinConfigView::new(); - let entries = vec![entry("a", "1", true), entry("b", "2", true)]; - - view.handle_input(key(KeyCode::Down), &entries); - assert_eq!(view.selected_index, 1); - } - - #[test] - fn browsing_down_clamped_at_last_entry() { - let mut view = BitcoinConfigView::new(); - view.selected_index = 1; - let entries = vec![entry("a", "1", true), entry("b", "2", true)]; - - view.handle_input(key(KeyCode::Down), &entries); - assert_eq!(view.selected_index, 1); - } - - #[test] - fn browsing_up_decrements_index() { - let mut view = BitcoinConfigView::new(); - view.selected_index = 1; - let entries = vec![entry("a", "1", true), entry("b", "2", true)]; - - view.handle_input(key(KeyCode::Up), &entries); - assert_eq!(view.selected_index, 0); - } - - #[test] - fn browsing_up_clamped_at_zero() { - let mut view = BitcoinConfigView::new(); - view.selected_index = 0; - let entries = vec![entry("a", "1", true)]; - - view.handle_input(key(KeyCode::Up), &entries); - assert_eq!(view.selected_index, 0); - } - - #[test] - fn browsing_enter_starts_editing_with_current_value() { - let mut view = BitcoinConfigView::new(); - let entries = vec![entry("rpcuser", "alice", true)]; - - view.handle_input(key(KeyCode::Enter), &entries); - assert!(view.editing); - assert_eq!(view.edit_input, "alice"); - } - - #[test] - fn browsing_enter_noop_when_entries_empty() { - let mut view = BitcoinConfigView::new(); - let entries: Vec = vec![]; - - view.handle_input(key(KeyCode::Enter), &entries); - assert!(!view.editing); - } - - #[test] - fn browsing_s_returns_save_action() { - let mut view = BitcoinConfigView::new(); - let entries = vec![entry("rpcuser", "alice", true)]; - - let action = view.handle_input(key(KeyCode::Char('s')), &entries); - assert!(matches!(action, AppAction::SaveBitcoinConfig)); - } - - #[test] - fn browsing_esc_sets_sidebar_focused() { - let mut view = BitcoinConfigView::new(); - view.sidebar_focused = false; - let entries = vec![entry("rpcuser", "alice", true)]; - - view.handle_input(key(KeyCode::Esc), &entries); - assert!(view.sidebar_focused); - } - - #[test] - fn navigation_clears_save_message() { - let entries = vec![entry("a", "1", true), entry("b", "2", true)]; - - // Up clears it - let mut view = BitcoinConfigView::new(); - view.selected_index = 1; - view.save_message = Some("saved".to_string()); - view.handle_input(key(KeyCode::Up), &entries); - assert!(view.save_message.is_none()); - - // Down clears it - let mut view = BitcoinConfigView::new(); - view.save_message = Some("saved".to_string()); - view.handle_input(key(KeyCode::Down), &entries); - assert!(view.save_message.is_none()); - - // Enter (start editing) clears it - let mut view = BitcoinConfigView::new(); - view.save_message = Some("saved".to_string()); - view.handle_input(key(KeyCode::Enter), &entries); - assert!(view.save_message.is_none()); - - // Esc (back to sidebar) clears it - let mut view = BitcoinConfigView::new(); - view.save_message = Some("saved".to_string()); - view.handle_input(key(KeyCode::Esc), &entries); - assert!(view.save_message.is_none()); - } - - #[test] - fn save_key_does_not_clear_save_message() { - let mut view = BitcoinConfigView::new(); - view.save_message = Some("Configuration correctly saved".to_string()); - let entries = vec![entry("rpcuser", "alice", true)]; - - let action = view.handle_input(key(KeyCode::Char('s')), &entries); - assert!(matches!(action, AppAction::SaveBitcoinConfig)); - assert_eq!( - view.save_message.as_deref(), - Some("Configuration correctly saved"), - "save_message must not be cleared when pressing s" - ); - } - - #[test] - fn commit_edit_clears_save_message() { - let mut view = BitcoinConfigView::new(); - view.editing = true; - view.edit_input = "newval".to_string(); - view.save_message = Some("saved".to_string()); - let entries = vec![entry("rpcuser", "alice", true)]; - - view.handle_input(key(KeyCode::Enter), &entries); - assert!(view.save_message.is_none()); - } - - #[test] - fn unrecognised_key_preserves_save_message() { - let mut view = BitcoinConfigView::new(); - view.save_message = Some("saved".to_string()); - let entries = vec![entry("rpcuser", "alice", true)]; - - view.handle_input(key(KeyCode::F(1)), &entries); - assert_eq!(view.save_message.as_deref(), Some("saved")); - } - - #[test] - fn render_with_entries_exercises_items_loop() { - use crate::app::App; - use crate::bitcoin_config::{ConfigCategory, ConfigSchema, ConfigType}; - use ratatui::Terminal; - use ratatui::backend::TestBackend; - - let mut app = App::new(); - // Set a path so render goes past the early-return guard - app.bitcoin_conf_path = Some(std::path::PathBuf::from("/tmp/bitcoin.conf")); - - // One enabled entry - let mut e1 = entry("rpcuser", "alice", true); - e1.schema = Some(ConfigSchema::new( - "rpcuser", - "", - ConfigType::String, - ConfigCategory::RPC, - "RPC username", - )); - - // One disabled entry with schema - let mut e2 = entry("dbcache", "450", false); - e2.schema = Some(ConfigSchema::new( - "dbcache", - "450", - ConfigType::Int, - ConfigCategory::Core, - "DB cache size", - )); - - // One disabled entry with no schema - let e3 = entry("unknownkey", "", false); - - app.bitcoin_data = vec![e1, e2, e3]; - - let mut terminal = Terminal::new(TestBackend::new(120, 30)).unwrap(); - terminal - .draw(|f| { - let area = f.area(); - BitcoinConfigView::render(f, &mut app, area); - }) - .unwrap(); - - let output: String = terminal - .backend() - .buffer() - .content() - .iter() - .map(|c| c.symbol().to_string()) - .collect(); - - assert!(output.contains("Bitcoin Configuration")); - } -} diff --git a/src/components/bitcoin_status_view.rs b/src/components/bitcoin_status_view.rs index 6b9df10..1ccfabe 100644 --- a/src/components/bitcoin_status_view.rs +++ b/src/components/bitcoin_status_view.rs @@ -280,7 +280,7 @@ mod tests { assert!(output.contains("Connection Count : -")); } - /// Renders the Peers tab (index 3) and returns the buffer content as a string. + /// Renders the Peers tab (index 1) and returns the buffer content as a string. fn render_peers_view(app: &App) -> String { let backend = TestBackend::new(80, 25); let mut terminal = Terminal::new(backend).unwrap(); diff --git a/src/main.rs b/src/main.rs index 7c04ad8..6481923 100644 --- a/src/main.rs +++ b/src/main.rs @@ -109,9 +109,9 @@ fn dispatch_key(key: event::KeyEvent, app: &mut App) -> KeyOutcome { // Ctrl-C is always a hard exit. // 'q' is suppressed while a text-input field is active. - let text_input_active = (app.current_screen == CurrentScreen::P2PoolConfig + let text_input_active = app.current_screen == CurrentScreen::P2PoolConfig && !app.p2pool_config_view.sidebar_focused - && app.p2pool_config_view.editing); + && app.p2pool_config_view.editing; if (key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('c')) || (!text_input_active && key.code == KeyCode::Char('q')) @@ -908,7 +908,7 @@ port = 46884 #[test] #[serial] - fn file_selected_for_settings_field_1_invalid_hostname_sets_error() { + fn file_selected_for_settings_field_0_invalid_hostname_sets_error() { use tempfile::tempdir; let dir = tempdir().unwrap(); @@ -931,7 +931,7 @@ port = 46884 #[test] #[serial] - fn file_selected_for_settings_field_1_load_failure_sets_error() { + fn file_selected_for_settings_field_0_load_failure_sets_error() { use tempfile::tempdir; let dir = tempdir().unwrap(); @@ -989,7 +989,7 @@ port = 46884 #[test] fn open_explorer_for_settings_sets_state() { let mut app = App::new(); - app.sidebar_index = 8; + app.sidebar_index = 7; app.toggle_menu(); let flow = handle_action(AppAction::OpenExplorerForSettings(1), &mut app).unwrap(); diff --git a/src/ui.rs b/src/ui.rs index 18f30c5..b972360 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -104,28 +104,6 @@ mod tests { insta::assert_debug_snapshot!(terminal.backend()); } - #[test] - fn test_bitcoin_status_tab_system_render() { - let mut terminal = make_terminal(); - let mut app = App::new(); - app.sidebar_index = 1; - app.toggle_menu(); - app.bitcoin_status_tab = 1; - terminal.draw(|f| ui(f, &mut app)).unwrap(); - insta::assert_debug_snapshot!(terminal.backend()); - } - - #[test] - fn test_bitcoin_status_tab_logs_render() { - let mut terminal = make_terminal(); - let mut app = App::new(); - app.sidebar_index = 1; - app.toggle_menu(); - app.bitcoin_status_tab = 2; - terminal.draw(|f| ui(f, &mut app)).unwrap(); - insta::assert_debug_snapshot!(terminal.backend()); - } - #[test] fn test_bitcoin_status_tab_peers_render() { let mut terminal = make_terminal(); From fce51ffbcd3d1d8213e9e5c4de22cdb4ebf538c6 Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Sun, 23 Aug 2026 14:19:50 +0000 Subject: [PATCH 3/4] test(settings): restore assertions dropped in bitcoin config removal Removing bitcoin_conf_path left several tests in settings.rs with no remaining assertions - some passed trivially with zero checks. Rebuilt using p2pool_conf_path as the differentiating field: - load_settings_returns_default_when_file_missing/for_invalid_toml: restored default-field assertions - load_settings_reads_valid_file: was writing/loading/asserting nothing; now round-trips a real p2pool_conf_path value - save_with_override_writes_to_override_dir_and_default: assert override and default copies match - load_settings_reads_from_override_dir_when_set / load_settings_falls_back_to_default_when_override_unreadable: pointer and authoritative Settings were identical, so nothing could prove which one loaded - gave each a distinct p2pool_conf_path and assert the correct one wins --- src/settings.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/settings.rs b/src/settings.rs index 483af2a..b6b5f73 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -203,6 +203,10 @@ mod tests { set_config_dir(&dir); // No settings.toml written let settings = load_settings(); + assert!(settings.p2pool_conf_path.is_none()); + assert!(settings.ln_conf_path.is_none()); + assert!(settings.shares_market_conf_path.is_none()); + assert!(settings.settings_dir_override.is_none()); } #[test] @@ -212,6 +216,10 @@ mod tests { set_config_dir(&dir); std::fs::write(dir.path().join("settings.toml"), "not valid toml :::").unwrap(); let settings = load_settings(); + assert!(settings.p2pool_conf_path.is_none()); + assert!(settings.ln_conf_path.is_none()); + assert!(settings.shares_market_conf_path.is_none()); + assert!(settings.settings_dir_override.is_none()); } #[test] @@ -219,6 +227,16 @@ mod tests { fn load_settings_reads_valid_file() { let dir = tempfile::tempdir().unwrap(); set_config_dir(&dir); + std::fs::write( + dir.path().join("settings.toml"), + r#"p2pool_conf_path = "/tmp/p2pool.toml""#, + ) + .unwrap(); + let settings = load_settings(); + assert_eq!( + settings.p2pool_conf_path, + Some(PathBuf::from("/tmp/p2pool.toml")) + ); } #[test] @@ -272,6 +290,11 @@ mod tests { let override_content = std::fs::read_to_string(&override_path).unwrap(); let default_content = std::fs::read_to_string(&default_path).unwrap(); + assert_eq!( + override_content, default_content, + "override and default copies must match" + ); + assert!(override_content.contains("settings_dir_override")); } #[test] @@ -283,6 +306,7 @@ mod tests { // Write a pointer in the default dir. let pointer = Settings { + p2pool_conf_path: Some(PathBuf::from("/default/p2pool.toml")), settings_dir_override: Some(override_dir.path().to_path_buf()), ..Default::default() }; @@ -294,6 +318,7 @@ mod tests { // Write the authoritative settings in the override dir. let authoritative = Settings { + p2pool_conf_path: Some(PathBuf::from("/override/p2pool.toml")), settings_dir_override: Some(override_dir.path().to_path_buf()), ..Default::default() }; @@ -304,6 +329,11 @@ mod tests { .unwrap(); let loaded = load_settings(); + assert_eq!( + loaded.p2pool_conf_path, + Some(PathBuf::from("/override/p2pool.toml")), + "must read from the override dir, not the default-location pointer" + ); } #[test] @@ -314,6 +344,7 @@ mod tests { // Pointer points to a directory that doesn't exist. let pointer = Settings { + p2pool_conf_path: Some(PathBuf::from("/default/p2pool.toml")), settings_dir_override: Some(PathBuf::from("/nonexistent/dir")), ..Default::default() }; @@ -325,5 +356,9 @@ mod tests { let loaded = load_settings(); // Override unreadable → falls back to the default-location settings. + assert_eq!( + loaded.p2pool_conf_path, + Some(PathBuf::from("/default/p2pool.toml")) + ); } } From 58fa82f995e27b489a0a5e43344358c23d7ffe65 Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Tue, 25 Aug 2026 16:39:10 +0000 Subject: [PATCH 4/4] Fix UI snapshots and P2Pool config path handling --- src/main.rs | 5 ++- ...ests__bitcoin_status_tab_peers_render.snap | 44 +++++++++++-------- src/ui.rs | 2 +- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6481923..fd21285 100644 --- a/src/main.rs +++ b/src/main.rs @@ -314,11 +314,11 @@ fn handle_action(action: AppAction, app: &mut App) -> Result> { ); should_save = false; } else { + app.p2pool_conf_path = Some(path.clone()); app.p2pool_config = Some(cfg); app.settings.p2pool_conf_path = Some(path.clone()); app.p2pool_config_view.warning_message = None; app.p2pool_config_view.selected_index = 0; - app.settings.p2pool_conf_path = Some(path.clone()); } } Err(e) => { @@ -1077,7 +1077,8 @@ port = 46884 app.explorer_trigger = Some(ExplorerTrigger::Settings(0)); run(AppAction::FileSelected(path.clone()), &mut app); - assert_eq!(app.settings.p2pool_conf_path, Some(path)); + assert_eq!(app.settings.p2pool_conf_path, Some(path.clone())); + assert_eq!(app.p2pool_conf_path, Some(path)); assert_eq!(app.current_screen, CurrentScreen::Settings); } diff --git a/src/snapshots/pdm__ui__tests__bitcoin_status_tab_peers_render.snap b/src/snapshots/pdm__ui__tests__bitcoin_status_tab_peers_render.snap index 0e166ca..fd739b9 100644 --- a/src/snapshots/pdm__ui__tests__bitcoin_status_tab_peers_render.snap +++ b/src/snapshots/pdm__ui__tests__bitcoin_status_tab_peers_render.snap @@ -10,31 +10,37 @@ TestBackend { "│Home ││ Chain Info │ Peers │", "│Bitcoin Status ││ │", "│P2Pool Config │└─────────────────────────────────────────────────────┘", - "│P2Pool Status │ ", - "│LN Config │ ", - "│LN Status │ ", - "│Shares Market │ ", - "│Settings │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "└───────────────────────┘ ", + "│P2Pool Status │┌ Peers ──────────────────────────────────────────────┐", + "│LN Config ││Select a P2Poolv2 config file to load Bitcoin Core │", + "│LN Status ││peer info. │", + "│Shares Market ││ │", + "│Settings ││ │", + "│ ││ │", + "│ ││ │", + "│ ││ │", + "│ ││ │", + "│ ││ │", + "│ ││ │", + "│ ││ │", + "│ ││ │", + "│ ││ │", + "│ ││ │", + "│ ││ │", + "│ ││ │", + "│ ││ │", + "└───────────────────────┘└─────────────────────────────────────────────────────┘", " ↑↓ Navigate sidebar ←→ Switch tab q Quit ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 40, y: 1, fg: Black, bg: Gray, underline: Reset, modifier: NONE, + x: 45, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 1, y: 2, fg: Black, bg: Gray, underline: Reset, modifier: NONE, x: 24, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 26, y: 5, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, + x: 76, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 26, y: 6, fg: DarkGray, bg: Reset, underline: Reset, modifier: NONE, + x: 36, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, x: 4, y: 23, fg: DarkGray, bg: Black, underline: Reset, modifier: NONE, x: 23, y: 23, fg: White, bg: DarkGray, underline: Reset, modifier: NONE, diff --git a/src/ui.rs b/src/ui.rs index b972360..d6b1e67 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -110,7 +110,7 @@ mod tests { let mut app = App::new(); app.sidebar_index = 1; app.toggle_menu(); - app.bitcoin_status_tab = 3; + app.bitcoin_status_tab = 1; terminal.draw(|f| ui(f, &mut app)).unwrap(); insta::assert_debug_snapshot!(terminal.backend()); }