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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,906 changes: 1,745 additions & 161 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ anyhow = "1.0.100"
config = "0.15.19"
crossterm = "0.29.0"
ratatui = "0.29.0"
serde = { version = "1.0", features = ["derive"] }
bitcoin = "0.31"
tokio = { version = "1.49.0", features = ["full"] }
reqwest = { version = "0.13.2", features = ["blocking"] }
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reqwest dependency is configured with the "blocking" feature, but the code uses the async API (client.get().send().await). The "blocking" feature is for synchronous HTTP requests. For async usage, you should remove "blocking" and keep only the default async features, or use no explicit features at all as the async API is the default.

Suggested change
reqwest = { version = "0.13.2", features = ["blocking"] }
reqwest = "0.13.2"

Copilot uses AI. Check for mistakes.

[dev-dependencies]
insta = "1.44.3"
Expand Down
35 changes: 34 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,61 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

use crate::components::file_explorer::FileExplorer;
use crate::components::metrics::P2PoolMetrics;
use crate::config::ConfigEntry as BitcoinEntry;
use crate::p2poolv2_config_parser::ConfigEntry as P2PoolEntry;
use std::path::PathBuf;

#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum CurrentScreen {
Home,
BitcoinConfig,
P2PoolConfig,
P2PoolStatus,
FileExplorer,
Exiting,
}

/// Actions that components (Explorer, Editors) can trigger.
/// This decouples input handling from business logic.
#[derive(Debug, Clone)]
pub enum AppAction {
None,
Quit,
ToggleMenu,
Navigate(CurrentScreen),
// Triggers the file explorer for a specific screen
OpenExplorer(CurrentScreen),
// Returned by the Explorer when user picks a file
FileSelected(PathBuf),
// Closes the explorer without selection
CloseModal,
}

pub struct App {
pub current_screen: CurrentScreen,
pub sidebar_index: usize,
pub explorer_trigger: Option<CurrentScreen>,
pub bitcoin_conf_path: Option<PathBuf>,
pub p2pool_conf_path: Option<PathBuf>,
pub node_metrics: Option<P2PoolMetrics>,
pub explorer: FileExplorer,
pub p2pool_data: Vec<P2PoolEntry>,
pub bitcoin_data: Vec<BitcoinEntry>,
}

impl App {
pub fn new() -> App {
App {
current_screen: CurrentScreen::Home,
sidebar_index: 0,
explorer_trigger: None,
bitcoin_conf_path: None,
p2pool_conf_path: None,
node_metrics: None,
explorer: FileExplorer::new(),
p2pool_data: Vec::new(),
bitcoin_data: Vec::new(),
}
}

Expand All @@ -35,6 +66,8 @@ impl App {
match self.sidebar_index {
0 => self.current_screen = CurrentScreen::Home,
1 => self.current_screen = CurrentScreen::BitcoinConfig,
2 => self.current_screen = CurrentScreen::P2PoolConfig,
3 => self.current_screen = CurrentScreen::P2PoolStatus,
_ => {}
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/components/file_explorer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later

use crate::app::AppAction;
use crossterm::event::{KeyCode, KeyEvent};
use std::fs;
use std::path::PathBuf;

Expand Down Expand Up @@ -115,6 +117,27 @@ impl FileExplorer {

None
}

pub fn handle_input(&mut self, key: KeyEvent) -> AppAction {
match key.code {
KeyCode::Up => {
self.previous();
AppAction::None
}
KeyCode::Down => {
self.next();
AppAction::None
}
KeyCode::Enter => {
if let Some(path) = self.select() {
return AppAction::FileSelected(path);
}
AppAction::None
}
KeyCode::Esc => AppAction::CloseModal,
_ => AppAction::None,
}
}
}

#[cfg(test)]
Expand Down
73 changes: 73 additions & 0 deletions src/components/metrics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2024 PDM Authors
// SPDX-License-Identifier: AGPL-3.0-or-later

#[derive(Debug, Default, Clone, PartialEq)]
pub struct P2PoolMetrics {
pub shares_accepted: u64,
pub shares_rejected: u64,
pub pool_difficulty: u64,
pub start_time: u64,
pub coinbase_total: u64,
}

impl P2PoolMetrics {
/// Parses a raw Prometheus text response into our P2PoolMetrics struct
pub fn parse_prometheus(raw_text: &str) -> Self {
let mut metrics = P2PoolMetrics::default();

for line in raw_text.lines() {
let trimmed = line.trim();

// Skip comments and empty lines
if trimmed.starts_with('#') || trimmed.is_empty() {
continue;
}

// Split by whitespace
let mut parts = trimmed.split_whitespace();
if let (Some(key), Some(value_str)) = (parts.next(), parts.next()) {
// Try to parse the value as a u64
let value = value_str.parse::<u64>().unwrap_or(0);

// Map the Prometheus keys to our struct fields
match key {
"shares_accepted_total" => metrics.shares_accepted = value,
"shares_rejected_total" => metrics.shares_rejected = value,
"pool_difficulty" => metrics.pool_difficulty = value,
"start_time_seconds" => metrics.start_time = value,
"coinbase_total" => metrics.coinbase_total = value,
_ => {} // Ignore metrics we don't care about
}
}
}

metrics
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_prometheus_output() {
let raw_data = "\
# HELP shares_accepted_total Total number of accepted shares
# TYPE shares_accepted_total counter
shares_accepted_total 0

# HELP start_time_seconds Pool start time in Unix timestamp
# TYPE start_time_seconds gauge
start_time_seconds 1771149691

coinbase_output{index=\"0\",address=\"tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk\"} 2500000000
coinbase_total 2500000000
";

let metrics = P2PoolMetrics::parse_prometheus(raw_data);

assert_eq!(metrics.shares_accepted, 0);
assert_eq!(metrics.start_time, 1771149691);
assert_eq!(metrics.coinbase_total, 2500000000);
}
}
1 change: 1 addition & 0 deletions src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pub mod file_explorer;
pub mod metrics;
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
pub mod app;
pub mod components;
pub mod config;
pub mod p2poolv2_config_parser;
pub mod ui;
Loading