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
30 changes: 30 additions & 0 deletions share/config/example-multiple.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
tick_rate = 250

[price]
enabled = true
currency = "USD"
big_text = true

[fees]
enabled = true

[[nodes]]
provider = "bitcoin_core"
[nodes.bitcoin_core]
host = "127.0.0.1"
rpc_port = 18443
rpc_user = "polaruser"
rpc_password = "polarpass"
zmq_port = 28334

[[nodes]]
provider = "core_lightning"
[nodes.core_lightning]
rest_address = "http://127.0.0.1:3010"
rest_rune = "your_actual_rune"

[[nodes]]
provider = "lnd"
[nodes.lnd]
rest_address = "https://127.0.0.1:8080"
macaroon_hex = "your_actual_macaroon"
1 change: 1 addition & 0 deletions share/config/example.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
tick_rate = 250
streamer_mode = false

[node]
provider = "bitcoin_core"
Expand Down
161 changes: 114 additions & 47 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// app.rs

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
use std::error;
use std::str::FromStr;
use std::{env, error};
use tokio::sync::mpsc;
use tokio::time::{Duration, Instant};
use tokio_util::sync::CancellationToken;
Expand All @@ -12,7 +10,7 @@ use crate::config::AppConfig;
use crate::event::Event;
use crate::fees::providers::FeesBlockchainInfo;
use crate::fees::{spawn_fees_checker, FeesState};
use crate::node::{Node, NodeProvider, NodeState};
use crate::node::{Node, NodeState};
use crate::price::providers::coinbase::PriceCoinbase;
use crate::price::{spawn_price_checker, PriceCurrency, PriceState};
use crate::widget::{DynamicNodeStatefulWidget, DynamicState};
Expand Down Expand Up @@ -41,48 +39,63 @@ pub struct AppState {
pub counter: u8,
pub price: PriceState,
pub fees: FeesState,
pub node: NodeState,
pub widget: Box<dyn DynamicNodeStatefulWidget>,
pub widget_state: Box<dyn DynamicState>,
pub node_states: Vec<NodeState>,
}

pub struct App {
pub node: Node,
pub nodes: Vec<Node>,
pub current_node_index: usize,
pub last_node_switch: Option<Instant>,
pub node_switch_interval: Duration,
pub seconds_until_rotation: u64,
pub thread: AppThread,
pub config: AppConfig,
pub widgets: Vec<Box<dyn DynamicNodeStatefulWidget>>,
pub state: AppState,
pub running: bool,
}

