diff --git a/pulse-ui/src/layout.rs b/pulse-ui/src/layout.rs index ca37247..9209c9d 100644 --- a/pulse-ui/src/layout.rs +++ b/pulse-ui/src/layout.rs @@ -26,23 +26,54 @@ impl LayoutItem { let allocation = if is_row { alloc.height } else { alloc.width }; - let remaining = allocation - - items - .iter() - .map(|item| item.get_size().get_fixed(allocation)) - .sum::(); + let fixed_total: u16 = items + .iter() + .map(|item| item.get_size().get_fixed(allocation)) + .sum(); + + let remaining = allocation.saturating_sub(fixed_total); let total_weight = items .iter() .map(|item| item.get_size().get_flex()) .sum::(); + let base_allocs: Vec = items + .iter() + .map(|item| item.get_size().get(allocation, remaining, total_weight)) + .collect(); + + let flex_alloc_total: u16 = items + .iter() + .zip(base_allocs.iter()) + .filter(|(item, _)| item.get_size().get_flex() > 0) + .map(|(_, a)| a) + .sum(); + + let gap = remaining.saturating_sub(flex_alloc_total); + + let fill_count = items + .iter() + .filter(|item| item.get_size().is_fill()) + .count() as u16; + + let gap_per_fill = if fill_count > 0 { gap / fill_count } else { 0 }; + let gap_remainder = if fill_count > 0 { gap % fill_count } else { 0 }; + let mut curr = if is_row { alloc.y } else { alloc.x }; + let mut fill_idx = 0u16; - for item in items { + for (i, item) in items.iter().enumerate() { let size = item.get_size(); + let mut unit_alloc = base_allocs[i]; - let unit_alloc = size.get(allocation, remaining, total_weight); + if size.is_fill() { + unit_alloc += gap_per_fill; + if fill_idx == 0 { + unit_alloc += gap_remainder; + } + fill_idx += 1; + } let item_alloc = if is_row { Rect { diff --git a/pulse-ui/src/unit.rs b/pulse-ui/src/unit.rs index 83ff31d..c21b503 100644 --- a/pulse-ui/src/unit.rs +++ b/pulse-ui/src/unit.rs @@ -9,6 +9,7 @@ pub enum Size { Fixed(u16), Flex(u16), Percent(u16), + Fill, } #[derive(Debug, Clone, Copy)] @@ -20,17 +21,22 @@ pub struct Rect { } impl Size { + pub fn is_fill(&self) -> bool { + matches!(self, Self::Fill) + } + pub fn get_fixed(&self, alloc: u16) -> u16 { match self { Self::Fixed(v) => *v, Self::Percent(v) => alloc * (*v) / 100, - Self::Flex(_) => 0, + Self::Flex(_) | Self::Fill => 0, } } pub fn get_flex(&self) -> u16 { match self { Self::Flex(f) => *f, + Self::Fill => 1, _ => 0, } } @@ -46,6 +52,13 @@ impl Size { remaining * v / total_weight } } + Self::Fill => { + if total_weight == 0 { + 0 + } else { + remaining / total_weight + } + } } } } diff --git a/pulse-ui/src/widget/input.rs b/pulse-ui/src/widget/input.rs index 76bcefe..051763e 100644 --- a/pulse-ui/src/widget/input.rs +++ b/pulse-ui/src/widget/input.rs @@ -67,6 +67,41 @@ impl InputState { true } + Event::Key(KeyEvent { + code: KeyCode::Char('h'), + modifiers, + .. + }) if modifiers.contains(KeyModifiers::CONTROL) => { + if self.cursor > 0 { + let prev = self.text[..self.cursor] + .char_indices() + .last() + .map(|(i, _)| i) + .unwrap(); + + self.text.remove(prev); + self.cursor = prev; + } + true + } + + Event::Key(KeyEvent { + code: KeyCode::Delete, + .. + }) => { + if self.cursor < self.text.len() { + let prev = self.text[..self.cursor+1] + .char_indices() + .last() + .map(|(i, _)| i) + .unwrap(); + + self.text.remove(prev); + self.cursor = prev; + } + true + } + Event::Key(KeyEvent { code: KeyCode::Char(c), modifiers, diff --git a/src/formatting.rs b/src/formatting.rs index 2eff77f..00b677d 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -1,13 +1,49 @@ -use crate::types::{Account, ActivePosition, WatchListItem}; +use crate::types::{ + Account, ActivePosition, EventLog, LogKind, Signal, SignalKind, System, WatchListItem, +}; pub trait Formatted { fn get_formatted(&self) -> Vec; } +impl Formatted for EventLog { + fn get_formatted(&self) -> Vec { + vec![ + format!( + "{}{:?}\x1b[0m", + match &self.kind { + LogKind::INFO => "\x1b[34m", + LogKind::DEBUG => "\x1b[36m", + LogKind::ERR => "\x1b[31m", + LogKind::WARN => "\x1b[33m", + }, + self.kind + ), + format!("(\x1b[37m{}\x1b[0m)", self.name), + self.message.clone(), + ] + } +} + +impl Formatted for Signal { + fn get_formatted(&self) -> Vec { + vec![ + if matches!(self.kind, SignalKind::BUY) { + format!("\x1b[32m{:?}\x1b[0m", self.kind) + } else { + format!("\x1b[31m{:?}\x1b[0m", self.kind) + }, + format!("\x1b[35m{}\x1b[0m", self.symbol), + format!("\x1b[34m{:?}\x1b[0m", self.param), + format_f64(self.price), + ] + } +} + impl Formatted for WatchListItem { fn get_formatted(&self) -> Vec { vec![ - format!("\x1b[35m{}\x1b[0m", self.symbol.to_string()), + format!("\x1b[35m{}\x1b[0m", self.symbol), format_f64(self.price), format!( "{} {}", @@ -25,7 +61,7 @@ impl Formatted for WatchListItem { impl Formatted for ActivePosition { fn get_formatted(&self) -> Vec { vec![ - format!("\x1b[35m{}\x1b[0m", self.symbol.to_string()), + format!("\x1b[35m{}\x1b[0m", self.symbol), format_f64(self.amount), format!( "{} {}", @@ -55,6 +91,18 @@ impl Formatted for Account { } } +impl Formatted for System { + fn get_formatted(&self) -> Vec { + vec![ + Property("Feed", format!("{:?}", self.feed)), + Property("Exchange", self.exchange.clone()), + Property("DEX", self.dex.clone()), + Property("Latency", format!("{} ms", self.latency)), + ] + .get_formatted() + } +} + impl Formatted for Property { fn get_formatted(&self) -> Vec { vec![format!("\x1b[91m{}\x1b[0m", self.0), self.1.clone()] diff --git a/src/main.rs b/src/main.rs index 6577301..d990a7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,14 +18,17 @@ use pulse_ui::{ use crate::{ formatting::{Formatted, apply_padding}, - types::{Account, ActivePosition, WatchListItem}, + types::{Account, ActivePosition, EventLog, Signal, System, WatchListItem}, }; pub struct PulseTradeApp { command: State, watch_list: State>, active_positions: State>, + logs: State>, + signals: State>, account: State, + system: State, } impl App for PulseTradeApp { @@ -75,6 +78,29 @@ impl App for PulseTradeApp { profit: 12.30, amount: 0.75, }); + + let mut signals = self.signals.lock().await; + + signals.push(Signal { + kind: types::SignalKind::BUY, + symbol: "BTC".to_string(), + param: types::SignalParameter::LIM, + price: 118_800.0, + }); + + signals.push(Signal { + kind: types::SignalKind::BUY, + symbol: "BTC".to_string(), + param: types::SignalParameter::TAP, + price: 120_000.0, + }); + + signals.push(Signal { + kind: types::SignalKind::BUY, + symbol: "BTC".to_string(), + param: types::SignalParameter::STL, + price: 118_000.0, + }); } async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box) { @@ -111,7 +137,7 @@ impl App for PulseTradeApp { LayoutItem::Widget(Size::Flex(1)), // Event logs LayoutItem::Spacing(Size::Fixed(1)), - LayoutItem::Widget(Size::Flex(1)), + LayoutItem::Widget(Size::Fill), // Input LayoutItem::Spacing(Size::Fixed(1)), LayoutItem::Widget(Size::Fixed(1)), @@ -166,32 +192,29 @@ impl App for PulseTradeApp { ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new( - vec![ - " SIGNALS", - " BUY BTC LIM 118,800.12", - " BUY BTC STL 118,000.00", - " BUY BTC TAP 120,000.00", - ] - .join("\n"), - ), + Box::new(format!( + " SIGNALS\n{}", + apply_padding(self.signals.lock().await.get_formatted()).join("\n") + )), ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new( - vec![ - " SYSTEM", - " Feed: Connected", - " Exchange: Binance", - " DEX: DEX SCREENER", - " Latency: 18 ms", - ] - .join("\n"), - ), + Box::new(format![ + " SYSTEM\n{}", + apply_padding(self.system.lock().await.get_formatted()).join("\n") + ]), ), ]), ); + layout.draw( + 5, + format![ + " EVENT LOGS\n{}", + apply_padding(self.logs.lock().await.get_formatted()).join("\n") + ], + ); + layout.draw(6, Input(" > ", &*self.command.lock().await)); } } @@ -202,6 +225,8 @@ async fn main() { command: ctx.use_state(InputState::new()), watch_list: ctx.use_state(Vec::new()), active_positions: ctx.use_state(Vec::new()), + signals: ctx.use_state(Vec::new()), + logs: ctx.use_state(Vec::new()), account: ctx.use_state(Account { equity: 25_483.21, liquid: 11_928.43, @@ -209,6 +234,12 @@ async fn main() { realized: 2_182.49, margin: 0.0, }), + system: ctx.use_state(System { + feed: types::Feed::Connected, + exchange: "Binance".to_string(), + dex: "DEX SCREENER".to_string(), + latency: 18, + }), }) .await; } diff --git a/src/types.rs b/src/types.rs index 3c3b4c1..758d400 100644 --- a/src/types.rs +++ b/src/types.rs @@ -17,3 +17,54 @@ pub struct Account { pub realized: f64, pub margin: f64, } + +#[derive(Debug, Clone, Copy)] +pub enum Feed { + Connected, + Disconnected, + Connecting, + Failed, +} + +pub struct System { + pub feed: Feed, + pub exchange: String, + pub dex: String, + pub latency: u16, +} + +#[derive(Debug, Clone, Copy)] +pub enum SignalKind { + BUY, + SELL, +} + +#[derive(Debug, Clone, Copy)] +pub enum SignalParameter { + LIM, + STL, + TAP, + CHK, +} + +pub struct Signal { + pub kind: SignalKind, + pub symbol: String, + pub param: SignalParameter, + pub price: f64, +} + +#[derive(Debug, Clone, Copy)] +pub enum LogKind { + INFO, + WARN, + ERR, + DEBUG, +} + +#[derive(Debug, Clone)] +pub struct EventLog { + pub kind: LogKind, + pub name: &'static str, + pub message: String, +}