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_sdk::prelude::*; pub struct PulseTradeApp { sock: Option, command: State, scroll: State>, strategy: State>, status: State>, watch_list: State>, active_positions: State>, logs: State>, signals: State>, inspect: State, } impl App for PulseTradeApp { async fn init(&mut self, _ctx: &pulse_ui::state::Context) {} async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box) { if let Some(Refresh) = event.downcast_ref() { return; } else 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, "POSITIONS", &self.active_positions).await, ), ), ( LayoutItem::Widget(Size::Flex(1)), Box::new(advanced_draw(&self.scroll, 2, "SIGNALS", &self.signals).await), ), ]), ); layout.draw( 4, SpacedColumns(vec![ ( LayoutItem::Widget(Size::Flex(1)), Box::new( advanced_option_draw(&self.scroll, 3, "CONFIGURATION", &self.strategy) .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_option_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 client = terminal::TerminalClient::new().await?; pulse_ui::run(|ctx| { client.use_app(PulseTradeApp { sock: None, command: ctx.use_state(InputState::new()), scroll: ctx.use_state(ScrollState(1, [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), strategy: ctx.use_state(None), status: ctx.use_state(None), }) }) .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"), ) } pub async fn advanced_option_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)), if let Some(state) = &*state.lock().await { apply_padding(state.get_formatted()).join("\n") } else { " Loading..".to_string() }, ) }