impl App {
pub fn new(
thread: AppThread,
widget: Box<dyn DynamicNodeStatefulWidget>,
widget_state: Box<dyn DynamicState>,
widgets: Vec<Box<dyn DynamicNodeStatefulWidget>>,
widget_states: Vec<Box<dyn DynamicState>>,
config: AppConfig,
) -> Self {
let (args, argv) = argmap::parse(env::args());
let config = AppConfig::new(args, argv).unwrap();
let cloned_thread = thread.clone();
let interval = Duration::from_secs(config.node_switch_interval.parse::<u64>().unwrap_or(5));
let num_nodes = widgets.len();
Self {
running: true,
config,
thread,
node: Node::new(cloned_thread),
nodes: (0..num_nodes)
.map(|_| Node::new(cloned_thread.clone()))
.collect(),
current_node_index: 0,
last_node_switch: None,
node_switch_interval: interval,
seconds_until_rotation: interval.as_secs(),
widgets,
state: AppState {
counter: 0,
price: PriceState::new(),
fees: FeesState::new(),
node: NodeState::new(),
widget,
widget_state,
node_states: widget_states
.into_iter()
.map(|ws| {
let mut ns = NodeState::new();
ns.widget_state = ws;
ns.current_node_index = 0; // Will be updated in tick
ns.total_nodes = num_nodes;
ns.seconds_until_rotation = interval.as_secs();
ns
})
.collect(),
},
}
}

pub fn init_node(&mut self, provider: Box<dyn NodeProvider + Send>) {
self.node.init(provider);
}

pub fn init_price(&mut self) {
spawn_price_checker::<PriceCoinbase>(
self.thread.clone(),
Expand All @@ -95,21 +108,26 @@ impl App {
}

pub fn tick(&mut self) {
let now = Instant::now();
let switch_interval = Duration::from_secs(3);
let keys: Vec<_> = self.state.node.services.keys().cloned().collect();

if !keys.is_empty() {
let should_advance = match self.state.node.last_service_switch {
Some(last) => now.duration_since(last) >= switch_interval,
None => true,
};

if should_advance {
let new_index = (self.state.node.service_display_index + 1) % keys.len();
self.state
.node
.set_last_service_switch(Some(now), new_index);
for (_i, node_state) in self.state.node_states.iter_mut().enumerate() {
node_state.tick();
node_state.current_node_index = self.current_node_index;
node_state.total_nodes = self.nodes.len();
node_state.seconds_until_rotation = self.seconds_until_rotation;
}

if self.nodes.len() > 1 {
let now = Instant::now();
if let Some(last_switch) = self.last_node_switch {
let elapsed = now.duration_since(last_switch).as_secs();
self.seconds_until_rotation =
self.node_switch_interval.as_secs().saturating_sub(elapsed);
if elapsed >= self.node_switch_interval.as_secs() {
self.current_node_index = (self.current_node_index + 1) % self.nodes.len();
self.last_node_switch = Some(now);
self.seconds_until_rotation = self.node_switch_interval.as_secs();
}
} else {
self.last_node_switch = Some(now);
}
}
}
Expand All @@ -134,12 +152,13 @@ impl App {
self.state.price = state;
}

pub fn handle_node_update<F>(&mut self, update_fn: F)
where
F: Fn(NodeState) -> NodeState + Send + Sync,
{
self.state.node = update_fn(self.state.node.clone());
self.state.widget_state = self.state.node.widget_state.clone_box();
pub fn handle_node_update(
&mut self,
index: usize,
update_fn: &(dyn Fn(NodeState) -> NodeState + Send + Sync),
) {
let updated = update_fn(self.state.node_states[index].clone());
self.state.node_states[index] = updated;
}

pub fn handle_fee_update(&mut self, state: FeesState) {
Expand All @@ -156,15 +175,63 @@ impl App {
self.quit();
}
}
KeyCode::Right => {
self.increment_counter();
KeyCode::Right | KeyCode::Char('n') => {
if self.nodes.len() > 1 {
self.current_node_index = (self.current_node_index + 1) % self.nodes.len();
self.last_node_switch = Some(Instant::now());
self.seconds_until_rotation = self.node_switch_interval.as_secs();
}
}
KeyCode::Left => {
self.decrement_counter();
if self.nodes.len() > 1 {
self.current_node_index = if self.current_node_index == 0 {
self.nodes.len() - 1
} else {
self.current_node_index - 1
};
self.last_node_switch = Some(Instant::now());
self.seconds_until_rotation = self.node_switch_interval.as_secs();
}
}
KeyCode::Up => {
if self.nodes.len() > 1 {
let new_interval = self.node_switch_interval.as_secs().saturating_add(1);
self.node_switch_interval = Duration::from_secs(new_interval);
self.seconds_until_rotation = new_interval;
self.last_node_switch = Some(Instant::now());
}
}
KeyCode::Down => {
if self.nodes.len() > 1 {
let new_interval = self.node_switch_interval.as_secs().saturating_sub(1);
self.node_switch_interval = Duration::from_secs(new_interval.max(1));
self.seconds_until_rotation = new_interval.max(1);
self.last_node_switch = Some(Instant::now());
}
}
KeyCode::Char(' ') => {}
_ => {}
}
Ok(())
}
}

pub fn handle_mouse_events(&mut self, mouse_event: MouseEvent) -> AppResult<()> {
if self.nodes.len() > 1 {
match mouse_event.kind {
MouseEventKind::Down(_) => {
let x = mouse_event.column;
let y = mouse_event.row;
let total_height = self.config.tick_rate.parse::<u64>().unwrap() as u16;
let status_panel_height = 1;
let frame_width = 80; // Fixed width for now
if y >= total_height - status_panel_height && x >= frame_width - 25 {
self.current_node_index = (self.current_node_index + 1) % self.nodes.len();
self.last_node_switch = Some(Instant::now());
self.seconds_until_rotation = self.node_switch_interval.as_secs();
}
}
_ => {}
}
}
Ok(())
}
}
48 changes: 32 additions & 16 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use config::{Config, ConfigError, File};
use serde_derive::Deserialize;
use std::collections::HashMap;

#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Deserialize, Clone, Default)]
#[allow(unused)]
pub struct BitcoinCoreSettings {
pub host: String,
Expand All @@ -13,12 +13,20 @@ pub struct BitcoinCoreSettings {
pub zmq_port: String,
}

#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Deserialize, Clone, Default)]
#[allow(unused)]
pub struct CoreLightningSettings {
pub rest_address: String,
pub rest_rune: String,
}

#[derive(Debug, Deserialize, Clone, Default)]
#[allow(unused)]
pub struct LndSettings {
pub rest_address: String,
pub macaroon_hex: String,
}

#[derive(Debug, Deserialize, Clone)]
#[allow(unused)]
pub struct PriceSettings {
Expand All @@ -35,27 +43,26 @@ pub struct FeesSettings {

#[derive(Debug, Deserialize, Clone)]
#[allow(unused)]
pub struct NodeSettings {
pub struct NodeConfig {
pub provider: String,
pub bitcoin_core: Option<BitcoinCoreSettings>,
pub core_lightning: Option<CoreLightningSettings>,
pub lnd: Option<LndSettings>,
}

#[derive(Debug, Deserialize, Clone)]
#[allow(unused)]
pub struct AppConfig {
pub tick_rate: String,
pub streamer_mode: bool,
pub node_switch_interval: String, // New field for rotation time in seconds
pub price: PriceSettings,
pub fees: FeesSettings,
pub bitcoin_core: BitcoinCoreSettings,
pub core_lightning: CoreLightningSettings,
pub lnd: LndSettings,
pub node: NodeSettings,
}

#[derive(Debug, Deserialize, Clone)]
#[allow(unused)]
pub struct LndSettings {
pub rest_address: String,
pub macaroon_hex: String,
#[serde(default)]
pub nodes: Vec<NodeConfig>,
}

fn match_string_to_bool(value: &str) -> bool {
Expand All @@ -74,9 +81,9 @@ impl AppConfig {
let mut s = Config::builder()
// general
.set_default("tick_rate", 250)?
// node provider default
.set_default("node.provider", "bitcoin_core")?
// bitcoin core defaults
.set_default("streamer_mode", false)?
.set_default("node_switch_interval", "5")? // Default rotation time of 5 seconds
// bitcoin core defaults (will be cleared if nodes is used)
.set_default("bitcoin_core.host", "localhost")?
.set_default("bitcoin_core.rpc_port", 8332)?
.set_default("bitcoin_core.rpc_user", "username")?
Expand Down Expand Up @@ -124,7 +131,7 @@ impl AppConfig {
.and_then(|v| Some(v.first().unwrap().as_str()))
{
match key.as_str() {
"price.enabled" | "fees.enabled" => {
"price.enabled" | "fees.enabled" | "streamer_mode" => {
s = s.set_override(key, match_string_to_bool(value))?;
}
_ => {
Expand All @@ -134,6 +141,15 @@ impl AppConfig {
}
}

s.build()?.try_deserialize()
let mut config: AppConfig = s.build()?.try_deserialize()?;

// Clear legacy providers if nodes array is used
if !config.nodes.is_empty() {
config.bitcoin_core = BitcoinCoreSettings::default();
config.core_lightning = CoreLightningSettings::default();
config.lnd = LndSettings::default();
}

Ok(config)
}
}
Loading