From f10a6c8f0ad23c1d8dfc9dc0dc32c5f873dc2bd4 Mon Sep 17 00:00:00 2001 From: JerelJr Date: Mon, 11 Mar 2024 21:56:26 -0400 Subject: [PATCH 1/7] TUI WIP --- Cargo.toml | 3 + src/app.rs | 304 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 src/app.rs diff --git a/Cargo.toml b/Cargo.toml index 8dfd11b..0626a57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,6 @@ structopt = "0.3.15" regex = "1.3.9" termcolor = "1.1" rustyline = "6.3.0" +crossterm = "0.27.0" +ratatui = "0.25.0" +unicode-width = "0.1.11" \ No newline at end of file diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..9d3aa3c --- /dev/null +++ b/src/app.rs @@ -0,0 +1,304 @@ +use crossterm::{ + event::{ + self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers, + MouseEventKind, + }, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState}; +use ratatui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::Line, + widgets::{Block, Borders, Paragraph}, + Frame, Terminal, +}; +use regex::RegexSet; +use std::{ + collections::VecDeque, + io::{self, Stdout}, + time::{Duration, Instant}, +}; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +lazy_static::lazy_static! { + static ref REGSET: RegexSet = RegexSet::new([ + r"^(\x60|\.|:|/|-|\+|o|s|h|d|y| ){50,}", // ASCII Chicken + r"^# ", // # command + r"(?m)^\s*(-|=|#)+\s*$", // ================ + r"^\[ =+ ?.* ?=+ \]", // [ ===== Headline ====== ] + r"^> \w+", // > Finished job + r"^(ERROR)|(WARNING): ", // ERROR: something went wrong :( + r"^.*: +.*", // -arg: value + r"^\[.*\]", // [default=something] + r"(?m)^\S+( \[?-\S*( <\S*>)?\]?)*\s*$", // command [-arg ] [-flag] + ]).unwrap(); + + static ref COLORSET: [(Color, Modifier);9] = [ + (Color::White, Modifier::empty()), // # command + (Color::White, Modifier::BOLD), // # command + (Color::Blue, Modifier::empty()), // ================ + (Color::Yellow, Modifier::BOLD), // [ ===== Headline ====== ] + (Color::Cyan, Modifier::empty()), // > Finished job + (Color::Red, Modifier::empty()), // ERROR: something went wrong :( + (Color::Green, Modifier::empty()), // -arg value + (Color::Green, Modifier::BOLD), // [default=something] + (Color::Yellow, Modifier::empty()), // command [-arg ] [-flag] + ]; +} +struct History { + hist: Vec, + index: usize, +} +impl History { + fn new() -> Self { + Self { + hist: vec!["".to_string()], + index: 0, + } + } + fn prev_cmd(&mut self) -> String { + if self.index > 0 { + self.index -= 1; + } + self.hist[self.index].to_string() + } + fn next_cmd(&mut self) -> String { + if self.index < self.hist.len() - 1 { + self.index += 1; + } + self.hist[self.index].to_string() + } + fn add(&mut self, entry: String) { + self.hist.insert(self.hist.len() - 1, entry) + } + fn reset(&mut self) { + self.index = self.hist.len() - 1 + } +} + +/// App holds the state of the application +pub struct App { + /// Current value of the input box + input: String, + /// All application output + output: Vec, + /// History of commands entered + cmd_history: History, + /// Scrollbar State + scroll_state: ScrollbarState, + scroll_pos: usize, + /// Cursor Position + cursor_pos: usize, +} + +impl<'a> App { + pub fn new() -> Self { + Self { + input: String::default(), + output: Vec::new(), + cmd_history: History::new(), + scroll_state: ScrollbarState::default(), + scroll_pos: 0, + cursor_pos: 0, + } + } + + fn delete_char(&mut self) { + if self.cursor_pos != 0 { + self.remove_char(self.cursor_pos) + } + } + + fn submit(&mut self) -> String { + let entr_txt: String = self.input.drain(..).collect(); + + self.output.push(entr_txt.clone()); + self.cmd_history.add(entr_txt.clone()); + self.cmd_history.reset(); + self.cursor_reset(); + + entr_txt + } + + fn put_char(&mut self, c: char) { + self.input.insert(self.cursor_pos, c); + self.cursor_right(); + } + + fn cursor_left(&mut self) { + self.cursor_pos = self.cursor_pos.saturating_sub(1).clamp(0, self.input.len()); + } + + fn cursor_right(&mut self) { + self.cursor_pos = self.cursor_pos.saturating_add(1).clamp(0, self.input.len()); + } + + fn cursor_reset(&mut self) { + self.cursor_pos = 0 + } + + fn scroll_up(&mut self) { + self.scroll_pos = self.scroll_pos.saturating_sub(1); + self.scroll_state = self.scroll_state.position(self.scroll_pos); + } + + fn scroll_down(&mut self) { + self.scroll_pos = self.scroll_pos.saturating_add(1); + self.scroll_state = self.scroll_state.position(self.scroll_pos); + } + + fn remove_char(&mut self, idx: usize) { + let left_idx = self.cursor_pos - 1; + + let left_part = self.input.chars().take(left_idx); + let right_part = self.input.chars().skip(idx); + + self.input = left_part.chain(right_part).collect(); + self.cursor_left(); + } + + fn parse>(s: S) -> Line<'a> { + let matches: Vec<_> = REGSET.matches(s.as_ref()).into_iter().collect(); + + let (color, modf) = if !matches.is_empty() { + COLORSET[matches[0]] + } else { + (Color::White, Modifier::empty()) + }; + Line::styled( + s.as_ref().to_string(), + Style::default().fg(color).add_modifier(modf), + ) + } + + pub async fn run( + mut self, + input_tx: UnboundedSender, + mut output_rx: UnboundedReceiver, + tick_rate: Duration, + ) -> io::Result<()> { + let mut exitspam: VecDeque = VecDeque::with_capacity(3); + let stdout = io::stdout(); + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + let mut prev_tick = Instant::now(); + + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnableMouseCapture, EnterAlternateScreen)?; + + loop { + terminal.draw(|f| self.ui(f))?; + + if let Ok(str) = output_rx.try_recv() { + self.output.push(str) + } + + let timeout = tick_rate.saturating_sub(prev_tick.elapsed()); + if event::poll(timeout)? { + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { + KeyCode::Enter => { + let entr_txt: String = self.submit(); + input_tx.send(format!("{}\r\n", entr_txt.clone())).unwrap(); + if entr_txt.to_uppercase() == "EXIT" { + break; + } + } + KeyCode::Char('c') + if key.modifiers == KeyModifiers::from_name("CONTROL").unwrap() => + { + if input_tx.send("stop\n".to_string()).is_err() { + self.output.push("Couldn't stop!".to_string()); + } + if exitspam.len() == 3 { + if let Some(time) = exitspam.pop_back() { + if Instant::now() - time <= Duration::new(3, 0) { + input_tx.send("EXIT".to_string()).expect("Couldn't exit!"); + break; + } else { + exitspam.push_front(Instant::now()); + } + } + } else { + exitspam.push_front(Instant::now()); + } + } + KeyCode::Char(c) => self.put_char(c), + KeyCode::Backspace => self.delete_char(), + KeyCode::Up => self.input = self.cmd_history.prev_cmd(), + KeyCode::Down => self.input = self.cmd_history.next_cmd(), + KeyCode::Left => self.cursor_left(), + KeyCode::Right => self.cursor_right(), + KeyCode::PageUp => self.scroll_up(), + KeyCode::PageDown => self.scroll_down(), + + _ => (), + }, + Event::Mouse(me) => match me.kind { + MouseEventKind::ScrollUp => self.scroll_up(), + MouseEventKind::ScrollDown => self.scroll_down(), + _ => (), + }, + _ => (), + } + } + if prev_tick.elapsed() >= tick_rate { + prev_tick = Instant::now(); + } + } + Self::shutdown(terminal) + } + + fn ui(&mut self, f: &mut Frame) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints([Constraint::Min(1), Constraint::Length(3)].as_ref()) + .split(f.size()); + + // Message Box + let lines: Vec = self.output.iter().map(Self::parse).collect(); + self.scroll_state = self.scroll_state.content_length(lines.len()); + let messages = Paragraph::new(lines) + .block(Block::default().borders(Borders::ALL).title("Messages")) + .scroll((self.scroll_pos as u16, 0)); + f.render_widget(messages, chunks[0]); + f.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("^")) + .end_symbol(Some("v")), + chunks[0], + &mut self.scroll_state, + ); + + // Input Box + let input = Paragraph::new(self.input.as_str()) + .style(Style::default().fg(Color::Yellow)) + .block(Block::default().borders(Borders::ALL).title("Input")); + f.render_widget(input, chunks[1]); + // Show cursor + f.set_cursor( + // Put cursor after input text + chunks[1].x + self.cursor_pos as u16 + 1, + // Leave room for border + chunks[1].y + 1, + ); + } + + fn shutdown(mut terminal: Terminal>) -> io::Result<()> { + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + DisableMouseCapture, + LeaveAlternateScreen + )?; + terminal.show_cursor()?; + Ok(()) + } +} From 0e8e7fb42c084fa8f9cc6fafdef451ce2776347f Mon Sep 17 00:00:00 2001 From: JerelJr Date: Tue, 2 Apr 2024 01:38:35 -0400 Subject: [PATCH 2/7] Update dependencies / implement app --- Cargo.toml | 16 ++++++++-------- src/app.rs | 33 +++++++++++++++++---------------- src/input.rs | 14 +++++++------- src/main.rs | 44 +++++++++++++++++++++++++------------------- src/port.rs | 2 +- 5 files changed, 58 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0626a57..ad77c34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,20 +8,20 @@ documentation = "https://github.com/SpacehuhnTech/Huhnitor" edition = "2018" [dependencies] -tokio = { version = "0.2.21", features = [ "full" ] } -tokio-util = { version = "0.3.1", features = [ "codec" ] } -tokio-serial = "4.3.3" +tokio = { version = "1.37.0", features = [ "full" ] } +tokio-util = { version = "0.7.10", features = [ "codec" ] } +tokio-serial = "5.4.4" -serialport = "3.3.0" +serialport = "4.3.0" futures = "0.3.5" -bytes = "0.5.4" -webbrowser = "0.5.2" +bytes = "1.6.0" +webbrowser = "0.8.13" lazy_static = "1.4.0" structopt = "0.3.15" regex = "1.3.9" termcolor = "1.1" -rustyline = "6.3.0" +rustyline = "14.0.0" crossterm = "0.27.0" -ratatui = "0.25.0" +ratatui = "0.26.1" unicode-width = "0.1.11" \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 9d3aa3c..5078de9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -52,6 +52,7 @@ struct History { hist: Vec, index: usize, } + impl History { fn new() -> Self { Self { @@ -210,24 +211,24 @@ impl<'a> App { } } KeyCode::Char('c') - if key.modifiers == KeyModifiers::from_name("CONTROL").unwrap() => - { - if input_tx.send("stop\n".to_string()).is_err() { - self.output.push("Couldn't stop!".to_string()); - } - if exitspam.len() == 3 { - if let Some(time) = exitspam.pop_back() { - if Instant::now() - time <= Duration::new(3, 0) { - input_tx.send("EXIT".to_string()).expect("Couldn't exit!"); - break; - } else { - exitspam.push_front(Instant::now()); + if key.modifiers == KeyModifiers::from_name("CONTROL").unwrap() => + { + if input_tx.send("stop\n".to_string()).is_err() { + self.output.push("Couldn't stop!".to_string()); + } + if exitspam.len() == 3 { + if let Some(time) = exitspam.pop_back() { + if Instant::now() - time <= Duration::new(3, 0) { + input_tx.send("EXIT".to_string()).expect("Couldn't exit!"); + break; + } else { + exitspam.push_front(Instant::now()); + } } + } else { + exitspam.push_front(Instant::now()); } - } else { - exitspam.push_front(Instant::now()); } - } KeyCode::Char(c) => self.put_char(c), KeyCode::Backspace => self.delete_char(), KeyCode::Up => self.input = self.cmd_history.prev_cmd(), @@ -290,8 +291,8 @@ impl<'a> App { ); } + /// restore terminal fn shutdown(mut terminal: Terminal>) -> io::Result<()> { - // restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), diff --git a/src/input.rs b/src/input.rs index 7883572..ff2ff2a 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,20 +1,21 @@ use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use std::collections::VecDeque; use std::time::{Instant, Duration}; +use rustyline::{Cmd, EventHandler, KeyCode, KeyEvent, Modifiers}; use crate::error; pub fn receiver(sender: UnboundedSender) { let mut exitspam: VecDeque = VecDeque::with_capacity(3); - let mut rl = rustyline::Editor::<()>::new(); - rl.bind_sequence(rustyline::KeyPress::Up, rustyline::Cmd::LineUpOrPreviousHistory(1)); - rl.bind_sequence(rustyline::KeyPress::Down, rustyline::Cmd::LineDownOrNextHistory(1)); + let mut rl = rustyline::DefaultEditor::new().expect("Unable to start command history"); + rl.bind_sequence(KeyEvent(KeyCode::Up, Modifiers::empty()), Cmd::LineUpOrPreviousHistory(1)); + rl.bind_sequence(KeyEvent(KeyCode::Down, Modifiers::empty()), Cmd::LineDownOrNextHistory(1)); loop { match rl.readline(">> ") { Ok(line) => { - rl.add_history_entry(&line); + rl.add_history_entry(&line).expect("TODO: panic message"); if sender.send(format!("{}\r\n", line.clone())).is_err() { error!("Couldn't report input to main thread!"); } @@ -22,7 +23,7 @@ pub fn receiver(sender: UnboundedSender) { if line.trim().to_uppercase() == "EXIT" { break; } - }, + } Err(rustyline::error::ReadlineError::Interrupted) => { sender.send("stop\n".to_string()).expect("Couldn't stop!"); @@ -39,8 +40,7 @@ pub fn receiver(sender: UnboundedSender) { exitspam.push_front(Instant::now()); } } - Err(e) => error!(e) - + Err(e) => error!(e) } } } diff --git a/src/main.rs b/src/main.rs index c367a07..2bc39d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,41 +1,44 @@ +use crate::app::App; use handler::handle; -use serialport::prelude::*; use std::env; use std::time::Duration; +use serialport::{DataBits, FlowControl, Parity, StopBits}; use structopt::StructOpt; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +mod app; #[macro_use] mod handler; mod input; mod output; mod port; -async fn monitor(cmd_port: Option, auto: bool, no_welcome: bool, out: &output::Preferences) { - let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); +async fn monitor( + cmd_port: Option, + auto: bool, + no_welcome: bool, + out: &output::Preferences, + app: App, +) { + let (input_tx, mut input_rx) = tokio::sync::mpsc::unbounded_channel(); + let (output_tx, output_rx) = tokio::sync::mpsc::unbounded_channel::(); + let input_clone = input_tx.clone(); - std::thread::spawn(|| input::receiver(sender)); + std::thread::spawn(|| input::receiver(input_clone)); - let settings = tokio_serial::SerialPortSettings { - baud_rate: 115200, - data_bits: DataBits::Eight, - flow_control: FlowControl::None, - parity: Parity::None, - stop_bits: StopBits::One, - timeout: Duration::from_secs(10), - }; let tty_path = if cmd_port.is_some() { cmd_port } else if auto { - port::auto(&mut receiver, out).await + port::auto(&mut input_rx, out).await } else { - port::manual(&mut receiver, out).await + port::manual(&mut input_rx, out).await }; if let Some(inner_tty_path) = tty_path { + let settings = tokio_serial::new(&inner_tty_path, 115200).data_bits(DataBits::Eight).flow_control(FlowControl::None).parity(Parity::None).stop_bits(StopBits::One).timeout(Duration::from_secs(10)); #[allow(unused_mut)] // Ignore warning from windows compilers - if let Ok(mut port) = tokio_serial::Serial::from_path(&inner_tty_path, &settings) { + if let Ok(mut port) = tokio_serial::SerialStream::open(&settings) { #[cfg(unix)] port.set_exclusive(false) .expect("Unable to set serial port exclusive to false"); @@ -50,6 +53,8 @@ async fn monitor(cmd_port: Option, auto: bool, no_welcome: bool, out: &o } } + tokio::spawn(async move { app.run(input_tx, output_rx, Duration::from_millis(15)).await }); + let mut buf = Vec::new(); loop { tokio::select! { @@ -59,7 +64,7 @@ async fn monitor(cmd_port: Option, auto: bool, no_welcome: bool, out: &o }, Ok(_) => { let input = String::from_utf8_lossy(&buf).to_string(); - out.print(&input); + output_tx.send(input).unwrap(); buf = Vec::new(); }, Err(e) => { @@ -68,7 +73,7 @@ async fn monitor(cmd_port: Option, auto: bool, no_welcome: bool, out: &o } }, - Some(text) = receiver.recv() => { + Some(text) = input_rx.recv() => { if text.trim().to_uppercase() == "EXIT" { break; } else if text.trim().to_uppercase() == "CLEAR" { @@ -111,7 +116,7 @@ struct Opt { /// Select port #[structopt(short, long)] port: Option, - + /// Disable welcome command #[structopt(short = "w", long = "no-welcome")] no_welcome: bool, @@ -131,7 +136,8 @@ async fn main() { if args.driver { out.driver(); } else { - monitor(args.port, !args.auto, args.no_welcome, &out).await; + let mut app = app::App::new(); + monitor(args.port, !args.auto, args.no_welcome, &out, app).await; } out.goodbye(); diff --git a/src/port.rs b/src/port.rs index f81c288..e135f25 100644 --- a/src/port.rs +++ b/src/port.rs @@ -6,7 +6,7 @@ use crate::output; async fn detect_port(ports: &mut Vec) -> Option { loop { - tokio::time::delay_for(std::time::Duration::from_millis(500)).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; if let Ok(new_ports) = available_ports() { for path in &new_ports { From f385063a5b9a68d79d479c06e701efd7a9ba3b0c Mon Sep 17 00:00:00 2001 From: JerelJr Date: Thu, 9 May 2024 16:29:47 -0400 Subject: [PATCH 3/7] Added scroll functionality + bug fixes --- src/app.rs | 172 +++++++++++++++++++++++++++++++++++---------------- src/input.rs | 46 +++++++------- src/main.rs | 10 ++- 3 files changed, 146 insertions(+), 82 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5078de9..a32c5d5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,6 @@ use crossterm::{ event::{ - self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers, - MouseEventKind, + self, Event, KeyCode, KeyEventKind, KeyModifiers, }, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, @@ -21,6 +20,7 @@ use std::{ io::{self, Stdout}, time::{Duration, Instant}, }; +use std::io::ErrorKind; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; lazy_static::lazy_static! { @@ -48,6 +48,30 @@ lazy_static::lazy_static! { (Color::Yellow, Modifier::empty()), // command [-arg ] [-flag] ]; } + +struct InterruptHandler(VecDeque); + +impl InterruptHandler { + fn new(cap: usize) -> Self { + Self(VecDeque::with_capacity(cap)) + } + fn interrupted(&mut self) -> bool { + if self.0.len() == 3 { + if let Some(time) = self.0.pop_back() { + if Instant::now() - time <= Duration::new(3, 0) { + true + } else { + self.0.push_front(Instant::now()); + false + } + } else { false } + } else { + self.0.push_front(Instant::now()); + false + } + } +} + struct History { hist: Vec, index: usize, @@ -80,6 +104,11 @@ impl History { } } +enum InputMode { + Normal, + Insert, +} + /// App holds the state of the application pub struct App { /// Current value of the input box @@ -88,11 +117,16 @@ pub struct App { output: Vec, /// History of commands entered cmd_history: History, + /// User-controlled scrolling + manual_scroll: bool, /// Scrollbar State - scroll_state: ScrollbarState, + scrollbar: ScrollbarState, + /// Scroll position scroll_pos: usize, /// Cursor Position cursor_pos: usize, + /// Input Mode + input_mode: InputMode, } impl<'a> App { @@ -101,9 +135,11 @@ impl<'a> App { input: String::default(), output: Vec::new(), cmd_history: History::new(), - scroll_state: ScrollbarState::default(), + manual_scroll: false, + scrollbar: ScrollbarState::default(), scroll_pos: 0, cursor_pos: 0, + input_mode: InputMode::Insert, } } @@ -143,12 +179,13 @@ impl<'a> App { fn scroll_up(&mut self) { self.scroll_pos = self.scroll_pos.saturating_sub(1); - self.scroll_state = self.scroll_state.position(self.scroll_pos); + self.scrollbar = self.scrollbar.position(self.scroll_pos); + self.manual_scroll = true; } fn scroll_down(&mut self) { self.scroll_pos = self.scroll_pos.saturating_add(1); - self.scroll_state = self.scroll_state.position(self.scroll_pos); + self.scrollbar = self.scrollbar.position(self.scroll_pos); } fn remove_char(&mut self, idx: usize) { @@ -175,22 +212,24 @@ impl<'a> App { ) } + /// Start render loop pub async fn run( mut self, input_tx: UnboundedSender, mut output_rx: UnboundedReceiver, tick_rate: Duration, ) -> io::Result<()> { - let mut exitspam: VecDeque = VecDeque::with_capacity(3); + let mut spam_handler = InterruptHandler::new(3); let stdout = io::stdout(); let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let mut prev_tick = Instant::now(); + let mut res: io::Result<()> = Ok(()); // setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); - execute!(stdout, EnableMouseCapture, EnterAlternateScreen)?; + execute!(stdout, EnterAlternateScreen)?; loop { terminal.draw(|f| self.ui(f))?; @@ -201,58 +240,68 @@ impl<'a> App { let timeout = tick_rate.saturating_sub(prev_tick.elapsed()); if event::poll(timeout)? { - match event::read()? { - Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { - KeyCode::Enter => { - let entr_txt: String = self.submit(); - input_tx.send(format!("{}\r\n", entr_txt.clone())).unwrap(); - if entr_txt.to_uppercase() == "EXIT" { - break; - } - } - KeyCode::Char('c') - if key.modifiers == KeyModifiers::from_name("CONTROL").unwrap() => - { - if input_tx.send("stop\n".to_string()).is_err() { - self.output.push("Couldn't stop!".to_string()); - } - if exitspam.len() == 3 { - if let Some(time) = exitspam.pop_back() { - if Instant::now() - time <= Duration::new(3, 0) { - input_tx.send("EXIT".to_string()).expect("Couldn't exit!"); + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match self.input_mode { + InputMode::Insert => { + match key.code { + KeyCode::Enter => { + let entr_txt: String = self.submit(); + input_tx.send(format!("{}\r\n", entr_txt.clone())).unwrap(); + if entr_txt.to_uppercase() == "EXIT" { break; - } else { - exitspam.push_front(Instant::now()); } } - } else { - exitspam.push_front(Instant::now()); + KeyCode::Char('c') + if key.modifiers == KeyModifiers::from_name("CONTROL").unwrap() => + { + if input_tx.send("stop\n".to_string()).is_err() { + self.output.push("Couldn't stop!".to_string()); + } + if spam_handler.interrupted() { + res = input_tx.send("EXIT".to_string()).map_err(|e| io::Error::new(ErrorKind::Other, e.0)); + break; + } + } + KeyCode::Char(c) => self.put_char(c), + KeyCode::Backspace => self.delete_char(), + KeyCode::Up => { + self.input = self.cmd_history.prev_cmd(); + self.cursor_pos = self.input.len(); + } + KeyCode::Down => { + self.input = self.cmd_history.next_cmd(); + self.cursor_pos = self.input.len(); + } + KeyCode::Left => self.cursor_left(), + KeyCode::Right => self.cursor_right(), + KeyCode::PageUp => self.scroll_up(), + KeyCode::PageDown => self.scroll_down(), + KeyCode::Esc => self.input_mode = InputMode::Normal, + + _ => (), } } - KeyCode::Char(c) => self.put_char(c), - KeyCode::Backspace => self.delete_char(), - KeyCode::Up => self.input = self.cmd_history.prev_cmd(), - KeyCode::Down => self.input = self.cmd_history.next_cmd(), - KeyCode::Left => self.cursor_left(), - KeyCode::Right => self.cursor_right(), - KeyCode::PageUp => self.scroll_up(), - KeyCode::PageDown => self.scroll_down(), - - _ => (), - }, - Event::Mouse(me) => match me.kind { - MouseEventKind::ScrollUp => self.scroll_up(), - MouseEventKind::ScrollDown => self.scroll_down(), - _ => (), - }, - _ => (), + InputMode::Normal => { + match key.code { + KeyCode::Up | KeyCode::PageUp => self.scroll_up(), + KeyCode::Down | KeyCode::PageDown => self.scroll_down(), + KeyCode::Char('e') => self.input_mode = InputMode::Insert, + _ => () + } + } + } + } } } + if prev_tick.elapsed() >= tick_rate { prev_tick = Instant::now(); } } - Self::shutdown(terminal) + Self::shutdown(terminal)?; + + res } fn ui(&mut self, f: &mut Frame) { @@ -262,11 +311,25 @@ impl<'a> App { .constraints([Constraint::Min(1), Constraint::Length(3)].as_ref()) .split(f.size()); - // Message Box + let (msg_color, input_color) = match self.input_mode { + InputMode::Insert => (Color::Yellow, Color::White), + InputMode::Normal => (Color::White, Color::Yellow) + }; + + // Set scroll position let lines: Vec = self.output.iter().map(Self::parse).collect(); - self.scroll_state = self.scroll_state.content_length(lines.len()); + let box_height = chunks[0].height as usize; + let visible_len = (lines.len() as isize - box_height as isize + 2).clamp(0, lines.len() as isize); + if !self.manual_scroll { + self.scroll_pos = visible_len as usize; + } else if self.scroll_pos >= visible_len as usize { + self.manual_scroll = false; + } + self.scrollbar = self.scrollbar.content_length(lines.len()); + + // Message Box let messages = Paragraph::new(lines) - .block(Block::default().borders(Borders::ALL).title("Messages")) + .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(msg_color)).title("Messages")) .scroll((self.scroll_pos as u16, 0)); f.render_widget(messages, chunks[0]); f.render_stateful_widget( @@ -274,13 +337,13 @@ impl<'a> App { .begin_symbol(Some("^")) .end_symbol(Some("v")), chunks[0], - &mut self.scroll_state, + &mut self.scrollbar, ); // Input Box let input = Paragraph::new(self.input.as_str()) .style(Style::default().fg(Color::Yellow)) - .block(Block::default().borders(Borders::ALL).title("Input")); + .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(input_color)).title("Input")); f.render_widget(input, chunks[1]); // Show cursor f.set_cursor( @@ -296,7 +359,6 @@ impl<'a> App { disable_raw_mode()?; execute!( terminal.backend_mut(), - DisableMouseCapture, LeaveAlternateScreen )?; terminal.show_cursor()?; diff --git a/src/input.rs b/src/input.rs index ff2ff2a..e9b1e28 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,7 +1,7 @@ use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use std::collections::VecDeque; use std::time::{Instant, Duration}; -use rustyline::{Cmd, EventHandler, KeyCode, KeyEvent, Modifiers}; +use rustyline::{Cmd, KeyCode, KeyEvent, Modifiers}; use crate::error; @@ -12,36 +12,34 @@ pub fn receiver(sender: UnboundedSender) { rl.bind_sequence(KeyEvent(KeyCode::Up, Modifiers::empty()), Cmd::LineUpOrPreviousHistory(1)); rl.bind_sequence(KeyEvent(KeyCode::Down, Modifiers::empty()), Cmd::LineDownOrNextHistory(1)); - loop { - match rl.readline(">> ") { - Ok(line) => { - rl.add_history_entry(&line).expect("TODO: panic message"); - if sender.send(format!("{}\r\n", line.clone())).is_err() { - error!("Couldn't report input to main thread!"); - } + match rl.readline(">> ") { + Ok(line) => { + rl.add_history_entry(&line).expect("TODO: panic message"); + if sender.send(format!("{}\r\n", line.clone())).is_err() { + error!("Couldn't report input to main thread!"); + } - if line.trim().to_uppercase() == "EXIT" { - break; - } + if line.trim().to_uppercase() == "EXIT" { + return; } - Err(rustyline::error::ReadlineError::Interrupted) => { - sender.send("stop\n".to_string()).expect("Couldn't stop!"); + } + Err(rustyline::error::ReadlineError::Interrupted) => { + sender.send("stop\n".to_string()).expect("Couldn't stop!"); - if exitspam.len() == 3 { - if let Some(time) = exitspam.pop_back() { - if Instant::now() - time <= Duration::new(3, 0) { - sender.send("EXIT".to_string()).expect("Couldn't exit!"); - break; - } else { - exitspam.push_front(Instant::now()); - } + if exitspam.len() == 3 { + if let Some(time) = exitspam.pop_back() { + if Instant::now() - time <= Duration::new(3, 0) { + sender.send("EXIT".to_string()).expect("Couldn't exit!"); + return; + } else { + exitspam.push_front(Instant::now()); } - } else { - exitspam.push_front(Instant::now()); } + } else { + exitspam.push_front(Instant::now()); } - Err(e) => error!(e) } + Err(e) => error!(e) } } diff --git a/src/main.rs b/src/main.rs index 2bc39d5..0d91963 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,8 +25,7 @@ async fn monitor( let input_clone = input_tx.clone(); std::thread::spawn(|| input::receiver(input_clone)); - - + let tty_path = if cmd_port.is_some() { cmd_port } else if auto { @@ -36,7 +35,12 @@ async fn monitor( }; if let Some(inner_tty_path) = tty_path { - let settings = tokio_serial::new(&inner_tty_path, 115200).data_bits(DataBits::Eight).flow_control(FlowControl::None).parity(Parity::None).stop_bits(StopBits::One).timeout(Duration::from_secs(10)); + let settings = tokio_serial::new(&inner_tty_path, 115200) + .data_bits(DataBits::Eight) + .flow_control(FlowControl::None) + .parity(Parity::None) + .stop_bits(StopBits::One) + .timeout(Duration::from_secs(10)); #[allow(unused_mut)] // Ignore warning from windows compilers if let Ok(mut port) = tokio_serial::SerialStream::open(&settings) { #[cfg(unix)] From da7865ccd15eb34f984cf4b5843c2d0baec4346b Mon Sep 17 00:00:00 2001 From: JerelJr Date: Fri, 10 May 2024 00:38:46 -0400 Subject: [PATCH 4/7] Made changes according to clippy suggestions --- Cargo.toml | 6 +++--- src/app.rs | 2 +- src/handler.rs | 2 +- src/input.rs | 21 --------------------- src/main.rs | 10 ++++------ src/output.rs | 4 ++-- src/port.rs | 2 +- 7 files changed, 12 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ad77c34..55e72d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,14 @@ documentation = "https://github.com/SpacehuhnTech/Huhnitor" edition = "2018" [dependencies] -tokio = { version = "1.37.0", features = [ "full" ] } -tokio-util = { version = "0.7.10", features = [ "codec" ] } +tokio = { version = "1.37.0", features = ["full"] } +tokio-util = { version = "0.7.10", features = ["codec"] } tokio-serial = "5.4.4" serialport = "4.3.0" futures = "0.3.5" bytes = "1.6.0" -webbrowser = "0.8.13" +webbrowser = "1.0.1" lazy_static = "1.4.0" structopt = "0.3.15" diff --git a/src/app.rs b/src/app.rs index a32c5d5..fdb758a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -286,7 +286,7 @@ impl<'a> App { match key.code { KeyCode::Up | KeyCode::PageUp => self.scroll_up(), KeyCode::Down | KeyCode::PageDown => self.scroll_down(), - KeyCode::Char('e') => self.input_mode = InputMode::Insert, + KeyCode::Esc => self.input_mode = InputMode::Insert, _ => () } } diff --git a/src/handler.rs b/src/handler.rs index 0290088..e5e0fcf 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -7,7 +7,7 @@ pub fn handle(command: String) -> String { let words = command.split(' ').collect::>(); let len = words.len(); if let Some(key) = words.get(1) { - match key.to_uppercase().trim().as_ref() { + match key.to_uppercase().trim() { "READ" => { if len > 2 { let mut out = String::new(); diff --git a/src/input.rs b/src/input.rs index e9b1e28..02422ac 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,13 +1,9 @@ use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -use std::collections::VecDeque; -use std::time::{Instant, Duration}; use rustyline::{Cmd, KeyCode, KeyEvent, Modifiers}; use crate::error; pub fn receiver(sender: UnboundedSender) { - let mut exitspam: VecDeque = VecDeque::with_capacity(3); - let mut rl = rustyline::DefaultEditor::new().expect("Unable to start command history"); rl.bind_sequence(KeyEvent(KeyCode::Up, Modifiers::empty()), Cmd::LineUpOrPreviousHistory(1)); rl.bind_sequence(KeyEvent(KeyCode::Down, Modifiers::empty()), Cmd::LineDownOrNextHistory(1)); @@ -18,26 +14,9 @@ pub fn receiver(sender: UnboundedSender) { if sender.send(format!("{}\r\n", line.clone())).is_err() { error!("Couldn't report input to main thread!"); } - - if line.trim().to_uppercase() == "EXIT" { - return; - } } Err(rustyline::error::ReadlineError::Interrupted) => { sender.send("stop\n".to_string()).expect("Couldn't stop!"); - - if exitspam.len() == 3 { - if let Some(time) = exitspam.pop_back() { - if Instant::now() - time <= Duration::new(3, 0) { - sender.send("EXIT".to_string()).expect("Couldn't exit!"); - return; - } else { - exitspam.push_front(Instant::now()); - } - } - } else { - exitspam.push_front(Instant::now()); - } } Err(e) => error!(e) } diff --git a/src/main.rs b/src/main.rs index 0d91963..b43767b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,7 @@ async fn monitor( let input_clone = input_tx.clone(); std::thread::spawn(|| input::receiver(input_clone)); - + let tty_path = if cmd_port.is_some() { cmd_port } else if auto { @@ -51,10 +51,8 @@ async fn monitor( out.connected(&inner_tty_path); - if !no_welcome { - if let Err(_) = port.write("welcome\r\n".as_bytes()).await { - out.print("Couldn't send welcome command!"); - } + if !no_welcome && port.write("welcome\r\n".as_bytes()).await.is_err() { + out.print("Couldn't send welcome command!"); } tokio::spawn(async move { app.run(input_tx, output_rx, Duration::from_millis(15)).await }); @@ -140,7 +138,7 @@ async fn main() { if args.driver { out.driver(); } else { - let mut app = app::App::new(); + let app = App::new(); monitor(args.port, !args.auto, args.no_welcome, &out, app).await; } diff --git a/src/output.rs b/src/output.rs index 796db8d..d19cb46 100644 --- a/src/output.rs +++ b/src/output.rs @@ -12,7 +12,7 @@ macro_rules! error { // Statically compile regex to avoid repetetive compiling // Rust Regex can be tested here: https://rustexp.lpil.uk/ lazy_static::lazy_static! { - static ref REGSET: RegexSet = RegexSet::new(&[ + static ref REGSET: RegexSet = RegexSet::new([ r"^(\x60|\.|:|/|-|\+|o|s|h|d|y| ){50,}", // ASCII Chicken r"^# ", // # command r"(?m)^\s*(-|=|#)+\s*$", // ================ @@ -77,7 +77,7 @@ pub struct Preferences { impl Preferences { pub fn print(&self, s: &str) { if self.color_enabled { - parse(&s); + parse(s); } else { print!("{}", s); } diff --git a/src/port.rs b/src/port.rs index e135f25..8c840f2 100644 --- a/src/port.rs +++ b/src/port.rs @@ -10,7 +10,7 @@ async fn detect_port(ports: &mut Vec) -> Option { if let Ok(new_ports) = available_ports() { for path in &new_ports { - if !ports.contains(&path) { + if !ports.contains(path) { return Some(path.port_name.clone()); } } From 5bab3288c71c4463f89410e6ceed28cc67968870 Mon Sep 17 00:00:00 2001 From: JerelJr Date: Fri, 10 May 2024 18:10:08 -0400 Subject: [PATCH 5/7] Fixed exit interrupt bug --- src/app.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index fdb758a..da679af 100644 --- a/src/app.rs +++ b/src/app.rs @@ -49,24 +49,27 @@ lazy_static::lazy_static! { ]; } -struct InterruptHandler(VecDeque); +struct InterruptHandler { + spam: VecDeque, + cap: usize, +} impl InterruptHandler { fn new(cap: usize) -> Self { - Self(VecDeque::with_capacity(cap)) + Self { spam: VecDeque::with_capacity(cap), cap } } fn interrupted(&mut self) -> bool { - if self.0.len() == 3 { - if let Some(time) = self.0.pop_back() { + if self.spam.len() == self.cap { + if let Some(time) = self.spam.pop_back() { if Instant::now() - time <= Duration::new(3, 0) { true } else { - self.0.push_front(Instant::now()); + self.spam.push_front(Instant::now()); false } } else { false } } else { - self.0.push_front(Instant::now()); + self.spam.push_front(Instant::now()); false } } @@ -219,7 +222,7 @@ impl<'a> App { mut output_rx: UnboundedReceiver, tick_rate: Duration, ) -> io::Result<()> { - let mut spam_handler = InterruptHandler::new(3); + let mut spam_handler = InterruptHandler::new(2); let stdout = io::stdout(); let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; From 70efb1199262d3506ac87c2efe1a49810bfb8fb0 Mon Sep 17 00:00:00 2001 From: JerelJr Date: Fri, 10 May 2024 19:23:17 -0400 Subject: [PATCH 6/7] Removed TODO --- src/input.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input.rs b/src/input.rs index 02422ac..fc1379e 100644 --- a/src/input.rs +++ b/src/input.rs @@ -10,7 +10,7 @@ pub fn receiver(sender: UnboundedSender) { match rl.readline(">> ") { Ok(line) => { - rl.add_history_entry(&line).expect("TODO: panic message"); + rl.add_history_entry(&line).expect("Unable to add history entry"); if sender.send(format!("{}\r\n", line.clone())).is_err() { error!("Couldn't report input to main thread!"); } From a9ab760e296193659b74bb47342a157901478a9c Mon Sep 17 00:00:00 2001 From: JerelJr Date: Tue, 14 May 2024 23:12:11 -0400 Subject: [PATCH 7/7] Reduced nesting in event handling loop --- src/app.rs | 109 +++++++++++++++++++++++++++++------------------------ 1 file changed, 60 insertions(+), 49 deletions(-) diff --git a/src/app.rs b/src/app.rs index da679af..0618346 100644 --- a/src/app.rs +++ b/src/app.rs @@ -21,6 +21,7 @@ use std::{ time::{Duration, Instant}, }; use std::io::ErrorKind; +use crossterm::event::KeyEvent; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; lazy_static::lazy_static! { @@ -107,6 +108,7 @@ impl History { } } +#[derive(PartialEq)] enum InputMode { Normal, Insert, @@ -215,6 +217,58 @@ impl<'a> App { ) } + fn event_handler(&mut self, key: KeyEvent, spam_handler: &mut InterruptHandler, input_tx: &UnboundedSender) -> io::Result { + if key.kind == KeyEventKind::Press && self.input_mode == InputMode::Insert { + match key.code { + KeyCode::Enter => { + let entr_txt: String = self.submit(); + input_tx.send(format!("{}\r\n", entr_txt.clone())).unwrap(); + if entr_txt.to_uppercase() == "EXIT" { + return Ok(false); + } + } + KeyCode::Char('c') + if key.modifiers == KeyModifiers::from_name("CONTROL").unwrap() => { + if input_tx.send("stop\n".to_string()).is_err() { + self.output.push("Couldn't stop!".to_string()); + } + if spam_handler.interrupted() { + let res: io::Result = match input_tx.send("EXIT".to_string()) { + Ok(_) => Ok(false), + Err(e) => Err(io::Error::new(ErrorKind::Other, e.0)) + }; + return res; + } + } + KeyCode::Char(c) => self.put_char(c), + KeyCode::Backspace => self.delete_char(), + KeyCode::Up => { + self.input = self.cmd_history.prev_cmd(); + self.cursor_pos = self.input.len(); + } + KeyCode::Down => { + self.input = self.cmd_history.next_cmd(); + self.cursor_pos = self.input.len(); + } + KeyCode::Left => self.cursor_left(), + KeyCode::Right => self.cursor_right(), + KeyCode::PageUp => self.scroll_up(), + KeyCode::PageDown => self.scroll_down(), + KeyCode::Esc => self.input_mode = InputMode::Normal, + + _ => (), + } + } else if key.kind == KeyEventKind::Press && self.input_mode == InputMode::Normal { + match key.code { + KeyCode::Up | KeyCode::PageUp => self.scroll_up(), + KeyCode::Down | KeyCode::PageDown => self.scroll_down(), + KeyCode::Esc => self.input_mode = InputMode::Insert, + _ => () + } + } + Ok(true) + } + /// Start render loop pub async fn run( mut self, @@ -244,56 +298,13 @@ impl<'a> App { let timeout = tick_rate.saturating_sub(prev_tick.elapsed()); if event::poll(timeout)? { if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { - match self.input_mode { - InputMode::Insert => { - match key.code { - KeyCode::Enter => { - let entr_txt: String = self.submit(); - input_tx.send(format!("{}\r\n", entr_txt.clone())).unwrap(); - if entr_txt.to_uppercase() == "EXIT" { - break; - } - } - KeyCode::Char('c') - if key.modifiers == KeyModifiers::from_name("CONTROL").unwrap() => - { - if input_tx.send("stop\n".to_string()).is_err() { - self.output.push("Couldn't stop!".to_string()); - } - if spam_handler.interrupted() { - res = input_tx.send("EXIT".to_string()).map_err(|e| io::Error::new(ErrorKind::Other, e.0)); - break; - } - } - KeyCode::Char(c) => self.put_char(c), - KeyCode::Backspace => self.delete_char(), - KeyCode::Up => { - self.input = self.cmd_history.prev_cmd(); - self.cursor_pos = self.input.len(); - } - KeyCode::Down => { - self.input = self.cmd_history.next_cmd(); - self.cursor_pos = self.input.len(); - } - KeyCode::Left => self.cursor_left(), - KeyCode::Right => self.cursor_right(), - KeyCode::PageUp => self.scroll_up(), - KeyCode::PageDown => self.scroll_down(), - KeyCode::Esc => self.input_mode = InputMode::Normal, - - _ => (), - } - } - InputMode::Normal => { - match key.code { - KeyCode::Up | KeyCode::PageUp => self.scroll_up(), - KeyCode::Down | KeyCode::PageDown => self.scroll_down(), - KeyCode::Esc => self.input_mode = InputMode::Insert, - _ => () - } - } + match self.event_handler(key, &mut spam_handler, &input_tx) { + Ok(false) => break, + Err(e) => { + res = Err(e); + break; } + _ => () } } }