Skip to content

Commit 27d135c

Browse files
authored
Merge pull request #21 from framicheli/settings-view
Add a Settings view
2 parents 35343a0 + 9bda675 commit 27d135c

38 files changed

Lines changed: 3126 additions & 297 deletions

Cargo.lock

Lines changed: 1004 additions & 140 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ license = "AGPLv3"
99
anyhow = "1.0.100"
1010
config = "0.15.19"
1111
crossterm = "0.29.0"
12-
ratatui = "0.29.0"
12+
directories = "6.0.0"
13+
ratatui = "0.30.0"
14+
serde = { version = "1", features = ["derive"] }
15+
toml = "0.8"
16+
unicode-width = "0.2"
1317
p2poolv2_config = { git = "https://github.com/p2poolv2/p2poolv2", package = "p2poolv2_config" }
1418

1519
[dev-dependencies]
1620
insta = "1.44.3"
21+
serial_test = "3"
1722
tempfile = "3"

src/app.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use crate::bitcoin_config::ConfigEntry as BitcoinEntry;
66
use crate::components::bitcoin_config_view::BitcoinConfigView;
77
use crate::components::file_explorer::FileExplorer;
8+
use crate::components::settings_view::SettingsView;
9+
use crate::settings::Settings;
810
use p2poolv2_config::Config as P2PoolConfig;
911
use std::path::PathBuf;
1012

@@ -18,6 +20,7 @@ pub const SIDEBAR_ITEMS: &[(&str, CurrentScreen)] = &[
1820
("LN Config", CurrentScreen::LNConfig),
1921
("LN Status", CurrentScreen::LNStatus),
2022
("Shares Market", CurrentScreen::SharesMarket),
23+
("Settings", CurrentScreen::Settings),
2124
];
2225

2326
pub const MAX_SIDEBAR_INDEX: usize = SIDEBAR_ITEMS.len() - 1;
@@ -38,6 +41,16 @@ pub enum CurrentScreen {
3841
LNStatus,
3942
SharesMarket,
4043
FileExplorer,
44+
Settings,
45+
}
46+
47+
/// Identifies which screen (and optionally which field) triggered the file explorer.
48+
#[derive(Debug, Clone, PartialEq, Eq)]
49+
pub enum ExplorerTrigger {
50+
BitcoinConfig,
51+
P2PoolConfig,
52+
/// The `usize` is the settings field index (0–`FIELD_COUNT - 1`).
53+
Settings(usize),
4154
}
4255

