pub mod command; pub mod formatting; pub mod terminal; use std::any::Any; use chrono::{Local, Utc}; use pulse_ui::{ App, layout::{LayoutItem, layout}, state::{Refresh, State}, unit::Size, widget::{ align::End, input::{Input, InputState}, outline::{Outline, VLine}, scroll::{ScrollState, ScrollText}, spaced::SpacedColumns, }, }; use crate::formatting::{Formatted, apply_padding}; use pulse_wire::terminal::{ ActivePosition, Alert, EventLog, InspectTarget, MarketOverview, Signal, Status, WatchListItem, }; pub struct PulseTradeApp { command: State, scroll: State>, watch_list: State>, active_positions: State>, logs: State>, signals: State>, market_overview: State, status: State, inspect: State, } impl App for PulseTradeApp { async fn init(&mut self, _ctx: &pulse_ui::state::Context) { let mut watch_list = self.watch_list.lock().await; watch_list.push(WatchListItem { symbol: "BTC".to_string(), price: 118_402.12, trend: 0.82, }); watch_list.push(WatchListItem { symbol: "ETH".to_string(), price: 3_912.48, trend: -0.41, }); watch_list.push(WatchListItem { symbol: "SOL".to_string(), price: 182.91, trend: 2.18, }); watch_list.push(WatchListItem { symbol: "XRP".to_string(), price: 2.84, trend: 1.22, }); let mut active_positions = self.active_positions.lock().await; active_positions.push(ActivePosition { symbol: "BTC".to_string(), profit: 125.50, amount: 0.25, }); active_positions.push(ActivePosition { symbol: "ETH".to_string(), profit: -32.75, amount: 1.0, }); active_positions.push(ActivePosition { symbol: "SOL".to_string(), profit: 84.20, amount: 5.0, }); active_positions.push(ActivePosition { symbol: "XRP".to_string(), profit: 12.30, amount: 0.75, }); let mut signals = self.signals.lock().await; signals.push(Signal { kind: pulse_wire::terminal::SignalKind::Buy, symbol: "BTC".to_string(), param: pulse_wire::terminal::SignalParameter::Lim, price: 118_800.0, }); signals.push(Signal { kind: pulse_wire::terminal::SignalKind::Buy, symbol: "BTC".to_string(), param: pulse_wire::terminal::SignalParameter::Tap, price: 120_000.0, }); signals.push(Signal { kind: pulse_wire::terminal::SignalKind::Buy, symbol: "BTC".to_string(), param: pulse_wire::terminal::SignalParameter::Stl, price: 118_000.0, }); let mut logs = self.logs.lock().await; logs.push(EventLog { kind: pulse_wire::terminal::LogKind::Warn, name: "pulse.init".to_string(), message: "We're still not done yet ;)".to_string(), }); let mut market_overview = self.market_overview.lock().await; market_overview.alerts.push(Alert { level: pulse_wire::terminal::AlertLevel::High, message: "BTC funding rate elevated".to_string(), }); market_overview.alerts.push(Alert { level: pulse_wire::terminal::AlertLevel::Medium, message: "Market volatility increasing".to_string(), }); market_overview.alerts.push(Alert { level: pulse_wire::terminal::AlertLevel::Low, message: "ETH volatility returning to normal".to_string(), }); } async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box) { if let Some(Refresh) = event.downcast_ref() { return; } if let Some(event) = event.downcast_ref() { if self.command.value.lock().await.handle_event(event) { return; } else if let crossterm::event::Event::Key(key) = event { if key.code.is_enter() { let mut command = self.command.lock().await; let command_text = command.text.clone(); command.cursor = 0; command.text.clear(); drop(command); self.execute_command(ctx, command_text.trim()).await; } else if key.code.is_up() { self.scroll.value.lock().await.up(); } else if key.code.is_down() { self.scroll.value.lock().await.down(); } else if key.code.is_back_tab() { self.scroll.value.lock().await.back_tab(); } else if key.code.is_tab() { self.scroll.value.lock().await.tab(); } } } } async fn layout(&self) -> pulse_ui::layout::LayoutItem { LayoutItem::Frame { padding: 1, item: Box::new(layout(vec![ LayoutItem::Columns { unit: Size::Fixed(1), items: vec![ LayoutItem::Widget(Size::Flex(1)), LayoutItem::Widget(Size::Flex(1)), LayoutItem::Widget(Size::Flex(1)), ], }, // Account status LayoutItem::Spacing(Size::Fixed(1)), LayoutItem::Widget(Size::Flex(1)), // Live status LayoutItem::Spacing(Size::Fixed(1)), LayoutItem::Widget(Size::Flex(1)), // Event logs LayoutItem::Spacing(Size::Fixed(1)), LayoutItem::Widget(Size::Fill), // Input LayoutItem::Spacing(Size::Fixed(1)), LayoutItem::Widget(Size::Fixed(1)), ])), } } async fn render(&mut self, layout: pulse_ui::layout::Allocation) { layout.draw_frame(0, Outline); layout.draw( 0, format!("  PULSE TRADER v{}", env!("CARGO_PKG_VERSION")), ); // layout.draw(1, Center("LIVE".to_string())); layout.draw( 2, End(format!( "{} UTC ({} Local)", Utc::now().format("%H:%M"), Local::now().format("%H:%M") )), ); layout.draw_frame(3, VLine); layout.draw_frame(4, VLine); layout.draw_frame(5, VLine); layout.draw_frame(6, VLine); layout.draw( 3, SpacedColumns(vec![ ( LayoutItem::Widget(Size::Flex(1)), Box::new(advanced_draw(&self.scroll, 0, "WATCH LIST", &self.watch_list).await), ), ( LayoutItem::Widget(Size::Flex(1)), Box::new( advanced_draw(&self.scroll, 1, "ACTIVE POSITIONS", &self.active_positions) .await, ), ), ( LayoutItem::Widget(Size::Flex(1)), Box::new( advanced_draw(&self.scroll, 2, "MARKET OVERVIEW", &self.market_overview) .await, ), ), ]), ); layout.draw( 4, SpacedColumns(vec![ ( LayoutItem::Widget(Size::Flex(1)), Box::new(advanced_draw(&self.scroll, 3, "SIGNALS", &self.signals).await), ), ( LayoutItem::Widget(Size::Flex(1)), Box::new(advanced_draw(&self.scroll, 4, "INSPECTOR", &self.inspect).await), ), ( LayoutItem::Widget(Size::Flex(1)), Box::new(advanced_draw(&self.scroll, 5, "STATUS", &self.status).await), ), ]), ); layout.draw( 5, advanced_draw(&self.scroll, 6, "EVENT LOGS", &self.logs).await, ); layout.draw(6, Input(" > ", &*self.command.lock().await)); } } #[tokio::main] async fn main() -> tokio::io::Result<()> { let mut client = terminal::TerminalClient::new().await?; client .send(pulse_wire::terminal::TerminalClientMessage::ExecuteCommand( "Hello".to_string(), )) .await?; pulse_ui::run(|ctx| { client.use_app(PulseTradeApp { command: ctx.use_state(InputState::new()), scroll: ctx.use_state(ScrollState(0, [0; 7])), 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()), inspect: ctx.use_state(InspectTarget::None), market_overview: ctx.use_state(MarketOverview { trend: pulse_wire::terminal::MarketTrend::Bullish, volatility: pulse_wire::terminal::Volatility::High, pressure: 0.324, alerts: Vec::new(), }), status: ctx.use_state(Status { feed: pulse_wire::terminal::Feed::Connected, exchange: "Binance".to_string(), dex: "DEX SCREENER".to_string(), latency: 18, }), }) }) .await; Ok(()) } pub async fn advanced_draw( scroll: &State>, index: usize, title: &'static str, state: &State, ) -> ScrollText { let scroll = scroll.lock().await; scroll.scroll( index, format!(" {}{title}\x1b[0m", scroll.get_selected(index)), apply_padding(state.lock().await.get_formatted()).join("\n"), ) }