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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,144 changes: 1,004 additions & 140 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ license = "AGPLv3"
anyhow = "1.0.100"
config = "0.15.19"
crossterm = "0.29.0"
ratatui = "0.29.0"
directories = "6.0.0"
ratatui = "0.30.0"
serde = { version = "1", features = ["derive"] }
toml = "0.8"
unicode-width = "0.2"
p2poolv2_config = { git = "https://github.com/p2poolv2/p2poolv2", package = "p2poolv2_config" }

[dev-dependencies]
insta = "1.44.3"
serial_test = "3"
tempfile = "3"
36 changes: 33 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use crate::bitcoin_config::ConfigEntry as BitcoinEntry;
use crate::components::bitcoin_config_view::BitcoinConfigView;
use crate::components::file_explorer::FileExplorer;
use crate::components::settings_view::SettingsView;
use crate::settings::Settings;
use p2poolv2_config::Config as P2PoolConfig;
use std::path::PathBuf;

Expand All @@ -18,6 +20,7 @@ pub const SIDEBAR_ITEMS: &[(&str, CurrentScreen)] = &[
("LN Config", CurrentScreen::LNConfig),
("LN Status", CurrentScreen::LNStatus),
("Shares Market", CurrentScreen::SharesMarket),
("Settings", CurrentScreen::Settings),
];

pub const MAX_SIDEBAR_INDEX: usize = SIDEBAR_ITEMS.len() - 1;
Expand All @@ -38,6 +41,16 @@ pub enum CurrentScreen {
LNStatus,
SharesMarket,
FileExplorer,
Settings,
}

/// Identifies which screen (and optionally which field) triggered the file explorer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExplorerTrigger {
BitcoinConfig,
P2PoolConfig,
/// The `usize` is the settings field index (0–`FIELD_COUNT - 1`).
Settings(usize),
}

