Generated
+248
@@ -2,18 +2,62 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
@@ -23,6 +67,12 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.29.0"
|
||||
@@ -91,6 +141,71 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
@@ -136,6 +251,21 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -178,6 +308,8 @@ dependencies = [
|
||||
name = "pulse-trader"
|
||||
version = "0.1.0-alpha.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"crossterm",
|
||||
"pulse-ui",
|
||||
"tokio",
|
||||
]
|
||||
@@ -230,6 +362,12 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -242,6 +380,12 @@ version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
@@ -273,6 +417,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
@@ -329,6 +479,51 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -351,12 +546,65 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
|
||||
@@ -4,8 +4,10 @@ version = "0.1.0-alpha.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.45"
|
||||
pulse-ui = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
crossterm = { workspace = true }
|
||||
|
||||
[workspace]
|
||||
members = ["pulse-ui"]
|
||||
|
||||
@@ -1,3 +1,53 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
mod platform {
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
pub fn is_backspace(event: &KeyEvent) -> bool {
|
||||
matches!(event.code, KeyCode::Backspace)
|
||||
|| matches!(
|
||||
event,
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('h'),
|
||||
modifiers,
|
||||
..
|
||||
} if modifiers.contains(KeyModifiers::CONTROL)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_delete(event: &KeyEvent) -> bool {
|
||||
matches!(event.code, KeyCode::Delete)
|
||||
|| matches!(
|
||||
event,
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('d'),
|
||||
modifiers,
|
||||
..
|
||||
} if modifiers.contains(KeyModifiers::CONTROL)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod platform {
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
|
||||
pub fn is_backspace(event: &KeyEvent) -> bool {
|
||||
matches!(event.code, KeyCode::Backspace)
|
||||
|| matches!(
|
||||
event,
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('h'),
|
||||
modifiers,
|
||||
..
|
||||
} if modifiers.contains(KeyModifiers::CONTROL)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_delete(event: &KeyEvent) -> bool {
|
||||
matches!(event.code, KeyCode::Delete)
|
||||
}
|
||||
}
|
||||
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
use crate::widget::Widget;
|
||||
@@ -22,7 +72,12 @@ 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, "❘");
|
||||
if self.1.cursor == text.len() {
|
||||
text.insert_str(self.1.cursor, "\x1b[47m \x1b[0m");
|
||||
} else {
|
||||
text.insert_str(self.1.cursor + 1, "\x1b[0m");
|
||||
text.insert_str(self.1.cursor, "\x1b[47m\x1b[30m");
|
||||
}
|
||||
scope.draw_text(0, format!("{}{text}", self.0));
|
||||
}
|
||||
}
|
||||
@@ -30,6 +85,34 @@ impl<'a> Widget for Input<'a> {
|
||||
impl InputState {
|
||||
pub fn handle_event(&mut self, event: &Event) -> bool {
|
||||
match event {
|
||||
Event::Key(key) if platform::is_backspace(key) => {
|
||||
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(key) if platform::is_delete(key) => {
|
||||
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::Left,
|
||||
..
|
||||
@@ -50,58 +133,6 @@ impl InputState {
|
||||
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('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,
|
||||
|
||||
@@ -67,3 +67,37 @@ impl Widget for SpacedColumns {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SpacedRows2(pub Vec<(LayoutItem, Box<dyn Widget>)>);
|
||||
|
||||
impl Widget for SpacedRows2 {
|
||||
fn render(&self, scope: &mut crate::render::RenderScope) {
|
||||
let alloc = scope.rect.allocate(&crate::layout::LayoutItem::Rows {
|
||||
unit: Percent(100),
|
||||
items: self.0.iter().map(|v| v.0.clone()).collect(),
|
||||
});
|
||||
|
||||
for i in 0..alloc.widgets.len() {
|
||||
let item = &self.0[i];
|
||||
|
||||
alloc.draw(i, &*item.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SpacedColumns2(pub Vec<(LayoutItem, Box<dyn Widget>)>);
|
||||
|
||||
impl Widget for SpacedColumns2 {
|
||||
fn render(&self, scope: &mut crate::render::RenderScope) {
|
||||
let alloc = scope.rect.allocate(&crate::layout::LayoutItem::Columns {
|
||||
unit: Percent(100),
|
||||
items: self.0.iter().map(|v| v.0.clone()).collect(),
|
||||
});
|
||||
|
||||
for i in 0..alloc.widgets.len() {
|
||||
let item = &self.0[i];
|
||||
|
||||
alloc.draw(i, &*item.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
use crate::{PulseTradeApp, types::EventLog};
|
||||
|
||||
impl PulseTradeApp {
|
||||
pub async fn execute_command(&mut self, ctx: &pulse_ui::state::Context, command: &str) {
|
||||
if command.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (command, _args) = if let Some((command, args)) = command.split_once(" ") {
|
||||
(command, args.split(" ").collect())
|
||||
} else {
|
||||
(command, Vec::new())
|
||||
};
|
||||
|
||||
match command {
|
||||
"exit" | "quit" | "leave" => {
|
||||
ctx.close().await;
|
||||
}
|
||||
|
||||
_ => {
|
||||
self.logs.lock().await.push(EventLog {
|
||||
kind: crate::types::LogKind::Err,
|
||||
name: "cmd",
|
||||
message: format!("Command '{}' not found", command),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
-18
@@ -1,21 +1,85 @@
|
||||
use crate::types::{
|
||||
Account, ActivePosition, EventLog, LogKind, Signal, SignalKind, System, WatchListItem,
|
||||
ActivePosition, Alert, AlertLevel, EventLog, InspectTarget, LogKind, MarketOverview, Signal,
|
||||
SignalKind, Status, WatchListItem,
|
||||
};
|
||||
|
||||
pub trait Formatted {
|
||||
fn get_formatted(&self) -> Vec<String>;
|
||||
}
|
||||
|
||||
impl Formatted for InspectTarget {
|
||||
fn get_formatted(&self) -> Vec<String> {
|
||||
match self {
|
||||
Self::None => vec!["\x1b[2mnothing to inspect\x1b[0m".to_string()],
|
||||
|
||||
Self::Symbol(boxed) => {
|
||||
let (watch, overview) = &**boxed;
|
||||
vec![
|
||||
Property("Symbol", format!("\x1b[35m{}\x1b[0m", watch.symbol)),
|
||||
Property("Price", format!("\x1b[96m{}\x1b[0m", format_f64(watch.price))),
|
||||
Property("Trend", format!("{}", format_f64(watch.trend))),
|
||||
Property("Market", format!("{}", overview.trend)),
|
||||
Property("Volatility", format!("{}", overview.volatility)),
|
||||
Property(
|
||||
"Pressure",
|
||||
format!("{:+.2}%", overview.pressure * 100.0),
|
||||
),
|
||||
Property("Alerts", format!("{}", overview.alerts.len())),
|
||||
]
|
||||
.get_formatted()
|
||||
}
|
||||
|
||||
Self::Position(pos) => vec![
|
||||
Property("Symbol", format!("\x1b[35m{}\x1b[0m", pos.symbol)),
|
||||
Property("Profit", format!("{:+.2}", pos.profit)),
|
||||
Property("Amount", format!("{}", pos.amount)),
|
||||
]
|
||||
.get_formatted(),
|
||||
|
||||
Self::Signal(sig) => vec![
|
||||
Property("Type", format!("{}", sig.kind)),
|
||||
Property("Symbol", format!("\x1b[35m{}\x1b[0m", sig.symbol)),
|
||||
Property("Parameter", format!("{}", sig.param)),
|
||||
Property("Price", format!("\x1b[96m{}\x1b[0m", format_f64(sig.price))),
|
||||
]
|
||||
.get_formatted(),
|
||||
|
||||
Self::Alert(alert) => vec![
|
||||
Property("Level", format!("{}", alert.level)),
|
||||
Property("Message", alert.message.clone()),
|
||||
]
|
||||
.get_formatted(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Formatted for Alert {
|
||||
fn get_formatted(&self) -> Vec<String> {
|
||||
vec![
|
||||
format!(
|
||||
"{}{}\x1b[0m",
|
||||
match &self.level {
|
||||
AlertLevel::Low => "\x1b[34m",
|
||||
AlertLevel::Medium => "\x1b[33m",
|
||||
AlertLevel::High => "\x1b[31m",
|
||||
},
|
||||
self.level
|
||||
),
|
||||
self.message.clone(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Formatted for EventLog {
|
||||
fn get_formatted(&self) -> Vec<String> {
|
||||
vec![
|
||||
format!(
|
||||
"{}{:?}\x1b[0m",
|
||||
"{}{}\x1b[0m",
|
||||
match &self.kind {
|
||||
LogKind::INFO => "\x1b[34m",
|
||||
LogKind::DEBUG => "\x1b[36m",
|
||||
LogKind::ERR => "\x1b[31m",
|
||||
LogKind::WARN => "\x1b[33m",
|
||||
LogKind::Info => "\x1b[34m",
|
||||
LogKind::Debug => "\x1b[36m",
|
||||
LogKind::Err => "\x1b[31m",
|
||||
LogKind::Warn => "\x1b[33m",
|
||||
},
|
||||
self.kind
|
||||
),
|
||||
@@ -28,13 +92,13 @@ impl Formatted for EventLog {
|
||||
impl Formatted for Signal {
|
||||
fn get_formatted(&self) -> Vec<String> {
|
||||
vec![
|
||||
if matches!(self.kind, SignalKind::BUY) {
|
||||
format!("\x1b[32m{:?}\x1b[0m", self.kind)
|
||||
if matches!(self.kind, SignalKind::Buy) {
|
||||
format!("\x1b[32m{}\x1b[0m", self.kind)
|
||||
} else {
|
||||
format!("\x1b[31m{:?}\x1b[0m", self.kind)
|
||||
format!("\x1b[31m{}\x1b[0m", self.kind)
|
||||
},
|
||||
format!("\x1b[35m{}\x1b[0m", self.symbol),
|
||||
format!("\x1b[34m{:?}\x1b[0m", self.param),
|
||||
format!("\x1b[34m{}\x1b[0m", self.param),
|
||||
format_f64(self.price),
|
||||
]
|
||||
}
|
||||
@@ -76,22 +140,27 @@ impl Formatted for ActivePosition {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Property(&'static str, String);
|
||||
struct Property(&'static str, String);
|
||||
|
||||
impl Formatted for Account {
|
||||
impl Formatted for MarketOverview {
|
||||
fn get_formatted(&self) -> Vec<String> {
|
||||
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)),
|
||||
Property("TREND", format!("{}", self.trend)),
|
||||
Property("VOLATILITY", format!("{}", self.volatility)),
|
||||
Property(
|
||||
"PRESSURE",
|
||||
if self.pressure.is_sign_positive() {
|
||||
format!("\x1b[32m{:.3}\x1b[0m", self.pressure)
|
||||
} else {
|
||||
format!("\x1b[31m{:.3}\x1b[0m", self.pressure)
|
||||
},
|
||||
),
|
||||
]
|
||||
.get_formatted()
|
||||
}
|
||||
}
|
||||
|
||||
impl Formatted for System {
|
||||
impl Formatted for Status {
|
||||
fn get_formatted(&self) -> Vec<String> {
|
||||
vec![
|
||||
Property("Feed", format!("{:?}", self.feed)),
|
||||
|
||||
+93
-40
@@ -1,15 +1,17 @@
|
||||
pub mod command;
|
||||
pub mod formatting;
|
||||
pub mod types;
|
||||
|
||||
use std::any::Any;
|
||||
|
||||
use chrono::{Local, Utc};
|
||||
use pulse_ui::{
|
||||
App,
|
||||
layout::{LayoutItem, layout},
|
||||
state::{Refresh, State},
|
||||
unit::Size,
|
||||
widget::{
|
||||
align::Center,
|
||||
align::End,
|
||||
input::{Input, InputState},
|
||||
outline::{Outline, VLine},
|
||||
spaced::SpacedColumns,
|
||||
@@ -18,7 +20,10 @@ use pulse_ui::{
|
||||
|
||||
use crate::{
|
||||
formatting::{Formatted, apply_padding},
|
||||
types::{Account, ActivePosition, EventLog, Signal, System, WatchListItem},
|
||||
types::{
|
||||
ActivePosition, Alert, EventLog, InspectTarget, MarketOverview, Signal, Status,
|
||||
WatchListItem,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct PulseTradeApp {
|
||||
@@ -27,12 +32,13 @@ pub struct PulseTradeApp {
|
||||
active_positions: State<Vec<ActivePosition>>,
|
||||
logs: State<Vec<EventLog>>,
|
||||
signals: State<Vec<Signal>>,
|
||||
account: State<Account>,
|
||||
system: State<System>,
|
||||
market_overview: State<MarketOverview>,
|
||||
status: State<Status>,
|
||||
inspect: State<InspectTarget>,
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -82,25 +88,50 @@ impl App for PulseTradeApp {
|
||||
let mut signals = self.signals.lock().await;
|
||||
|
||||
signals.push(Signal {
|
||||
kind: types::SignalKind::BUY,
|
||||
kind: types::SignalKind::Buy,
|
||||
symbol: "BTC".to_string(),
|
||||
param: types::SignalParameter::LIM,
|
||||
param: types::SignalParameter::Lim,
|
||||
price: 118_800.0,
|
||||
});
|
||||
|
||||
signals.push(Signal {
|
||||
kind: types::SignalKind::BUY,
|
||||
kind: types::SignalKind::Buy,
|
||||
symbol: "BTC".to_string(),
|
||||
param: types::SignalParameter::TAP,
|
||||
param: types::SignalParameter::Tap,
|
||||
price: 120_000.0,
|
||||
});
|
||||
|
||||
signals.push(Signal {
|
||||
kind: types::SignalKind::BUY,
|
||||
kind: types::SignalKind::Buy,
|
||||
symbol: "BTC".to_string(),
|
||||
param: types::SignalParameter::STL,
|
||||
param: types::SignalParameter::Stl,
|
||||
price: 118_000.0,
|
||||
});
|
||||
|
||||
let mut logs = self.logs.lock().await;
|
||||
|
||||
logs.push(EventLog {
|
||||
kind: types::LogKind::Warn,
|
||||
name: "pulse.init",
|
||||
message: "We're still not done yet ;)".to_string(),
|
||||
});
|
||||
|
||||
let mut market_overview = self.market_overview.lock().await;
|
||||
|
||||
market_overview.alerts.push(Alert {
|
||||
level: types::AlertLevel::High,
|
||||
message: "BTC funding rate elevated".to_string(),
|
||||
});
|
||||
|
||||
market_overview.alerts.push(Alert {
|
||||
level: types::AlertLevel::Medium,
|
||||
message: "Market volatility increasing".to_string(),
|
||||
});
|
||||
|
||||
market_overview.alerts.push(Alert {
|
||||
level: types::AlertLevel::Low,
|
||||
message: "ETH volatility returning to normal".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box<dyn Any + Send + Sync>) {
|
||||
@@ -111,10 +142,18 @@ impl App for PulseTradeApp {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.close().await;
|
||||
}
|
||||
|
||||
async fn layout(&self) -> pulse_ui::layout::LayoutItem {
|
||||
@@ -147,9 +186,19 @@ impl App for PulseTradeApp {
|
||||
|
||||
async fn render(&mut self, layout: pulse_ui::layout::Allocation) {
|
||||
layout.draw_frame(0, Outline);
|
||||
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(
|
||||
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);
|
||||
@@ -173,23 +222,20 @@ impl App for PulseTradeApp {
|
||||
.join("\n")
|
||||
)),
|
||||
),
|
||||
(
|
||||
LayoutItem::Widget(Size::Flex(1)),
|
||||
Box::new(format![
|
||||
" ACCOUNT\n{}",
|
||||
apply_padding(self.account.lock().await.get_formatted()).join("\n")
|
||||
]),
|
||||
),
|
||||
(LayoutItem::Widget(Size::Flex(1)), {
|
||||
let mo = self.market_overview.lock().await;
|
||||
Box::new(format!(
|
||||
" MARKET OVERVIEW\n{}\n\n{}",
|
||||
apply_padding(mo.get_formatted()).join("\n"),
|
||||
apply_padding(mo.alerts.get_formatted()).join("\n")
|
||||
))
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
layout.draw(
|
||||
4,
|
||||
SpacedColumns(vec![
|
||||
(
|
||||
LayoutItem::Widget(Size::Flex(1)),
|
||||
Box::new(vec![" ACTIVE STRATEGIES"].join("\n")),
|
||||
),
|
||||
(
|
||||
LayoutItem::Widget(Size::Flex(1)),
|
||||
Box::new(format!(
|
||||
@@ -199,20 +245,27 @@ impl App for PulseTradeApp {
|
||||
),
|
||||
(
|
||||
LayoutItem::Widget(Size::Flex(1)),
|
||||
Box::new(format![
|
||||
" SYSTEM\n{}",
|
||||
apply_padding(self.system.lock().await.get_formatted()).join("\n")
|
||||
]),
|
||||
Box::new(format!(
|
||||
" INSPECTOR\n{}",
|
||||
apply_padding(self.inspect.lock().await.get_formatted()).join("\n")
|
||||
)),
|
||||
),
|
||||
(
|
||||
LayoutItem::Widget(Size::Flex(1)),
|
||||
Box::new(format!(
|
||||
" STATUS\n{}",
|
||||
apply_padding(self.status.lock().await.get_formatted()).join("\n")
|
||||
)),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
layout.draw(
|
||||
5,
|
||||
format![
|
||||
format!(
|
||||
" EVENT LOGS\n{}",
|
||||
apply_padding(self.logs.lock().await.get_formatted()).join("\n")
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
layout.draw(6, Input(" > ", &*self.command.lock().await));
|
||||
@@ -227,14 +280,14 @@ async fn main() {
|
||||
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,
|
||||
unreal: 483.12,
|
||||
realized: 2_182.49,
|
||||
margin: 0.0,
|
||||
inspect: ctx.use_state(InspectTarget::None),
|
||||
market_overview: ctx.use_state(MarketOverview {
|
||||
trend: types::MarketTrend::Bullish,
|
||||
volatility: types::Volatility::High,
|
||||
pressure: 0.324,
|
||||
alerts: Vec::new(),
|
||||
}),
|
||||
system: ctx.use_state(System {
|
||||
status: ctx.use_state(Status {
|
||||
feed: types::Feed::Connected,
|
||||
exchange: "Binance".to_string(),
|
||||
dex: "DEX SCREENER".to_string(),
|
||||
|
||||
+118
-17
@@ -1,21 +1,58 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WatchListItem {
|
||||
pub symbol: String,
|
||||
pub price: f64,
|
||||
pub trend: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ActivePosition {
|
||||
pub symbol: String,
|
||||
pub profit: f64,
|
||||
pub amount: f64,
|
||||
}
|
||||
|
||||
pub struct Account {
|
||||
pub equity: f64,
|
||||
pub liquid: f64,
|
||||
pub unreal: f64,
|
||||
pub realized: f64,
|
||||
pub margin: f64,
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MarketTrend {
|
||||
Bullish,
|
||||
Bearish,
|
||||
Neutral,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MarketTrend {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Bullish => write!(f, "Bullish"),
|
||||
Self::Bearish => write!(f, "Bearish"),
|
||||
Self::Neutral => write!(f, "Neutral"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Volatility {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Volatility {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Low => write!(f, "Low"),
|
||||
Self::Medium => write!(f, "Medium"),
|
||||
Self::High => write!(f, "High"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarketOverview {
|
||||
pub trend: MarketTrend,
|
||||
pub volatility: Volatility,
|
||||
pub pressure: f64,
|
||||
|
||||
pub alerts: Vec<Alert>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -26,7 +63,7 @@ pub enum Feed {
|
||||
Failed,
|
||||
}
|
||||
|
||||
pub struct System {
|
||||
pub struct Status {
|
||||
pub feed: Feed,
|
||||
pub exchange: String,
|
||||
pub dex: String,
|
||||
@@ -35,18 +72,39 @@ pub struct System {
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SignalKind {
|
||||
BUY,
|
||||
SELL,
|
||||
Buy,
|
||||
Sell,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SignalKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Buy => write!(f, "BUY"),
|
||||
Self::Sell => write!(f, "SELL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SignalParameter {
|
||||
LIM,
|
||||
STL,
|
||||
TAP,
|
||||
CHK,
|
||||
Lim,
|
||||
Stl,
|
||||
Tap,
|
||||
Chk,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SignalParameter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Lim => write!(f, "LIM"),
|
||||
Self::Stl => write!(f, "STL"),
|
||||
Self::Tap => write!(f, "TAP"),
|
||||
Self::Chk => write!(f, "CHK"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Signal {
|
||||
pub kind: SignalKind,
|
||||
pub symbol: String,
|
||||
@@ -56,10 +114,21 @@ pub struct Signal {
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum LogKind {
|
||||
INFO,
|
||||
WARN,
|
||||
ERR,
|
||||
DEBUG,
|
||||
Info,
|
||||
Warn,
|
||||
Err,
|
||||
Debug,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LogKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Info => write!(f, "INFO"),
|
||||
Self::Warn => write!(f, "WARN"),
|
||||
Self::Err => write!(f, "ERR"),
|
||||
Self::Debug => write!(f, "DEBUG"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -68,3 +137,35 @@ pub struct EventLog {
|
||||
pub name: &'static str,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AlertLevel {
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AlertLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::High => write!(f, "H"),
|
||||
Self::Medium => write!(f, "M"),
|
||||
Self::Low => write!(f, "L"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Alert {
|
||||
pub level: AlertLevel,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InspectTarget {
|
||||
None,
|
||||
Symbol(Box<(WatchListItem, MarketOverview)>),
|
||||
Position(ActivePosition),
|
||||
Signal(Signal),
|
||||
Alert(Alert),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user