From eb670ffe8c235b36c2babc408a83f503e1ec63f0 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 10:55:47 +0200 Subject: [PATCH] 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; }