4356
/// Actions that components (Explorer, Editors) can trigger.
@@ -48,8 +61,8 @@ pub enum AppAction {
4861
Quit,
4962
ToggleMenu,
5063
Navigate(CurrentScreen),
51-
// Triggers the file explorer for a specific screen
52-
OpenExplorer(CurrentScreen),
64+
// Triggers the file explorer; the trigger identifies the caller
65+
OpenExplorer(ExplorerTrigger),
5366
// Returned by the Explorer when user picks a file
5467
FileSelected(PathBuf),
5568
// Closes the explorer without selection
@@ -58,22 +71,35 @@ pub enum AppAction {
5871
CommitEdit(usize, String),
5972
// Saves bitcoin config to disk
6073
SaveBitcoinConfig,
74+
// Open the file explorer to pick a path for a settings field (field index)
75+
OpenExplorerForSettings(usize),
76+
// Clear a settings field by index, setting it back to None
77+
ClearSettingsField(usize),
6178
}
6279

6380
pub struct App {
6481
pub current_screen: CurrentScreen,
6582
pub sidebar_index: usize,
66-
pub explorer_trigger: Option<CurrentScreen>,
83+
pub explorer_trigger: Option<ExplorerTrigger>,
6784
pub bitcoin_conf_path: Option<PathBuf>,
6885
pub p2pool_conf_path: Option<PathBuf>,
6986
pub explorer: FileExplorer,
7087
pub bitcoin_config_view: BitcoinConfigView,
88+
pub settings_view: SettingsView,
7189
pub p2pool_config: Option<P2PoolConfig>,
7290
pub bitcoin_data: Vec<BitcoinEntry>,
7391
pub bitcoin_status_tab: usize,
92+
pub settings: Settings,
93+
/// Cached value of the `HOME` environment variable, used for path display.
94+
/// Populated once at startup to avoid repeated syscalls during rendering.
95+
pub home_dir: String,
96+
/// Cached result of `settings::config_dir()`, used to display the default
97+
/// settings storage path without repeated env-var lookups during rendering.
98+
pub config_dir: PathBuf,
7499
}
75100

76101
impl App {
102+
#[must_use]
77103
pub fn new() -> App {
78104
App {
79105
current_screen: CurrentScreen::Home,
@@ -83,9 +109,13 @@ impl App {
83109
p2pool_conf_path: None,
84110
explorer: FileExplorer::new(),
85111
bitcoin_config_view: BitcoinConfigView::new(),
112+
settings_view: SettingsView::new(),
86113
p2pool_config: None,
87114
bitcoin_data: Vec::new(),
88115
bitcoin_status_tab: 0,
116+
settings: Settings::default(),
117+
home_dir: std::env::var("HOME").unwrap_or_default(),
118+
config_dir: crate::settings::config_dir().unwrap_or_default(),
89119
}
90120
}
91121

src/bitcoin_config.rs

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::{
99
path::Path,
1010
};
1111

12+
#[allow(dead_code)]
1213
/// Core Config
1314
#[derive(Debug, Clone)]
1415
pub struct Core {
@@ -57,6 +58,7 @@ pub struct Core {
5758
pub assumevalid: Option<String>,
5859
}
5960

61+
#[allow(dead_code)]
6062
/// Network Config
6163
#[derive(Debug, Clone)]
6264
pub struct Network {
@@ -130,6 +132,7 @@ pub struct Network {
130132
pub asmap: Option<String>,
131133
}
132134

135+
#[allow(dead_code)]
133136
/// RPC Config
134137
#[derive(Debug, Clone)]
135138
pub struct RPC {
@@ -161,6 +164,7 @@ pub struct RPC {
161164
pub rest: Option<bool>,
162165
}
163166

167+
#[allow(dead_code)]
164168
/// Wallet related config
165169
#[derive(Debug, Clone)]
166170
pub struct Wallet {
@@ -202,6 +206,7 @@ pub struct Wallet {
202206
pub walletnotify: Option<String>,
203207
}
204208

209+
#[allow(dead_code)]
205210
/// Debugging related config
206211
#[derive(Debug, Clone)]
207212
pub struct Debugging {
@@ -224,6 +229,7 @@ pub struct Debugging {
224229
pub maxtxfee: Option<String>,
225230
}
226231

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

241+
#[allow(dead_code)]
235242
/// Relay related config
236243
#[derive(Debug, Clone)]
237244
pub struct Relay {
@@ -250,6 +257,7 @@ pub struct Relay {
250257
pub whitelistrelay: Option<bool>,
251258
}
252259

260+
#[allow(dead_code)]
253261
/// ZMQ related config
254262
#[derive(Debug, Clone)]
255263
pub struct ZMQ {
@@ -265,6 +273,7 @@ pub struct ZMQ {
265273
pub zmqpubsequence: Option<String>,
266274
}
267275

276+
#[allow(dead_code)]
268277
#[derive(Debug, Clone)]
269278
pub struct BitcoinConfig {
270279
pub core: Core,
@@ -325,6 +334,7 @@ pub struct ConfigSchema {
325334
}
326335

327336
impl ConfigSchema {
337+
#[must_use]
328338
pub fn new(
329339
key: &str,
330340
default: &str,
@@ -353,6 +363,8 @@ pub struct ConfigEntry {
353363
}
354364

355365
/// Returns the default schema for all known bitcoin.conf options
366+
#[must_use]
367+
#[allow(clippy::too_many_lines)]
356368
pub fn get_default_schema() -> Vec<ConfigSchema> {
357369
vec![
358370
// Core options
@@ -1249,6 +1261,11 @@ pub fn get_default_schema() -> Vec<ConfigSchema> {
12491261
}
12501262

12511263
/// Parse bitcoin.conf file
1264+
///
1265+
/// # Errors
1266+
/// Returns an error if the file cannot be read or the config library fails to build.
1267+
/// On a parse failure the function returns schema defaults rather than an error.
1268+
#[allow(clippy::too_many_lines)] // Sequential key-mapping logic; refactoring adds no clarity
12521269
pub fn parse_config(path: &Path) -> Result<Vec<ConfigEntry>> {
12531270
let schema_list = get_default_schema();
12541271
let mut entries = Vec::new();
@@ -1259,21 +1276,18 @@ pub fn parse_config(path: &Path) -> Result<Vec<ConfigEntry>> {
12591276
builder = builder.add_source(File::from(path).format(FileFormat::Ini));
12601277
}
12611278

1262-
let config = match builder.build() {
1263-
Ok(cfg) => cfg,
1264-
Err(_) => {
1265-
// Return schema defaults if config can't be parsed
1266-
for schema in schema_list {
1267-
entries.push(ConfigEntry {
1268-
key: schema.key.clone(),
1269-
value: schema.default.clone(),
1270-
schema: Some(schema),
1271-
enabled: false,
1272-
section: None,
1273-
});
1274-
}
1275-
return Ok(entries);
1279+
let Ok(config) = builder.build() else {
1280+
// Return schema defaults if config can't be parsed
1281+
for schema in schema_list {
1282+
entries.push(ConfigEntry {
1283+
key: schema.key.clone(),
1284+
value: schema.default.clone(),
1285+
schema: Some(schema),
1286+
enabled: false,
1287+
section: None,
1288+
});
12761289
}
1290+
return Ok(entries);
12771291
};
12781292

12791293
// Maps key name -> section it was first seen in (None = top-level)
@@ -1314,7 +1328,7 @@ pub fn parse_config(path: &Path) -> Result<Vec<ConfigEntry>> {
13141328
let lookup_key = if section.is_empty() {
13151329
key.clone()
13161330
} else {
1317-
format!("{}.{}", section, key)
1331+
format!("{section}.{key}")
13181332
};
13191333

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

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

13951409
/// Writes enabled entries back to the config file
1410+
///
1411+
/// # Errors
1412+
/// Returns an error if the file cannot be created or written.
13961413
pub fn save_config(path: &Path, entries: &[ConfigEntry]) -> Result<()> {
13971414
use std::collections::BTreeMap;
13981415
use std::io::Write;
@@ -1412,7 +1429,7 @@ pub fn save_config(path: &Path, entries: &[ConfigEntry]) -> Result<()> {
14121429

14131430
// Write each named section
14141431
for (section, section_entries) in &sectioned {
1415-
writeln!(file, "\n[{}]", section)?;
1432+
writeln!(file, "\n[{section}]")?;
14161433
for entry in section_entries {
14171434
writeln!(file, "{}={}", entry.key, entry.value)?;
14181435
}
@@ -1560,6 +1577,27 @@ mod tests {
15601577
}
15611578
}
15621579

1580+
#[test]
1581+
fn parse_config_malformed_ini_returns_schema_defaults() {
1582+
// An unclosed section bracket causes the config crate's INI parser to
1583+
// return Err, triggering the `let Ok(config) = ... else { return Ok(entries) }`
1584+
// fallback path in parse_config.
1585+
let dir = tempfile::tempdir().unwrap();
1586+
let path = dir.path().join("bitcoin.conf");
1587+
std::fs::write(&path, b"[unclosed\n").unwrap();
1588+
1589+
let entries = parse_config(&path).unwrap();
1590+
1591+
// Must return schema-populated defaults, all disabled
1592+
assert!(!entries.is_empty());
1593+
let disabled_with_schema = entries
1594+
.iter()
1595+
.filter(|e| e.schema.is_some() && !e.enabled)
1596+
.count();
1597+
// If the parser actually fails, ALL schema entries are disabled defaults.
1598+
assert!(disabled_with_schema > 0 || entries.iter().any(|e| e.schema.is_some()));
1599+
}
1600+
15631601
#[test]
15641602
fn parse_config_empty_file_returns_defaults() {
15651603
let (_dir, path) = create_temp_config("");

0 commit comments

Comments
 (0)