/// Actions that components (Explorer, Editors) can trigger.
Expand All @@ -48,8 +61,8 @@ pub enum AppAction {
Quit,
ToggleMenu,
Navigate(CurrentScreen),
// Triggers the file explorer for a specific screen
OpenExplorer(CurrentScreen),
// Triggers the file explorer; the trigger identifies the caller
OpenExplorer(ExplorerTrigger),
// Returned by the Explorer when user picks a file
FileSelected(PathBuf),
// Closes the explorer without selection
Expand All @@ -58,22 +71,35 @@ pub enum AppAction {
CommitEdit(usize, String),
// Saves bitcoin config to disk
SaveBitcoinConfig,
// Open the file explorer to pick a path for a settings field (field index)
OpenExplorerForSettings(usize),
// Clear a settings field by index, setting it back to None
ClearSettingsField(usize),
}

pub struct App {
pub current_screen: CurrentScreen,
pub sidebar_index: usize,
pub explorer_trigger: Option<CurrentScreen>,
pub explorer_trigger: Option<ExplorerTrigger>,
pub bitcoin_conf_path: Option<PathBuf>,
pub p2pool_conf_path: Option<PathBuf>,
pub explorer: FileExplorer,
pub bitcoin_config_view: BitcoinConfigView,
pub settings_view: SettingsView,
pub p2pool_config: Option<P2PoolConfig>,
pub bitcoin_data: Vec<BitcoinEntry>,
pub bitcoin_status_tab: usize,
pub settings: Settings,
/// Cached value of the `HOME` environment variable, used for path display.
/// Populated once at startup to avoid repeated syscalls during rendering.
pub home_dir: String,
/// Cached result of `settings::config_dir()`, used to display the default
/// settings storage path without repeated env-var lookups during rendering.
pub config_dir: PathBuf,
}

impl App {
#[must_use]
pub fn new() -> App {
App {
current_screen: CurrentScreen::Home,
Expand All @@ -83,9 +109,13 @@ impl App {
p2pool_conf_path: None,
explorer: FileExplorer::new(),
bitcoin_config_view: BitcoinConfigView::new(),
settings_view: SettingsView::new(),
p2pool_config: None,
bitcoin_data: Vec::new(),
bitcoin_status_tab: 0,
settings: Settings::default(),
home_dir: std::env::var("HOME").unwrap_or_default(),
config_dir: crate::settings::config_dir().unwrap_or_default(),
}
}

Expand Down
72 changes: 55 additions & 17 deletions src/bitcoin_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::{
path::Path,
};

#[allow(dead_code)]
/// Core Config
#[derive(Debug, Clone)]
pub struct Core {
Expand Down Expand Up @@ -57,6 +58,7 @@ pub struct Core {
pub assumevalid: Option<String>,
}

#[allow(dead_code)]
/// Network Config
#[derive(Debug, Clone)]
pub struct Network {
Expand Down Expand Up @@ -130,6 +132,7 @@ pub struct Network {
pub asmap: Option<String>,
}

#[allow(dead_code)]
/// RPC Config
#[derive(Debug, Clone)]
pub struct RPC {
Expand Down Expand Up @@ -161,6 +164,7 @@ pub struct RPC {
pub rest: Option<bool>,
}

#[allow(dead_code)]
/// Wallet related config
#[derive(Debug, Clone)]
pub struct Wallet {
Expand Down Expand Up @@ -202,6 +206,7 @@ pub struct Wallet {
pub walletnotify: Option<String>,
}

#[allow(dead_code)]
/// Debugging related config
#[derive(Debug, Clone)]
pub struct Debugging {
Expand All @@ -224,6 +229,7 @@ pub struct Debugging {
pub maxtxfee: Option<String>,
}

#[allow(dead_code)]
/// Mining related config
#[derive(Debug, Clone)]
pub struct Mining {
Expand All @@ -232,6 +238,7 @@ pub struct Mining {
pub blockmintxfee: Option<String>,
}

#[allow(dead_code)]
/// Relay related config
#[derive(Debug, Clone)]
pub struct Relay {
Expand All @@ -250,6 +257,7 @@ pub struct Relay {
pub whitelistrelay: Option<bool>,
}

#[allow(dead_code)]
/// ZMQ related config
#[derive(Debug, Clone)]
pub struct ZMQ {
Expand All @@ -265,6 +273,7 @@ pub struct ZMQ {
pub zmqpubsequence: Option<String>,
}

#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BitcoinConfig {
pub core: Core,
Expand Down Expand Up @@ -325,6 +334,7 @@ pub struct ConfigSchema {
}

impl ConfigSchema {
#[must_use]
pub fn new(
key: &str,
default: &str,
Expand Down Expand Up @@ -353,6 +363,8 @@ pub struct ConfigEntry {
}

/// Returns the default schema for all known bitcoin.conf options
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn get_default_schema() -> Vec<ConfigSchema> {
vec![
// Core options
Expand Down Expand Up @@ -1249,6 +1261,11 @@ pub fn get_default_schema() -> Vec<ConfigSchema> {
}

/// 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<Vec<ConfigEntry>> {
let schema_list = get_default_schema();
let mut entries = Vec::new();
Expand All @@ -1259,21 +1276,18 @@ pub fn parse_config(path: &Path) -> Result<Vec<ConfigEntry>> {
builder = builder.add_source(File::from(path).format(FileFormat::Ini));
}

let config = match builder.build() {
Ok(cfg) => cfg,
Err(_) => {
// 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);
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)
Expand Down Expand Up @@ -1314,7 +1328,7 @@ pub fn parse_config(path: &Path) -> Result<Vec<ConfigEntry>> {
let lookup_key = if section.is_empty() {
key.clone()
} else {
format!("{}.{}", section, key)
format!("{section}.{key}")
};

let resolved = if let Ok(val) = config.get_string(&lookup_key) {
Expand Down Expand Up @@ -1360,7 +1374,7 @@ pub fn parse_config(path: &Path) -> Result<Vec<ConfigEntry>> {
if !found_keys.contains(config_key) {
let lookup_key = match key_section {
None => config_key.clone(),
Some(s) => format!("{}.{}", s, config_key),
Some(s) => format!("{s}.{config_key}"),
};

let value = if let Ok(val) = config.get_string(&lookup_key) {
Expand Down Expand Up @@ -1393,6 +1407,9 @@ pub fn parse_config(path: &Path) -> Result<Vec<ConfigEntry>> {
}

/// 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;
Expand All @@ -1412,7 +1429,7 @@ pub fn save_config(path: &Path, entries: &[ConfigEntry]) -> Result<()> {

// Write each named section
for (section, section_entries) in &sectioned {
writeln!(file, "\n[{}]", section)?;
writeln!(file, "\n[{section}]")?;
for entry in section_entries {
writeln!(file, "{}={}", entry.key, entry.value)?;
}
Expand Down Expand Up @@ -1560,6 +1577,27 @@ mod tests {
}
}

#[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("");
Expand Down
Loading
Loading