Merge pull request #6 from selimaj-dev/more-ui

More UI
This commit is contained in:
2026-07-15 15:12:54 +02:00
committed by GitHub
6 changed files with 241 additions and 32 deletions
+38 -7
View File
@@ -26,23 +26,54 @@ impl LayoutItem {
let allocation = if is_row { alloc.height } else { alloc.width }; let allocation = if is_row { alloc.height } else { alloc.width };
let remaining = allocation let fixed_total: u16 = items
- items .iter()
.iter() .map(|item| item.get_size().get_fixed(allocation))
.map(|item| item.get_size().get_fixed(allocation)) .sum();
.sum::<u16>();
let remaining = allocation.saturating_sub(fixed_total);
let total_weight = items let total_weight = items
.iter() .iter()
.map(|item| item.get_size().get_flex()) .map(|item| item.get_size().get_flex())
.sum::<u16>(); .sum::<u16>();
let base_allocs: Vec<u16> = 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 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 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 { let item_alloc = if is_row {
Rect { Rect {
+14 -1
View File
@@ -9,6 +9,7 @@ pub enum Size {
Fixed(u16), Fixed(u16),
Flex(u16), Flex(u16),
Percent(u16), Percent(u16),
Fill,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -20,17 +21,22 @@ pub struct Rect {
} }
impl Size { impl Size {
pub fn is_fill(&self) -> bool {
matches!(self, Self::Fill)
}
pub fn get_fixed(&self, alloc: u16) -> u16 { pub fn get_fixed(&self, alloc: u16) -> u16 {
match self { match self {
Self::Fixed(v) => *v, Self::Fixed(v) => *v,
Self::Percent(v) => alloc * (*v) / 100, Self::Percent(v) => alloc * (*v) / 100,
Self::Flex(_) => 0, Self::Flex(_) | Self::Fill => 0,
} }
} }
pub fn get_flex(&self) -> u16 { pub fn get_flex(&self) -> u16 {
match self { match self {
Self::Flex(f) => *f, Self::Flex(f) => *f,
Self::Fill => 1,
_ => 0, _ => 0,
} }
} }
@@ -46,6 +52,13 @@ impl Size {
remaining * v / total_weight remaining * v / total_weight
} }
} }
Self::Fill => {
if total_weight == 0 {
0
} else {
remaining / total_weight
}
}
} }
} }
} }
+35
View File
@@ -67,6 +67,41 @@ impl InputState {
true 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 { Event::Key(KeyEvent {
code: KeyCode::Char(c), code: KeyCode::Char(c),
modifiers, modifiers,
+51 -3
View File
@@ -1,13 +1,49 @@
use crate::types::{Account, ActivePosition, WatchListItem}; use crate::types::{
Account, ActivePosition, EventLog, LogKind, Signal, SignalKind, System, WatchListItem,
};
pub trait Formatted { pub trait Formatted {
fn get_formatted(&self) -> Vec<String>; fn get_formatted(&self) -> Vec<String>;
} }
impl Formatted for EventLog {
fn get_formatted(&self) -> Vec<String> {
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<String> {
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 { impl Formatted for WatchListItem {
fn get_formatted(&self) -> Vec<String> { fn get_formatted(&self) -> Vec<String> {
vec![ vec![
format!("\x1b[35m{}\x1b[0m", self.symbol.to_string()), format!("\x1b[35m{}\x1b[0m", self.symbol),
format_f64(self.price), format_f64(self.price),
format!( format!(
"{} {}", "{} {}",
@@ -25,7 +61,7 @@ impl Formatted for WatchListItem {
impl Formatted for ActivePosition { impl Formatted for ActivePosition {
fn get_formatted(&self) -> Vec<String> { fn get_formatted(&self) -> Vec<String> {
vec![ vec![
format!("\x1b[35m{}\x1b[0m", self.symbol.to_string()), format!("\x1b[35m{}\x1b[0m", self.symbol),
format_f64(self.amount), format_f64(self.amount),
format!( format!(
"{} {}", "{} {}",
@@ -55,6 +91,18 @@ impl Formatted for Account {
} }
} }
impl Formatted for System {
fn get_formatted(&self) -> Vec<String> {
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 { impl Formatted for Property {
fn get_formatted(&self) -> Vec<String> { fn get_formatted(&self) -> Vec<String> {
vec![format!("\x1b[91m{}\x1b[0m", self.0), self.1.clone()] vec![format!("\x1b[91m{}\x1b[0m", self.0), self.1.clone()]
+52 -21
View File
@@ -18,14 +18,17 @@ use pulse_ui::{
use crate::{ use crate::{
formatting::{Formatted, apply_padding}, formatting::{Formatted, apply_padding},
types::{Account, ActivePosition, WatchListItem}, types::{Account, ActivePosition, EventLog, Signal, System, WatchListItem},
}; };
pub struct PulseTradeApp { pub struct PulseTradeApp {
command: State<InputState>, command: State<InputState>,
watch_list: State<Vec<WatchListItem>>, watch_list: State<Vec<WatchListItem>>,
active_positions: State<Vec<ActivePosition>>, active_positions: State<Vec<ActivePosition>>,
logs: State<Vec<EventLog>>,
signals: State<Vec<Signal>>,
account: State<Account>, account: State<Account>,
system: State<System>,
} }
impl App for PulseTradeApp { impl App for PulseTradeApp {
@@ -75,6 +78,29 @@ impl App for PulseTradeApp {
profit: 12.30, profit: 12.30,
amount: 0.75, 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<dyn Any + Send + Sync>) { async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box<dyn Any + Send + Sync>) {
@@ -111,7 +137,7 @@ impl App for PulseTradeApp {
LayoutItem::Widget(Size::Flex(1)), LayoutItem::Widget(Size::Flex(1)),
// Event logs // Event logs
LayoutItem::Spacing(Size::Fixed(1)), LayoutItem::Spacing(Size::Fixed(1)),
LayoutItem::Widget(Size::Flex(1)), LayoutItem::Widget(Size::Fill),
// Input // Input
LayoutItem::Spacing(Size::Fixed(1)), LayoutItem::Spacing(Size::Fixed(1)),
LayoutItem::Widget(Size::Fixed(1)), LayoutItem::Widget(Size::Fixed(1)),
@@ -166,32 +192,29 @@ impl App for PulseTradeApp {
), ),
( (
LayoutItem::Widget(Size::Flex(1)), LayoutItem::Widget(Size::Flex(1)),
Box::new( Box::new(format!(
vec![ " SIGNALS\n{}",
" SIGNALS", apply_padding(self.signals.lock().await.get_formatted()).join("\n")
" BUY BTC LIM 118,800.12", )),
" BUY BTC STL 118,000.00",
" BUY BTC TAP 120,000.00",
]
.join("\n"),
),
), ),
( (
LayoutItem::Widget(Size::Flex(1)), LayoutItem::Widget(Size::Flex(1)),
Box::new( Box::new(format![
vec![ " SYSTEM\n{}",
" SYSTEM", apply_padding(self.system.lock().await.get_formatted()).join("\n")
" Feed: Connected", ]),
" Exchange: Binance",
" DEX: DEX SCREENER",
" Latency: 18 ms",
]
.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)); layout.draw(6, Input(" > ", &*self.command.lock().await));
} }
} }
@@ -202,6 +225,8 @@ async fn main() {
command: ctx.use_state(InputState::new()), command: ctx.use_state(InputState::new()),
watch_list: ctx.use_state(Vec::new()), watch_list: ctx.use_state(Vec::new()),
active_positions: 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 { account: ctx.use_state(Account {
equity: 25_483.21, equity: 25_483.21,
liquid: 11_928.43, liquid: 11_928.43,
@@ -209,6 +234,12 @@ async fn main() {
realized: 2_182.49, realized: 2_182.49,
margin: 0.0, margin: 0.0,
}), }),
system: ctx.use_state(System {
feed: types::Feed::Connected,
exchange: "Binance".to_string(),
dex: "DEX SCREENER".to_string(),
latency: 18,
}),
}) })
.await; .await;
} }
+51
View File
@@ -17,3 +17,54 @@ pub struct Account {
pub realized: f64, pub realized: f64,
pub margin: 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,
}