From 755f6476c559b35023e7695ae0571b01eedd6684 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 09:29:59 +0200 Subject: [PATCH 01/11] Vline --- pulse-ui/src/layout.rs | 2 +- pulse-ui/src/widget/outline.rs | 30 ++++++++++++++++++++++++++ src/main.rs | 39 +++++++++++++++++----------------- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/pulse-ui/src/layout.rs b/pulse-ui/src/layout.rs index 2d992cb..ca37247 100644 --- a/pulse-ui/src/layout.rs +++ b/pulse-ui/src/layout.rs @@ -114,7 +114,7 @@ impl Rect { } pub fn layout(rows: Vec) -> LayoutItem { - LayoutItem::Columns { + LayoutItem::Rows { unit: Size::Percent(100), items: rows, } diff --git a/pulse-ui/src/widget/outline.rs b/pulse-ui/src/widget/outline.rs index 8ff1e8c..0d0d75f 100644 --- a/pulse-ui/src/widget/outline.rs +++ b/pulse-ui/src/widget/outline.rs @@ -23,3 +23,33 @@ impl Widget for Outline { scope.draw_text((scope.rect.width - 1, scope.rect.height - 1), "┘"); } } + +pub struct TVLine; + +impl Widget for TVLine { + fn render(&self, scope: &mut crate::render::RenderScope) { + scope.draw_text(0, "─".repeat(scope.rect.width as usize)); + + scope.draw_text(0, "┌"); + scope.draw_text((scope.rect.width - 1, 0), "┐"); + } +} + +pub struct BVLine; + +impl Widget for BVLine { + fn render(&self, scope: &mut crate::render::RenderScope) { + scope.draw_text(0, "─".repeat(scope.rect.width as usize)); + + scope.draw_text(0, "└"); + scope.draw_text((scope.rect.width - 1, 0), "┘"); + } +} + +pub struct VLine; + +impl Widget for VLine { + fn render(&self, scope: &mut crate::render::RenderScope) { + scope.draw_text(0, "─".repeat(scope.rect.width as usize)); + } +} diff --git a/src/main.rs b/src/main.rs index 414d870..e7dedfe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,11 @@ use std::any::Any; use pulse_ui::{ - App, layout::{LayoutItem, layout}, state::{Refresh, State}, unit::Size, widget::{outline::Outline, spaced::{SpacedColumns, SpacedRows}}, + App, + layout::{LayoutItem, layout}, + state::{Refresh, State}, + unit::Size, + widget::outline::VLine, }; pub struct PulseTradeApp { @@ -24,27 +28,24 @@ impl App for PulseTradeApp { } async fn layout(&self) -> pulse_ui::layout::LayoutItem { - layout(vec![LayoutItem::Frame { - padding: 1, - item: Box::new(LayoutItem::Widget(Size::Flex(1))), - }]) + 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)), + ], + }, + LayoutItem::Spacing(Size::Fixed(1)), + ]) } async fn render(&mut self, layout: pulse_ui::layout::Allocation) { - layout.draw_frame(0, Outline); - - layout.draw( - 0, - SpacedColumns(vec![ - (LayoutItem::Widget(Size::Flex(1)), Box::new("one")), - (LayoutItem::Widget(Size::Flex(1)), Box::new("two")), - (LayoutItem::Widget(Size::Flex(1)), Box::new("three")), - ( - LayoutItem::Widget(Size::Flex(1)), - Box::new(self.count.display().await), - ), - ]), - ); + layout.draw_frame(2, VLine); + layout.draw(0, "one"); + layout.draw(1, "two"); + layout.draw(2, "three"); } } From 7530ae5849c333490a75328cdb12de16c8917230 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 09:35:57 +0200 Subject: [PATCH 02/11] Top table --- src/main.rs | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main.rs b/src/main.rs index e7dedfe..1e660af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,10 @@ use pulse_ui::{ layout::{LayoutItem, layout}, state::{Refresh, State}, unit::Size, - widget::outline::VLine, + widget::{ + center::Center, + outline::{Outline, VLine}, + }, }; pub struct PulseTradeApp { @@ -28,24 +31,28 @@ impl App for PulseTradeApp { } async fn layout(&self) -> pulse_ui::layout::LayoutItem { - 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)), - ], - }, - LayoutItem::Spacing(Size::Fixed(1)), - ]) + 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)), + ], + }, + LayoutItem::Spacing(Size::Fixed(1)), + ])), + } } async fn render(&mut self, layout: pulse_ui::layout::Allocation) { - layout.draw_frame(2, VLine); - layout.draw(0, "one"); - layout.draw(1, "two"); - layout.draw(2, "three"); + layout.draw_frame(0, Outline); + layout.draw_frame(3, VLine); + layout.draw(0, format!("PULSETRADER v0.1.0")); + layout.draw(1, Center("LIVE".to_string())); + layout.draw(2, Center(format!("14:32:51 UTC"))); } } From 6283ff8f9122e25ed88a874ce55ac8c05618caa4 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 09:57:18 +0200 Subject: [PATCH 03/11] App Layout --- src/main.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 1e660af..9071d16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use pulse_ui::{ widget::{ center::Center, outline::{Outline, VLine}, + spaced::{SpacedColumns, SpacedRows}, }, }; @@ -42,17 +43,66 @@ impl App for PulseTradeApp { 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::Flex(1)), + // 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_frame(3, VLine); layout.draw(0, format!("PULSETRADER v0.1.0")); layout.draw(1, Center("LIVE".to_string())); layout.draw(2, Center(format!("14:32:51 UTC"))); + 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(SpacedRows(vec![])), + ), + ( + LayoutItem::Widget(Size::Flex(1)), + Box::new(SpacedRows(vec![])), + ), + ( + LayoutItem::Widget(Size::Flex(1)), + Box::new(SpacedRows(vec![])), + ), + ]), + ); + layout.draw( + 4, + SpacedColumns(vec![ + ( + LayoutItem::Widget(Size::Flex(1)), + Box::new(SpacedRows(vec![])), + ), + ( + LayoutItem::Widget(Size::Flex(1)), + Box::new(SpacedRows(vec![])), + ), + ( + LayoutItem::Widget(Size::Flex(1)), + Box::new(SpacedRows(vec![])), + ), + ]), + ); } } From 42284ac7485968a85866f8bbbbceaf2286750b8e Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 10:04:15 +0200 Subject: [PATCH 04/11] Dynamic and End aling --- pulse-ui/src/widget/{center.rs => align.rs} | 19 +++++++++++++++++++ pulse-ui/src/widget/dynamic.rs | 9 +++++++++ pulse-ui/src/widget/mod.rs | 3 ++- src/main.rs | 2 +- 4 files changed, 31 insertions(+), 2 deletions(-) rename pulse-ui/src/widget/{center.rs => align.rs} (52%) create mode 100644 pulse-ui/src/widget/dynamic.rs diff --git a/pulse-ui/src/widget/center.rs b/pulse-ui/src/widget/align.rs similarity index 52% rename from pulse-ui/src/widget/center.rs rename to pulse-ui/src/widget/align.rs index 33ac06e..b72c7fd 100644 --- a/pulse-ui/src/widget/center.rs +++ b/pulse-ui/src/widget/align.rs @@ -18,3 +18,22 @@ impl Widget for Center { scope.draw_text((x, y), &self.0); } } + +pub struct End(pub String); + +impl Widget for End { + fn render(&self, scope: &mut crate::render::RenderScope) { + let mut width = 0; + let mut height = 0; + + for line in self.0.lines() { + height += 1; + width = width.max(line.len() as u16); + } + + let x = scope.rect.width - width; + let y = scope.rect.height - height; + + scope.draw_text((x, y), &self.0); + } +} diff --git a/pulse-ui/src/widget/dynamic.rs b/pulse-ui/src/widget/dynamic.rs new file mode 100644 index 0000000..9b76bb4 --- /dev/null +++ b/pulse-ui/src/widget/dynamic.rs @@ -0,0 +1,9 @@ +use crate::{unit::Rect, widget::Widget}; + +pub struct Dynamic(pub fn(Rect) -> Box); + +impl Widget for Dynamic { + fn render(&self, scope: &mut crate::render::RenderScope) { + (self.0)(scope.rect).render(scope); + } +} diff --git a/pulse-ui/src/widget/mod.rs b/pulse-ui/src/widget/mod.rs index d75c0e6..d1b3b3b 100644 --- a/pulse-ui/src/widget/mod.rs +++ b/pulse-ui/src/widget/mod.rs @@ -1,6 +1,7 @@ -pub mod center; +pub mod align; pub mod outline; pub mod spaced; +pub mod dynamic; use crate::render::RenderScope; diff --git a/src/main.rs b/src/main.rs index 9071d16..2ce7d3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use pulse_ui::{ state::{Refresh, State}, unit::Size, widget::{ - center::Center, + align::Center, outline::{Outline, VLine}, spaced::{SpacedColumns, SpacedRows}, }, From 87b561693f74d4c836b658d306e00c85ded0cf86 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 10:23:51 +0200 Subject: [PATCH 05/11] Account status --- pulse-ui/src/widget/dynamic.rs | 4 +-- src/main.rs | 51 ++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/pulse-ui/src/widget/dynamic.rs b/pulse-ui/src/widget/dynamic.rs index 9b76bb4..939e481 100644 --- a/pulse-ui/src/widget/dynamic.rs +++ b/pulse-ui/src/widget/dynamic.rs @@ -1,8 +1,8 @@ use crate::{unit::Rect, widget::Widget}; -pub struct Dynamic(pub fn(Rect) -> Box); +pub struct Dynamic(pub fn(Rect) -> W); -impl Widget for Dynamic { +impl Widget for Dynamic { fn render(&self, scope: &mut crate::render::RenderScope) { (self.0)(scope.rect).render(scope); } diff --git a/src/main.rs b/src/main.rs index 2ce7d3b..87e87ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::any::Any; use pulse_ui::{ App, layout::{LayoutItem, layout}, - state::{Refresh, State}, + state::Refresh, unit::Size, widget::{ align::Center, @@ -12,9 +12,7 @@ use pulse_ui::{ }, }; -pub struct PulseTradeApp { - count: State, -} +pub struct PulseTradeApp {} impl App for PulseTradeApp { async fn init(&mut self, ctx: &pulse_ui::state::Context) {} @@ -24,11 +22,7 @@ impl App for PulseTradeApp { return; } - *self.count.lock().await += 1; - - if *self.count.lock().await > 10 { - ctx.close().await; - } + ctx.close().await; } async fn layout(&self) -> pulse_ui::layout::LayoutItem { @@ -61,7 +55,7 @@ impl App for PulseTradeApp { async fn render(&mut self, layout: pulse_ui::layout::Allocation) { layout.draw_frame(0, Outline); - layout.draw(0, format!("PULSETRADER v0.1.0")); + layout.draw(0, format!(" PULSE TRADER v0.1.0")); layout.draw(1, Center("LIVE".to_string())); layout.draw(2, Center(format!("14:32:51 UTC"))); layout.draw_frame(3, VLine); @@ -74,15 +68,41 @@ impl App for PulseTradeApp { SpacedColumns(vec![ ( LayoutItem::Widget(Size::Flex(1)), - Box::new(SpacedRows(vec![])), + Box::new( + vec![ + "WATCHLIST", + "BTC 118,402.12 ▲ 0.82%", + "ETH 3,912.48 ▼ 0.41%", + "SOL 182.91 ▲ 2.18%", + "XRP 2.84 ▲ 1.22%", + ] + .join("\n"), + ), ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new(SpacedRows(vec![])), + Box::new( + vec![ + "ACTIVE POSITIONS", + "BTC +4,120.40 20,000.00", + "SOL +2,435.40 20,000.00", + ] + .join("\n"), + ), ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new(SpacedRows(vec![])), + Box::new( + vec![ + "ACCOUNT", + "Equity: $25,483.21", + "Liquid: $11,928.43", + "Unreal: +$483.12", + "Realized: +$2,182.49", + "Margin: 0.00%", + ] + .join("\n"), + ), ), ]), ); @@ -108,8 +128,5 @@ impl App for PulseTradeApp { #[tokio::main] async fn main() { - pulse_ui::run(|ctx| PulseTradeApp { - count: ctx.use_state(0), - }) - .await; + pulse_ui::run(|_ctx| PulseTradeApp {}).await; } From 23a79f2db6b92b684afccdcaf74ef43e1e585f83 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 10:29:30 +0200 Subject: [PATCH 06/11] Live status --- src/main.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 87e87ab..e407661 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,20 +106,37 @@ impl App for PulseTradeApp { ), ]), ); + layout.draw( 4, SpacedColumns(vec![ ( LayoutItem::Widget(Size::Flex(1)), - Box::new(SpacedRows(vec![])), + Box::new(vec!["ACTIVE STRATEGIES"].join("\n")), ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new(SpacedRows(vec![])), + Box::new( + vec![ + "SIGNALS", + "BUY BTC LIM 118,800.12", + "STL BTC 120,00.00", + ] + .join("\n"), + ), ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new(SpacedRows(vec![])), + Box::new( + vec![ + "SYSTEM", + "Feed: Connected", + "Exchange: Binance", + "DEX: DEX SCREENER", + "Latency: 18 ms", + ] + .join("\n"), + ), ), ]), ); From eb670ffe8c235b36c2babc408a83f503e1ec63f0 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 10:55:47 +0200 Subject: [PATCH 07/11] Command Input --- pulse-ui/src/state.rs | 10 ++++ pulse-ui/src/widget/input.rs | 93 ++++++++++++++++++++++++++++++++++++ pulse-ui/src/widget/mod.rs | 1 + src/main.rs | 30 ++++++++---- 4 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 pulse-ui/src/widget/input.rs diff --git a/pulse-ui/src/state.rs b/pulse-ui/src/state.rs index 7ff8e6b..f84f952 100644 --- a/pulse-ui/src/state.rs +++ b/pulse-ui/src/state.rs @@ -10,6 +10,7 @@ pub struct Context { pub(crate) tx: Sender>, } +#[derive(Debug)] pub struct State { pub value: Arc>, tx: Sender>, @@ -46,6 +47,15 @@ impl State { } } +impl Clone for State { + fn clone(&self) -> Self { + State { + value: self.value.clone(), + tx: self.tx.clone(), + } + } +} + impl<'a, T> std::ops::Deref for StateGuard<'a, T> { type Target = T; diff --git a/pulse-ui/src/widget/input.rs b/pulse-ui/src/widget/input.rs new file mode 100644 index 0000000..76bcefe --- /dev/null +++ b/pulse-ui/src/widget/input.rs @@ -0,0 +1,93 @@ +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; + +use crate::widget::Widget; + +#[derive(Debug, Clone)] +pub struct InputState { + pub text: String, + pub cursor: usize, +} + +impl InputState { + pub fn new() -> Self { + Self { + text: String::new(), + cursor: 0, + } + } +} + +pub struct Input<'a>(pub &'a str, pub &'a InputState); + +impl<'a> Widget for Input<'a> { + fn render(&self, scope: &mut crate::render::RenderScope) { + let mut text = self.1.text.clone(); + text.insert_str(self.1.cursor, "❘"); + scope.draw_text(0, format!("{}{text}", self.0)); + } +} + +impl InputState { + pub fn handle_event(&mut self, event: &Event) -> bool { + match event { + Event::Key(KeyEvent { + code: KeyCode::Left, + .. + }) => { + if self.cursor > 0 { + self.cursor -= 1; + } + true + } + + Event::Key(KeyEvent { + code: KeyCode::Right, + .. + }) => { + if self.cursor < self.text.len() { + self.cursor += 1; + } + true + } + + Event::Key(KeyEvent { + code: KeyCode::Backspace, + .. + }) => { + 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::Char(c), + modifiers, + .. + }) => { + if !modifiers.contains(KeyModifiers::CONTROL) + && !modifiers.contains(KeyModifiers::ALT) + { + self.text.insert(self.cursor, *c); + self.cursor += c.len_utf8(); + } + true + } + + Event::Paste(content) => { + self.text.insert_str(self.cursor, content); + self.cursor += content.len(); + true + } + + _ => false, + } + } +} diff --git a/pulse-ui/src/widget/mod.rs b/pulse-ui/src/widget/mod.rs index d1b3b3b..dc75b5f 100644 --- a/pulse-ui/src/widget/mod.rs +++ b/pulse-ui/src/widget/mod.rs @@ -2,6 +2,7 @@ pub mod align; pub mod outline; pub mod spaced; pub mod dynamic; +pub mod input; use crate::render::RenderScope; diff --git a/src/main.rs b/src/main.rs index e407661..f419680 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,16 +3,19 @@ use std::any::Any; use pulse_ui::{ App, layout::{LayoutItem, layout}, - state::Refresh, + state::{Refresh, State}, unit::Size, widget::{ align::Center, + input::{Input, InputState}, outline::{Outline, VLine}, - spaced::{SpacedColumns, SpacedRows}, + spaced::SpacedColumns, }, }; -pub struct PulseTradeApp {} +pub struct PulseTradeApp { + command: State, +} impl App for PulseTradeApp { async fn init(&mut self, ctx: &pulse_ui::state::Context) {} @@ -22,6 +25,12 @@ impl App for PulseTradeApp { return; } + if let Some(event) = event.downcast_ref() { + if self.command.value.lock().await.handle_event(event) { + return; + } + } + ctx.close().await; } @@ -117,12 +126,8 @@ impl App for PulseTradeApp { ( LayoutItem::Widget(Size::Flex(1)), Box::new( - vec![ - "SIGNALS", - "BUY BTC LIM 118,800.12", - "STL BTC 120,00.00", - ] - .join("\n"), + vec!["SIGNALS", "BUY BTC LIM 118,800.12", "STL BTC 120,00.00"] + .join("\n"), ), ), ( @@ -140,10 +145,15 @@ impl App for PulseTradeApp { ), ]), ); + + layout.draw(6, Input("> ", &*self.command.lock().await)); } } #[tokio::main] async fn main() { - pulse_ui::run(|_ctx| PulseTradeApp {}).await; + pulse_ui::run(|ctx| PulseTradeApp { + command: ctx.use_state(InputState::new()), + }) + .await; } From c773bfb2bec3b5022c5315ca9059eb62d64046dd Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 10:58:17 +0200 Subject: [PATCH 08/11] Item spacing --- src/main.rs | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index f419680..c1565d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,11 +79,11 @@ impl App for PulseTradeApp { LayoutItem::Widget(Size::Flex(1)), Box::new( vec![ - "WATCHLIST", - "BTC 118,402.12 ▲ 0.82%", - "ETH 3,912.48 ▼ 0.41%", - "SOL 182.91 ▲ 2.18%", - "XRP 2.84 ▲ 1.22%", + " WATCHLIST", + " BTC 118,402.12 ▲ 0.82%", + " ETH 3,912.48 ▼ 0.41%", + " SOL 182.91 ▲ 2.18%", + " XRP 2.84 ▲ 1.22%", ] .join("\n"), ), @@ -92,9 +92,9 @@ impl App for PulseTradeApp { LayoutItem::Widget(Size::Flex(1)), Box::new( vec![ - "ACTIVE POSITIONS", - "BTC +4,120.40 20,000.00", - "SOL +2,435.40 20,000.00", + " ACTIVE POSITIONS", + " BTC +4,120.40 20,000.00", + " SOL +2,435.40 20,000.00", ] .join("\n"), ), @@ -103,12 +103,12 @@ impl App for PulseTradeApp { LayoutItem::Widget(Size::Flex(1)), Box::new( vec![ - "ACCOUNT", - "Equity: $25,483.21", - "Liquid: $11,928.43", - "Unreal: +$483.12", - "Realized: +$2,182.49", - "Margin: 0.00%", + " ACCOUNT", + " Equity: $25,483.21", + " Liquid: $11,928.43", + " Unreal: +$483.12", + " Realized: +$2,182.49", + " Margin: 0.00%", ] .join("\n"), ), @@ -121,24 +121,28 @@ impl App for PulseTradeApp { SpacedColumns(vec![ ( LayoutItem::Widget(Size::Flex(1)), - Box::new(vec!["ACTIVE STRATEGIES"].join("\n")), + Box::new(vec![" ACTIVE STRATEGIES"].join("\n")), ), ( LayoutItem::Widget(Size::Flex(1)), Box::new( - vec!["SIGNALS", "BUY BTC LIM 118,800.12", "STL BTC 120,00.00"] - .join("\n"), + vec![ + " SIGNALS", + " BUY BTC LIM 118,800.12", + " STL BTC 120,00.00", + ] + .join("\n"), ), ), ( LayoutItem::Widget(Size::Flex(1)), Box::new( vec![ - "SYSTEM", - "Feed: Connected", - "Exchange: Binance", - "DEX: DEX SCREENER", - "Latency: 18 ms", + " SYSTEM", + " Feed: Connected", + " Exchange: Binance", + " DEX: DEX SCREENER", + " Latency: 18 ms", ] .join("\n"), ), From d2cc33d5176946bb93f5c99f7de092aedaf44b57 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 11:42:00 +0200 Subject: [PATCH 09/11] Types, and testing formatted view --- src/main.rs | 50 ++++++++++++++++++++-------- src/types.rs | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 src/types.rs diff --git a/src/main.rs b/src/main.rs index c1565d2..0a72b37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +pub mod types; + use std::any::Any; use pulse_ui::{ @@ -13,12 +15,38 @@ use pulse_ui::{ }, }; +use crate::types::{Formatted, WatchListItem, space_out}; + pub struct PulseTradeApp { command: State, + watch_list: State>, } impl App for PulseTradeApp { - async fn init(&mut self, ctx: &pulse_ui::state::Context) {} + 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, + }); + } async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box) { if let Some(Refresh) = event.downcast_ref() { @@ -77,16 +105,10 @@ impl App for PulseTradeApp { SpacedColumns(vec![ ( LayoutItem::Widget(Size::Flex(1)), - Box::new( - vec![ - " WATCHLIST", - " BTC 118,402.12 ▲ 0.82%", - " ETH 3,912.48 ▼ 0.41%", - " SOL 182.91 ▲ 2.18%", - " XRP 2.84 ▲ 1.22%", - ] - .join("\n"), - ), + Box::new(format!( + " WATCHLIST\n{}", + space_out(self.watch_list.lock().await.get_formatted()).join("\n") + )), ), ( LayoutItem::Widget(Size::Flex(1)), @@ -129,7 +151,8 @@ impl App for PulseTradeApp { vec![ " SIGNALS", " BUY BTC LIM 118,800.12", - " STL BTC 120,00.00", + " BUY BTC STL 118,000.00", + " BUY BTC TAP 120,000.00", ] .join("\n"), ), @@ -150,7 +173,7 @@ impl App for PulseTradeApp { ]), ); - layout.draw(6, Input("> ", &*self.command.lock().await)); + layout.draw(6, Input(" > ", &*self.command.lock().await)); } } @@ -158,6 +181,7 @@ impl App for PulseTradeApp { async fn main() { pulse_ui::run(|ctx| PulseTradeApp { command: ctx.use_state(InputState::new()), + watch_list: ctx.use_state(Vec::new()), }) .await; } diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..9a5c1de --- /dev/null +++ b/src/types.rs @@ -0,0 +1,94 @@ +pub trait Formatted { + fn get_formatted(&self) -> Vec; +} + +pub struct WatchListItem { + pub symbol: String, + pub price: f64, + pub trend: f64, +} + +pub struct ActivePosition { + pub symbol: String, + pub profit: f64, + pub lot: f64, +} + +pub struct Account { + pub equity: f64, + pub liquid: f64, + pub unreal: f64, + pub realized: f64, + pub margin: f64, +} + +impl Formatted for Vec { + fn get_formatted(&self) -> Vec { + let mut output = Vec::new(); + + let rows: Vec> = self.iter().map(|v| v.get_formatted()).collect(); + + if rows.is_empty() { + return output; + } + + let column_count = rows.iter().map(|r| r.len()).max().unwrap_or(0); + + let mut widths = vec![0; column_count]; + + // Find the largest width per column + for row in &rows { + for (i, col) in row.iter().enumerate() { + widths[i] = widths[i].max(col.len()); + } + } + + // Render rows + for (row_idx, row) in rows.iter().enumerate() { + let mut row_out = String::new(); + + for (col_idx, col) in row.iter().enumerate() { + let width = widths[col_idx]; + + // Right-align numbers, left-align text could be customized here + row_out.push_str(&format!("{:>width$}", col, width = width)); + + if col_idx + 1 != row.len() { + row_out.push_str(" "); + } + } + + if row_idx + 1 != rows.len() { + output.push(row_out); + } + } + + output + } +} + +impl Formatted for WatchListItem { + fn get_formatted(&self) -> Vec { + vec![ + self.symbol.to_string(), + self.price.to_string(), + format!( + "{} {}", + if self.trend.is_sign_positive() { + "▲" + } else { + "▼" + }, + self.trend.abs() + ), + ] + } +} + +pub fn space_out(mut items: Vec) -> Vec { + for item in &mut items { + item.insert(0, ' '); + } + + items +} From f5d30551746b91ff58f5f9bd97c9f3ae28105ad1 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 12:03:30 +0200 Subject: [PATCH 10/11] Separated formatting --- src/formatting.rs | 120 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 47 +++++++++++++----- src/types.rs | 77 +---------------------------- 3 files changed, 157 insertions(+), 87 deletions(-) create mode 100644 src/formatting.rs diff --git a/src/formatting.rs b/src/formatting.rs new file mode 100644 index 0000000..1ba62e7 --- /dev/null +++ b/src/formatting.rs @@ -0,0 +1,120 @@ +use crate::types::{ActivePosition, WatchListItem}; + +pub trait Formatted { + fn get_formatted(&self) -> Vec; +} + +impl Formatted for WatchListItem { + fn get_formatted(&self) -> Vec { + vec![ + format!("\x1b[35m{}\x1b[0m", self.symbol.to_string()), + format_f64(self.price), + format!( + "{} {}", + if self.trend.is_sign_positive() { + "\x1b[32m▲\x1b[0m" + } else { + "\x1b[31m▼\x1b[0m" + }, + format_f64(self.trend.abs()) + ), + ] + } +} + +impl Formatted for ActivePosition { + fn get_formatted(&self) -> Vec { + vec![ + format!("\x1b[35m{}\x1b[0m", self.symbol.to_string()), + format_f64(self.amount), + format!( + "{} {}", + if self.profit.is_sign_positive() { + "\x1b[32m▲\x1b[0m" + } else { + "\x1b[31m▼\x1b[0m" + }, + format_f64(self.profit.abs()) + ), + ] + } +} + +impl Formatted for Vec { + fn get_formatted(&self) -> Vec { + let mut output = Vec::new(); + + let rows: Vec> = self.iter().map(|v| v.get_formatted()).collect(); + + if rows.is_empty() { + return output; + } + + let column_count = rows.iter().map(|r| r.len()).max().unwrap_or(0); + + let mut widths = vec![0; column_count]; + + for row in &rows { + for (i, col) in row.iter().enumerate() { + widths[i] = widths[i].max(col.len()); + } + } + + for row in rows { + let mut row_out = String::new(); + + for (col_idx, col) in row.iter().enumerate() { + let width = widths[col_idx]; + + row_out.push_str(&format!("{:>width$}", col, width = width)); + + if col_idx + 1 != row.len() { + row_out.push_str(" "); + } + } + + output.push(row_out); + } + + output + } +} + +pub fn apply_padding(mut items: Vec) -> Vec { + for item in &mut items { + item.insert(0, ' '); + } + + items +} + +pub fn format_f64(value: f64) -> String { + let val = value.to_string(); + let parts: Vec<&str> = val.split('.').collect(); + + let int = parts[0].to_string(); + let negative = int.starts_with('-'); + + let start = if negative { 1 } else { 0 }; + let mut result = String::new(); + + for (i, c) in int[start..].chars().rev().enumerate() { + if i > 0 && i % 3 == 0 { + result.push(','); + } + result.push(c); + } + + let mut formatted: String = result.chars().rev().collect(); + + if negative { + formatted.insert(0, '-'); + } + + if parts.len() > 1 { + formatted.push('.'); + formatted.push_str(parts[1]); + } + + formatted +} diff --git a/src/main.rs b/src/main.rs index 0a72b37..b1c09d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +pub mod formatting; pub mod types; use std::any::Any; @@ -15,11 +16,14 @@ use pulse_ui::{ }, }; -use crate::types::{Formatted, WatchListItem, space_out}; +use crate::{ + formatting::{Formatted, apply_padding}, types::{ActivePosition, WatchListItem}, +}; pub struct PulseTradeApp { command: State, watch_list: State>, + active_positions: State>, } impl App for PulseTradeApp { @@ -34,7 +38,7 @@ impl App for PulseTradeApp { watch_list.push(WatchListItem { symbol: "ETH".to_string(), price: 3_912.48, - trend: 0.41, + trend: -0.41, }); watch_list.push(WatchListItem { symbol: "SOL".to_string(), @@ -46,6 +50,29 @@ impl App for PulseTradeApp { 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, + }); } async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box) { @@ -107,19 +134,16 @@ impl App for PulseTradeApp { LayoutItem::Widget(Size::Flex(1)), Box::new(format!( " WATCHLIST\n{}", - space_out(self.watch_list.lock().await.get_formatted()).join("\n") + apply_padding(self.watch_list.lock().await.get_formatted()).join("\n") )), ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new( - vec![ - " ACTIVE POSITIONS", - " BTC +4,120.40 20,000.00", - " SOL +2,435.40 20,000.00", - ] - .join("\n"), - ), + Box::new(format!( + " ACTIVE POSITIONS\n{}", + apply_padding(self.active_positions.lock().await.get_formatted()) + .join("\n") + )), ), ( LayoutItem::Widget(Size::Flex(1)), @@ -182,6 +206,7 @@ async fn main() { pulse_ui::run(|ctx| PulseTradeApp { command: ctx.use_state(InputState::new()), watch_list: ctx.use_state(Vec::new()), + active_positions: ctx.use_state(Vec::new()), }) .await; } diff --git a/src/types.rs b/src/types.rs index 9a5c1de..3c3b4c1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,7 +1,3 @@ -pub trait Formatted { - fn get_formatted(&self) -> Vec; -} - pub struct WatchListItem { pub symbol: String, pub price: f64, @@ -11,7 +7,7 @@ pub struct WatchListItem { pub struct ActivePosition { pub symbol: String, pub profit: f64, - pub lot: f64, + pub amount: f64, } pub struct Account { @@ -21,74 +17,3 @@ pub struct Account { pub realized: f64, pub margin: f64, } - -impl Formatted for Vec { - fn get_formatted(&self) -> Vec { - let mut output = Vec::new(); - - let rows: Vec> = self.iter().map(|v| v.get_formatted()).collect(); - - if rows.is_empty() { - return output; - } - - let column_count = rows.iter().map(|r| r.len()).max().unwrap_or(0); - - let mut widths = vec![0; column_count]; - - // Find the largest width per column - for row in &rows { - for (i, col) in row.iter().enumerate() { - widths[i] = widths[i].max(col.len()); - } - } - - // Render rows - for (row_idx, row) in rows.iter().enumerate() { - let mut row_out = String::new(); - - for (col_idx, col) in row.iter().enumerate() { - let width = widths[col_idx]; - - // Right-align numbers, left-align text could be customized here - row_out.push_str(&format!("{:>width$}", col, width = width)); - - if col_idx + 1 != row.len() { - row_out.push_str(" "); - } - } - - if row_idx + 1 != rows.len() { - output.push(row_out); - } - } - - output - } -} - -impl Formatted for WatchListItem { - fn get_formatted(&self) -> Vec { - vec![ - self.symbol.to_string(), - self.price.to_string(), - format!( - "{} {}", - if self.trend.is_sign_positive() { - "▲" - } else { - "▼" - }, - self.trend.abs() - ), - ] - } -} - -pub fn space_out(mut items: Vec) -> Vec { - for item in &mut items { - item.insert(0, ' '); - } - - items -} From 02a06b2647ccdd95d91f299fccfe4d1f14bbafdd Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 12:30:02 +0200 Subject: [PATCH 11/11] Account and properties --- src/formatting.rs | 25 +++++++++++++++++++++++-- src/main.rs | 26 ++++++++++++++------------ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/formatting.rs b/src/formatting.rs index 1ba62e7..2eff77f 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -1,4 +1,4 @@ -use crate::types::{ActivePosition, WatchListItem}; +use crate::types::{Account, ActivePosition, WatchListItem}; pub trait Formatted { fn get_formatted(&self) -> Vec; @@ -40,6 +40,27 @@ impl Formatted for ActivePosition { } } +pub struct Property(&'static str, String); + +impl Formatted for Account { + fn get_formatted(&self) -> Vec { + vec![ + Property("Equity", format_f64(self.equity)), + Property("Liquid", format_f64(self.liquid)), + Property("Unreal", format_f64(self.unreal)), + Property("Realized", format_f64(self.realized)), + Property("Margin", format_f64(self.margin)), + ] + .get_formatted() + } +} + +impl Formatted for Property { + fn get_formatted(&self) -> Vec { + vec![format!("\x1b[91m{}\x1b[0m", self.0), self.1.clone()] + } +} + impl Formatted for Vec { fn get_formatted(&self) -> Vec { let mut output = Vec::new(); @@ -66,7 +87,7 @@ impl Formatted for Vec { for (col_idx, col) in row.iter().enumerate() { let width = widths[col_idx]; - row_out.push_str(&format!("{:>width$}", col, width = width)); + row_out.push_str(&format!("{:, watch_list: State>, active_positions: State>, + account: State, } impl App for PulseTradeApp { @@ -147,17 +149,10 @@ impl App for PulseTradeApp { ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new( - vec![ - " ACCOUNT", - " Equity: $25,483.21", - " Liquid: $11,928.43", - " Unreal: +$483.12", - " Realized: +$2,182.49", - " Margin: 0.00%", - ] - .join("\n"), - ), + Box::new(format![ + " ACCOUNT\n{}", + apply_padding(self.account.lock().await.get_formatted()).join("\n") + ]), ), ]), ); @@ -207,6 +202,13 @@ async fn main() { command: ctx.use_state(InputState::new()), watch_list: ctx.use_state(Vec::new()), active_positions: ctx.use_state(Vec::new()), + account: ctx.use_state(Account { + equity: 25_483.21, + liquid: 11_928.43, + unreal: 483.12, + realized: 2_182.49, + margin: 0.0, + }), }) .await; }