commit c1d54f8b5e0912c3baeed21c89183b4b5ab1354f Author: Austen Adler Date: Sat Apr 24 13:35:39 2021 -0400 Smush all history into one commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..799021d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,88 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "libc" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" + +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "redox_syscall" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall", +] + +[[package]] +name = "rpn_rs" +version = "0.1.0" +dependencies = [ + "termion", + "tui", +] + +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + +[[package]] +name = "tui" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ced152a8e9295a5b168adc254074525c17ac4a83c90b2716274cc38118bddc9" +dependencies = [ + "bitflags", + "cassowary", + "termion", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0f86362 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rpn_rs" +version = "0.1.0" +authors = ["Austen Adler "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tui = "0.14" +termion = "1.5" diff --git a/src/calc.rs b/src/calc.rs new file mode 100644 index 0000000..0801da1 --- /dev/null +++ b/src/calc.rs @@ -0,0 +1,335 @@ +pub mod constants; +pub mod errors; +pub mod operations; + +use constants::CalculatorConstant; +use constants::CalculatorConstants; +use constants::CalculatorConstantsIter; +use constants::CalculatorMacro; +use constants::CalculatorMacros; +use constants::CalculatorMacrosIter; +use constants::CalculatorRegisters; +use constants::CalculatorRegistersIter; +use errors::CalculatorError; +use operations::CalculatorOperation; +use std::collections::VecDeque; + +enum OpArgs { + Unary(f64), + Binary([f64; 2]), + None, +} + +struct CalculatorStateChange { + pop: OpArgs, + push: OpArgs, +} + +pub struct Calculator<'a> { + stack: VecDeque, + macros: CalculatorMacros<'a>, + constants: CalculatorConstants<'a>, + registers: CalculatorRegisters, + undo_buf: Vec, + redo_buf: Vec, +} + +impl<'a> Default for Calculator<'a> { + fn default() -> Calculator<'a> { + Calculator { + stack: vec![1.2, 1.3].into_iter().collect(), + undo_buf: vec![], + redo_buf: vec![], + registers: CalculatorRegisters::new(), + macros: [ + ( + 'm', + CalculatorMacro { + help: "64?>64%", + value: " 64?>64%", + }, + ), + ( + 'u', + CalculatorMacro { + help: "Quadratic Formula", + value: "RcRbRarbnrb2 ^4 rarc**-v+2 ra*/rbnrb2^4 rarc**-v-2 ra*/", + }, + ), + ( + 's', + CalculatorMacro { + help: "Sample data", + value: "\\\\2 5 3n", + }, + ), + ] + .iter() + .cloned() + .collect(), + constants: [ + ( + 't', + CalculatorConstant { + help: "Tau (2pi)", + value: std::f64::consts::TAU, + }, + ), + ( + 'e', + CalculatorConstant { + help: "Euler's Number e", + value: std::f64::consts::E, + }, + ), + ( + 'p', + CalculatorConstant { + help: "Pi", + value: std::f64::consts::PI, + }, + ), + ] + .iter() + .cloned() + .collect(), + } + } +} + +impl<'a> Calculator<'a> { + pub fn get_constants_iter(&'a self) -> CalculatorConstantsIter<'a> { + self.constants.iter() + } + pub fn get_macros_iter(&'a self) -> CalculatorMacrosIter<'a> { + self.macros.iter() + } + pub fn get_registers_iter(&self) -> CalculatorRegistersIter { + self.registers.iter() + } + + pub fn push_constant(&mut self, key: char) -> Result<(), CalculatorError> { + match self.constants.get(&key) { + Some(CalculatorConstant { value, .. }) => { + let value = *value; + self.push(value) + } + None => Err(CalculatorError::NoSuchConstant), + } + } + pub fn push_register(&mut self, key: char) -> Result<(), CalculatorError> { + match self.registers.get(&key) { + Some(f) => { + let f = *f; + self.push(f) + } + None => Err(CalculatorError::NoSuchRegister), + } + } + // TODO: Use hashmap + pub fn save_register(&mut self, key: char) -> Result<(), CalculatorError> { + let f = self.pop()?; + self.registers.insert(key, f); + Ok(()) + } + pub fn get_macro(&mut self, key: char) -> Result<&CalculatorMacro<'a>, CalculatorError> { + match self.macros.get(&key) { + Some(m) => Ok(m), + None => Err(CalculatorError::NoSuchMacro), + } + } + + pub fn push(&mut self, f: f64) -> Result<(), CalculatorError> { + self.direct_state_change(CalculatorStateChange { + pop: OpArgs::None, + push: OpArgs::Unary(f), + }) + } + pub fn pop(&mut self) -> Result { + let f = self.checked_get(0)?; + self.direct_state_change(CalculatorStateChange { + pop: OpArgs::Unary(f), + push: OpArgs::None, + })?; + Ok(f) + } + pub fn get_stack(&self) -> &VecDeque { + &self.stack + } + //TODO: VecDeque could have other types + pub fn op(&mut self, op: CalculatorOperation) -> Result<(), CalculatorError> { + let state_change = match op { + CalculatorOperation::Add => self.binary_op(|[a, b]| OpArgs::Unary(b + a)), + CalculatorOperation::Subtract => self.binary_op(|[a, b]| OpArgs::Unary(b - a)), + CalculatorOperation::Multiply => self.binary_op(|[a, b]| OpArgs::Unary(b * a)), + CalculatorOperation::Divide => self.binary_op(|[a, b]| OpArgs::Unary(b / a)), + CalculatorOperation::IntegerDivide => { + self.binary_op(|[a, b]| OpArgs::Unary(b.div_euclid(a))) + } + CalculatorOperation::Negate => self.unary_op(|a| OpArgs::Unary(-a)), + CalculatorOperation::AbsoluteValue => self.unary_op(|a| OpArgs::Unary(a.abs())), + CalculatorOperation::Inverse => self.unary_op(|a| OpArgs::Unary(1.0 / a)), + CalculatorOperation::Modulo => self.binary_op(|[a, b]| OpArgs::Unary(b % a)), + //CalculatorOperation::Remainder => { + // self.binary_op(|[a, b]| OpArgs::Unary(b.rem_euclid(a))) + //} + CalculatorOperation::Dup => self.unary_op(|a| OpArgs::Binary([a, a])), + CalculatorOperation::Drop => self.unary_op(|_| OpArgs::None), + CalculatorOperation::Swap => self.binary_op(|[a, b]| OpArgs::Binary([b, a])), + CalculatorOperation::Sin => self.unary_op(|a| OpArgs::Unary(a.sin())), + CalculatorOperation::Cos => self.unary_op(|a| OpArgs::Unary(a.cos())), + CalculatorOperation::Tan => self.unary_op(|a| OpArgs::Unary(a.tan())), + CalculatorOperation::ASin => self.unary_op(|a| OpArgs::Unary(a.asin())), + CalculatorOperation::ACos => self.unary_op(|a| OpArgs::Unary(a.acos())), + CalculatorOperation::ATan => self.unary_op(|a| OpArgs::Unary(a.atan())), + CalculatorOperation::Sqrt => self.unary_op(|a| OpArgs::Unary(a.sqrt())), + // CalculatorOperation::Factorial => vec![args[0].()], + CalculatorOperation::Log => self.unary_op(|a| OpArgs::Unary(a.log10())), + CalculatorOperation::Ln => self.unary_op(|a| OpArgs::Unary(a.ln())), + CalculatorOperation::Pow => self.binary_op(|[a, b]| OpArgs::Unary(b.powf(a))), + CalculatorOperation::E => self.binary_op(|[a, b]| OpArgs::Unary(b * 10.0f64.powf(a))), + CalculatorOperation::Undo => { + let s = self + .undo_buf + .pop() + .ok_or_else(|| CalculatorError::EmptyHistory(String::from("undo")))?; + return self.apply_state_change(s, false); + } + CalculatorOperation::Redo => { + let s = self + .redo_buf + .pop() + .ok_or_else(|| CalculatorError::EmptyHistory(String::from("redo")))?; + return self.apply_state_change(s, true); + } + }; + + self.direct_state_change(state_change?) + } + + fn unary_op( + &mut self, + op: impl FnOnce(f64) -> OpArgs, + ) -> Result { + let arg = self + .stack + .get(0) + .ok_or(CalculatorError::NotEnoughStackEntries)?; + Ok(CalculatorStateChange { + pop: OpArgs::Unary(*arg), + push: op(*arg), + }) + } + fn binary_op( + &mut self, + op: impl FnOnce([f64; 2]) -> OpArgs, + ) -> Result { + let args: [f64; 2] = [ + *self + .stack + .get(0) + .ok_or(CalculatorError::NotEnoughStackEntries)?, + *self + .stack + .get(1) + .ok_or(CalculatorError::NotEnoughStackEntries)?, + ]; + Ok(CalculatorStateChange { + pop: OpArgs::Binary(args), + push: op(args), + }) + } + + fn direct_state_change(&mut self, c: CalculatorStateChange) -> Result<(), CalculatorError> { + let result = self.apply_state_change(c, true); + if result.is_ok() { + // Clear the redo buffer since this was a direct state change + self.redo_buf = vec![]; + } + result + } + + fn apply_state_change( + &mut self, + c: CalculatorStateChange, + forward: bool, + ) -> Result<(), CalculatorError> { + let (to_pop, to_push) = if forward { + (&c.pop, &c.push) + } else { + (&c.push, &c.pop) + }; + + match to_push { + OpArgs::Unary(a) => { + if a.is_nan() || a.is_infinite() { + return Err(CalculatorError::ArithmeticError); + } + } + OpArgs::Binary([a, b]) => { + if a.is_nan() || b.is_nan() || a.is_infinite() || b.is_infinite() { + return Err(CalculatorError::ArithmeticError); + } + } + OpArgs::None => {} + }; + + match to_pop { + OpArgs::Unary(a) => { + self.stack_eq(0, *a)?; + self.stack.pop_front(); + } + OpArgs::Binary([a, b]) => { + if forward { + self.stack_eq(0, *a)?; + self.stack_eq(1, *b)?; + } else { + self.stack_eq(0, *a)?; + self.stack_eq(1, *b)?; + } + self.stack.pop_front(); + self.stack.pop_front(); + } + OpArgs::None => {} + }; + + match to_push { + OpArgs::Unary(a) => { + self.stack.push_front(*a); + } + OpArgs::Binary([a, b]) => { + self.stack.push_front(*b); + self.stack.push_front(*a); + } + OpArgs::None => {} + }; + + if forward { + self.undo_buf.push(c); + } else { + self.redo_buf.push(c); + } + + Ok(()) + } + + fn stack_eq(&self, idx: usize, value: f64) -> Result<(), CalculatorError> { + if (self.checked_get(idx)? - value).abs() > f64::EPSILON { + Err(CalculatorError::CorruptStateChange(format!( + "Stack index {} should be {}, but is {}", + idx, + value, + self.checked_get(idx)?, + ))) + } else { + Ok(()) + } + } + + fn checked_get(&self, idx: usize) -> Result { + match self.stack.get(idx) { + None => Err(CalculatorError::NotEnoughStackEntries), + Some(r) => Ok(*r), + } + } +} diff --git a/src/calc/constants.rs b/src/calc/constants.rs new file mode 100644 index 0000000..e8d84ea --- /dev/null +++ b/src/calc/constants.rs @@ -0,0 +1,23 @@ +use std::collections::hash_map::Iter; +use std::collections::HashMap; + +#[derive(Debug, Clone, Copy)] +pub struct CalculatorConstant<'a> { + pub help: &'a str, + pub value: f64, +} + +#[derive(Debug, Clone, Copy)] +pub struct CalculatorMacro<'a> { + pub help: &'a str, + pub value: &'a str, +} + +pub type CalculatorConstants<'a> = HashMap>; +pub type CalculatorConstantsIter<'a> = Iter<'a, char, CalculatorConstant<'a>>; + +pub type CalculatorMacros<'a> = HashMap>; +pub type CalculatorMacrosIter<'a> = Iter<'a, char, CalculatorMacro<'a>>; + +pub type CalculatorRegisters = HashMap; +pub type CalculatorRegistersIter<'a> = Iter<'a, char, f64>; diff --git a/src/calc/errors.rs b/src/calc/errors.rs new file mode 100644 index 0000000..6ffa49f --- /dev/null +++ b/src/calc/errors.rs @@ -0,0 +1,26 @@ +pub enum CalculatorError { + ArithmeticError, + NotEnoughStackEntries, + CorruptStateChange(String), + EmptyHistory(String), + NoSuchConstant, + NoSuchRegister, + NoSuchMacro, +} + +impl CalculatorError { + //TODO: Use &str instead of Strings + pub fn message(&self) -> String { + match self { + CalculatorError::ArithmeticError => String::from("Arithmetic Error"), + CalculatorError::NotEnoughStackEntries => String::from("Not enough items in the stack"), + CalculatorError::CorruptStateChange(msg) => { + String::from("Corrupt state change: ") + msg + } + CalculatorError::EmptyHistory(msg) => String::from("No history to ") + msg, + CalculatorError::NoSuchConstant => String::from("No such constant"), + CalculatorError::NoSuchRegister => String::from("No such register"), + CalculatorError::NoSuchMacro => String::from("No such macro"), + } + } +} diff --git a/src/calc/operations.rs b/src/calc/operations.rs new file mode 100644 index 0000000..4bfcca7 --- /dev/null +++ b/src/calc/operations.rs @@ -0,0 +1,102 @@ +pub enum CalculatorOperation { + Add, + Subtract, + Multiply, + Divide, + Negate, + AbsoluteValue, + Inverse, + Modulo, + IntegerDivide, + //Remainder, + Drop, + Dup, + Swap, + Sin, + Cos, + Tan, + ASin, + ACos, + ATan, + Sqrt, + Undo, + Redo, + Pow, + // Factorial, + Log, + Ln, + E, +} + +impl CalculatorOperation { + pub fn from_char(key: char) -> Result { + match key { + '+' => Ok(CalculatorOperation::Add), + '-' => Ok(CalculatorOperation::Subtract), + '*' => Ok(CalculatorOperation::Multiply), + '/' => Ok(CalculatorOperation::Divide), + 'n' => Ok(CalculatorOperation::Negate), + '|' => Ok(CalculatorOperation::AbsoluteValue), + 'i' => Ok(CalculatorOperation::Inverse), + '%' => Ok(CalculatorOperation::Modulo), + //'r' => Ok(CalculatorOperation::Remainder), + '\\' => Ok(CalculatorOperation::Drop), + '?' => Ok(CalculatorOperation::IntegerDivide), + '\n' => Ok(CalculatorOperation::Dup), + '>' => Ok(CalculatorOperation::Swap), + 's' => Ok(CalculatorOperation::Sin), + 'c' => Ok(CalculatorOperation::Cos), + 't' => Ok(CalculatorOperation::Tan), + 'S' => Ok(CalculatorOperation::ASin), + 'C' => Ok(CalculatorOperation::ACos), + 'T' => Ok(CalculatorOperation::ATan), + 'v' => Ok(CalculatorOperation::Sqrt), + //TODO: Should not be calculator states probably + 'u' => Ok(CalculatorOperation::Undo), + 'U' => Ok(CalculatorOperation::Redo), + '^' => Ok(CalculatorOperation::Pow), + //'!' => Ok(CalculatorOperation::Factorial), + 'l' => Ok(CalculatorOperation::Log), + 'L' => Ok(CalculatorOperation::Ln), + 'e' => Ok(CalculatorOperation::E), + _ => Err(()), + } + } + + pub fn num_stack(&self) -> usize { + match self { + CalculatorOperation::Add + | CalculatorOperation::Subtract + | CalculatorOperation::Multiply + | CalculatorOperation::Divide + | CalculatorOperation::Modulo + | CalculatorOperation::IntegerDivide + | CalculatorOperation::Swap + | CalculatorOperation::Pow + | CalculatorOperation::E + //| CalculatorOperation::Remainder + => 2, + CalculatorOperation::Negate + | CalculatorOperation::AbsoluteValue + | CalculatorOperation::Inverse + | CalculatorOperation::Drop + + | CalculatorOperation::Sin + | CalculatorOperation::Cos + | CalculatorOperation::Tan + | CalculatorOperation::ASin + | CalculatorOperation::ACos + | CalculatorOperation::ATan + | CalculatorOperation::Sqrt + | CalculatorOperation::Dup + + //|CalculatorOperation::Factorial + | CalculatorOperation::Log + | CalculatorOperation::Ln + => 1, + CalculatorOperation::Undo + | CalculatorOperation::Redo + => 0, + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8c59a0a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,444 @@ +#![allow(unused_variables)] +#![allow(dead_code)] + +mod calc; +mod util; + +use calc::constants::CalculatorMacro; +use calc::operations::CalculatorOperation; +use calc::Calculator; +use std::cmp; +use std::convert::TryFrom; +use std::process; +use util::event::{Event, Events}; +//use util::event::T; +use std::{error::Error, io}; +use termion::{event::Key, raw::IntoRawMode, screen::AlternateScreen}; +use tui::{ + backend::TermionBackend, + layout::{Constraint, Direction, Layout}, + style::{Modifier, Style}, + terminal::Frame, + text::{Span, Spans, Text}, + widgets::{Block, Borders, Clear, List, ListItem, Paragraph}, + Terminal, +}; + +struct Dimensions { + width: u16, + height: u16, +} + +enum RegisterState { + Save, + Load, +} + +enum AppState { + Calculator, + Help, + Constants, + Macros, + Registers(RegisterState), +} + +struct App<'a> { + input: String, + calculator: Calculator<'a>, + error_msg: Option, + state: AppState, + current_macro: Option, +} + +impl<'a> Default for App<'a> { + fn default() -> App<'a> { + App { + input: String::new(), + calculator: Calculator::default(), + error_msg: None, + state: AppState::Calculator, + current_macro: None, + } + } +} + +fn main() -> Result<(), Box> { + let stdout = io::stdout().into_raw_mode()?; + // let stdout = MouseTerminal::from(stdout); + let stdout = AlternateScreen::from(stdout); + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let events = Events::new(); + let mut app = App::default(); + + loop { + terminal.draw(|f| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints( + [ + Constraint::Length(1), + Constraint::Min(1), + Constraint::Length(3), + ] + .as_ref(), + ) + .split(f.size()); + + let msg = match &app.error_msg { + Some(e) => vec![ + Span::raw("Error: "), + Span::styled(e, Style::default().add_modifier(Modifier::RAPID_BLINK)), + ], + None => vec![ + Span::raw("Press "), + Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to exit, "), + Span::styled("h", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" for help"), + ], + }; + + let text = Text::from(Spans::from(msg)); + let help_message = Paragraph::new(text); + f.render_widget(help_message, chunks[0]); + + let mut stack: Vec = app + .calculator + .get_stack() + .iter() + .take(chunks[1].height as usize - 2) + .enumerate() + .rev() + .map(|(i, m)| { + let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))]; + ListItem::new(content) + }) + .collect(); + + // stack.insert( + // 0, + // ListItem::new(Span::raw(format!("dbg: {}", chunks[2].height))), + // ); + + for _ in 0..(chunks[1] + .height + .saturating_sub(stack.len() as u16) + .saturating_sub(2)) + { + stack.insert(0, ListItem::new(Span::raw("~"))); + } + + let stack = + List::new(stack).block(Block::default().borders(Borders::ALL).title("Stack")); + f.render_widget(stack, chunks[1]); + + let input = Paragraph::new(app.input.as_ref()) + .style(Style::default()) + .block(Block::default().borders(Borders::ALL).title("Input")); + f.render_widget(input, chunks[2]); + + f.set_cursor(chunks[2].x + app.input.len() as u16 + 1, chunks[2].y + 1); + + match &app.state { + AppState::Help => { + draw_clippy_rect( + ClippyRectangle { + title: "Help", + msg: "+ => Add s => Sin\n\ + - => Subtract c => Cos\n\ + * => Multiply t => Tan\n\ + / => Divide S => ASin\n\ + n => Negate C => ACos\n\ + | => Abs T => ATan\n\ + i => Inverse v => Sqrt\n\ + % => Modulo u => Undo\n\ + \\ => Drop U => Redo\n\ + ? => IntegerDivide ^ => Pow\n\ + => Dup l => Log\n\ + > => Swap L => Ln\n\ + e => E ^c => Constants\n\ + ^m => Macros rR => Registers", + }, + f, + ); + } + AppState::Constants => { + draw_clippy_rect( + ClippyRectangle { + title: "Constants", + msg: app + .calculator + .get_constants_iter() + .map(|(key, constant)| { + format!("{}: {} ({})", key, constant.help, constant.value) + }) + .fold(String::new(), |acc, s| acc + &s + "\n") + .trim_end(), + }, + f, + ); + } + AppState::Registers(state) => { + let title = match state { + RegisterState::Save => "Registers (press char to save)", + RegisterState::Load => "Registers", + }; + draw_clippy_rect( + ClippyRectangle { + title: title, + msg: app + .calculator + .get_registers_iter() + .map(|(key, value)| format!("{}: {}", key, value)) + .fold(String::new(), |acc, s| acc + &s + "\n") + .trim_end(), + }, + f, + ); + } + AppState::Macros => { + draw_clippy_rect( + ClippyRectangle { + title: "Macros", + msg: app + .calculator + .get_macros_iter() + .map(|(key, mac)| format!("{}: {}", key, mac.help)) + .fold(String::new(), |acc, s| acc + &s + "\n") + .trim_end(), + }, + f, + ); + } + _ => {} + } + })?; + + if let Event::Input(key) = events.next()? { + handle_key(&mut app, &events, key); + } + + for e in events.try_iter() { + match e { + Event::Input(key) => handle_key(&mut app, &events, key), + Event::MacroEnd => app.current_macro = None, + _ => continue, + } + } + + app.current_macro = None; + } + // TODO: Bubble up return Ok so we can handle saving + //Ok(()) +} + +fn calc_operation(app: &mut App, c: char) { + if let Ok(op) = CalculatorOperation::from_char(c) { + if let Ok(f) = app.input.parse::() { + if app.calculator.push(f).is_ok() { + app.input.clear(); + } + } + + app.error_msg = match app.calculator.op(op) { + Err(e) => Some(e.message()), + Ok(()) => None, + } + } +} + +struct ClippyRectangle<'a> { + title: &'a str, + msg: &'a str, +} + +impl ClippyRectangle<'_> { + // TODO: Make this static somehow + fn size(&self) -> Dimensions { + let (width, height) = self.msg.lines().fold((0, 0), |(width, height), l| { + (cmp::max(width, l.len()), height + 1) + }); + Dimensions { + width: u16::try_from(width).unwrap_or(u16::MAX), + height: u16::try_from(height).unwrap_or(u16::MAX), + } + } +} + +fn draw_clippy_rect(c: ClippyRectangle, f: &mut Frame>) { + let block = Block::default().title(c.title).borders(Borders::ALL); + let dimensions = c.size(); + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Min(1), + Constraint::Length(dimensions.height + 2), + ] + .as_ref(), + ) + .split(f.size()); + let area = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Min(1), + Constraint::Length(cmp::max(dimensions.width, c.title.len() as u16) + 2), + ] + .as_ref(), + ) + .split(popup_layout[1])[1]; + f.render_widget(Clear, area); + + let help_message = Paragraph::new(c.msg) + .style(Style::default()) + .block(Block::default().borders(Borders::ALL).title(c.title)); + f.render_widget(help_message, area); +} + +fn handle_key(app: &mut App, events: &Events, key: Key) { + match &app.state { + AppState::Calculator => { + app.error_msg = None; + match key { + Key::Char('q') => { + process::exit(0); + } + Key::Ctrl('c') => { + app.state = AppState::Constants; + } + Key::Char('r') => { + app.state = AppState::Registers(RegisterState::Load); + } + Key::Char('R') => { + app.state = AppState::Registers(RegisterState::Save); + } + Key::Char('m') => { + app.state = AppState::Macros; + } + Key::Char('h') => { + app.state = AppState::Help; + } + Key::Char(c @ '0'..='9') => { + app.input.push(c); + } + Key::Char(c @ 'e') | Key::Char(c @ '.') => { + if !app.input.contains(c) { + app.input.push(c); + } + } + Key::Char('\n') | Key::Char(' ') => { + if app.input.is_empty() { + calc_operation(app, '\n'); + } else if let Ok(f) = app.input.parse::() { + if app.calculator.push(f).is_ok() { + app.input.clear(); + } + } + } + Key::Right => { + calc_operation(app, '>'); + } + Key::Down => { + if let Ok(x) = app.calculator.pop() { + app.input = x.to_string(); + } + } + Key::Backspace => { + app.input.pop(); + } + Key::Delete => { + app.input.clear(); + } + Key::Char(c) => { + calc_operation(app, c); + } + _ => {} + } + } + AppState::Help => match key { + Key::Esc | Key::Char('q') => { + app.state = AppState::Calculator; + } + _ => {} + }, + AppState::Constants => match key { + Key::Esc | Key::Char('q') => { + app.state = AppState::Calculator; + } + Key::Char(c) => { + app.error_msg = match app.calculator.push_constant(c) { + Err(e) => Some(e.message()), + Ok(()) => { + app.input.clear(); + app.state = AppState::Calculator; + None + } + } + } + _ => {} + }, + AppState::Registers(task) => match key { + Key::Esc | Key::Char('q') => { + app.state = AppState::Calculator; + } + Key::Char(c) => match task { + RegisterState::Save => { + app.error_msg = match app.calculator.save_register(c) { + Err(e) => Some(e.message()), + Ok(()) => { + app.input.clear(); + app.state = AppState::Calculator; + None + } + } + } + RegisterState::Load => { + app.error_msg = match app.calculator.push_register(c) { + Err(e) => Some(e.message()), + Ok(()) => { + app.input.clear(); + app.state = AppState::Calculator; + None + } + } + } + }, + _ => {} + }, + AppState::Macros => match key { + Key::Esc | Key::Char('q') => { + app.state = AppState::Calculator; + } + Key::Char(c) => { + if !app.input.is_empty() { + //TODO: A better way to do this + if let Ok(f) = app.input.parse::() { + if app.calculator.push(f).is_ok() { + app.input.clear(); + } else { + return; + } + } else { + return; + } + } + + // TODO: Handle macros internally to the calculator + app.error_msg = match app.calculator.get_macro(c) { + Ok(CalculatorMacro { value, .. }) => { + // let value = *value; + events.fill_event_buf(value); + app.state = AppState::Calculator; + None + } + Err(e) => Some(e.message()), + } + } + _ => {} + }, + } +} diff --git a/src/user_input.rs b/src/user_input.rs new file mode 100644 index 0000000..6fcb631 --- /dev/null +++ b/src/user_input.rs @@ -0,0 +1,181 @@ +/// A simple example demonstrating how to handle user input. This is +/// a bit out of the scope of the library as it does not provide any +/// input handling out of the box. However, it may helps some to get +/// started. +/// +/// This is a very simple example: +/// * A input box always focused. Every character you type is registered +/// here +/// * Pressing Backspace erases a character +/// * Pressing Enter pushes the current input in the history of previous +/// messages + +#[allow(dead_code)] +mod util; + +use crate::util::event::{Event, Events}; +use std::{error::Error, io}; +use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; +use tui::{ + backend::TermionBackend, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, Borders, List, ListItem, Paragraph}, + Terminal, +}; +use unicode_width::UnicodeWidthStr; + +enum InputMode { + Normal, + Editing, +} + +/// App holds the state of the application +struct App { + /// Current value of the input box + input: String, + /// Current input mode + input_mode: InputMode, + /// History of recorded messages + messages: Vec, +} + +impl Default for App { + fn default() -> App { + App { + input: String::new(), + input_mode: InputMode::Normal, + messages: Vec::new(), + } + } +} + +fn main() -> Result<(), Box> { + // Terminal initialization + let stdout = io::stdout().into_raw_mode()?; + let stdout = MouseTerminal::from(stdout); + let stdout = AlternateScreen::from(stdout); + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Setup event handlers + let mut events = Events::new(); + + // Create default app state + let mut app = App::default(); + + loop { + // Draw UI + terminal.draw(|f| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints( + [ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Min(1), + ] + .as_ref(), + ) + .split(f.size()); + + let (msg, style) = match app.input_mode { + InputMode::Normal => ( + vec![ + Span::raw("Press "), + Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to exit, "), + Span::styled("e", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to start editing."), + ], + Style::default().add_modifier(Modifier::RAPID_BLINK), + ), + InputMode::Editing => ( + vec![ + Span::raw("Press "), + Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to stop editing, "), + Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to record the message"), + ], + Style::default(), + ), + }; + let mut text = Text::from(Spans::from(msg)); + text.patch_style(style); + let help_message = Paragraph::new(text); + f.render_widget(help_message, chunks[0]); + + let input = Paragraph::new(app.input.as_ref()) + .style(match app.input_mode { + InputMode::Normal => Style::default(), + InputMode::Editing => Style::default().fg(Color::Yellow), + }) + .block(Block::default().borders(Borders::ALL).title("Input")); + f.render_widget(input, chunks[1]); + match app.input_mode { + InputMode::Normal => + // Hide the cursor. `Frame` does this by default, so we don't need to do anything here + {} + + InputMode::Editing => { + // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering + f.set_cursor( + // Put cursor past the end of the input text + chunks[1].x + app.input.width() as u16 + 1, + // Move one line down, from the border to the input line + chunks[1].y + 1, + ) + } + } + + let messages: Vec = app + .messages + .iter() + .enumerate() + .map(|(i, m)| { + let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))]; + ListItem::new(content) + }) + .collect(); + let messages = + List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages")); + f.render_widget(messages, chunks[2]); + })?; + + // Handle input + if let Event::Input(input) = events.next()? { + match app.input_mode { + InputMode::Normal => match input { + Key::Char('e') => { + app.input_mode = InputMode::Editing; + events.disable_exit_key(); + } + Key::Char('q') => { + break; + } + _ => {} + }, + InputMode::Editing => match input { + Key::Char('\n') => { + app.messages.push(app.input.drain(..).collect()); + } + Key::Char(c) => { + app.input.push(c); + } + Key::Backspace => { + app.input.pop(); + } + Key::Esc => { + app.input_mode = InputMode::Normal; + events.enable_exit_key(); + } + _ => {} + }, + } + } + } + Ok(()) +} diff --git a/src/util/event.rs b/src/util/event.rs new file mode 100644 index 0000000..fdcb282 --- /dev/null +++ b/src/util/event.rs @@ -0,0 +1,120 @@ +use std::io; +use std::sync::mpsc; +use std::sync::mpsc::TryIter; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use std::thread; +use std::time::Duration; + +use termion::event::Key; +use termion::input::TermRead; + +pub enum Event { + Input(I), + Tick, + MacroEnd, +} + +/// A small event handler that wrap termion input and tick events. Each event +/// type is handled in its own thread and returned to a common `Receiver` +#[allow(dead_code)] +pub struct Events { + rx: mpsc::Receiver>, + tx: mpsc::Sender>, + input_handle: thread::JoinHandle<()>, + ignore_exit_key: Arc, + tick_handle: thread::JoinHandle<()>, +} + +#[derive(Debug, Clone, Copy)] +pub struct Config { + pub exit_key: Key, + pub tick_rate: Duration, +} + +impl Default for Config { + fn default() -> Config { + Config { + exit_key: Key::Char('q'), + tick_rate: Duration::from_millis(1000), + } + } +} + +impl Events { + pub fn new() -> Events { + Events::with_config(Config::default()) + } + + pub fn with_config(config: Config) -> Events { + let (tx, rx) = mpsc::channel(); + let mac_tx = tx.clone(); + let ignore_exit_key = Arc::new(AtomicBool::new(true)); + let input_handle = { + let tx = tx.clone(); + let ignore_exit_key = ignore_exit_key.clone(); + thread::spawn(move || { + let stdin = io::stdin(); + for evt in stdin.keys() { + if let Ok(key) = evt { + if let Err(err) = tx.send(Event::Input(key)) { + eprintln!("{}", err); + return; + } + if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key { + return; + } + } + } + }) + }; + let tick_handle = { + thread::spawn(move || loop { + if tx.send(Event::Tick).is_err() { + break; + } + thread::sleep(config.tick_rate); + }) + }; + Events { + rx, + tx: mac_tx, + ignore_exit_key, + input_handle, + tick_handle, + } + } + + pub fn fill_event_buf(&self, mac: &str) { + for c in mac.chars() { + // TODO: Catch errors + if let Err(_) = self.tx.send(Event::Input(Key::Char(c))) { + //return; + } + } + //self.tx.send(Event::MacroEnd); + } + + pub fn next(&self) -> Result, mpsc::RecvError> { + self.rx.recv() + } + + pub fn try_next(&self) -> Result, mpsc::TryRecvError> { + self.rx.try_recv() + } + pub fn try_iter(&self) -> TryIter> { + self.rx.try_iter() + } + + #[allow(dead_code)] + pub fn disable_exit_key(&mut self) { + self.ignore_exit_key.store(true, Ordering::Relaxed); + } + + #[allow(dead_code)] + pub fn enable_exit_key(&mut self) { + self.ignore_exit_key.store(false, Ordering::Relaxed); + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..53f1126 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1 @@ +pub mod event;