Smush all history into one commit

This commit is contained in:
Austen Adler 2021-04-24 13:35:39 -04:00
commit c1d54f8b5e
11 changed files with 1332 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

88
Cargo.lock generated Normal file
View File

@ -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"

11
Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "rpn_rs"
version = "0.1.0"
authors = ["Austen Adler <agadler@austenadler.com>"]
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"

335
src/calc.rs Normal file
View File

@ -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<f64>,
macros: CalculatorMacros<'a>,
constants: CalculatorConstants<'a>,
registers: CalculatorRegisters,
undo_buf: Vec<CalculatorStateChange>,
redo_buf: Vec<CalculatorStateChange>,
}
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: "<cr>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<f64, CalculatorError> {
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<f64> {
&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<CalculatorStateChange, CalculatorError> {
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<CalculatorStateChange, CalculatorError> {
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<f64, CalculatorError> {
match self.stack.get(idx) {
None => Err(CalculatorError::NotEnoughStackEntries),
Some(r) => Ok(*r),
}
}
}

23
src/calc/constants.rs Normal file
View File

@ -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<char, CalculatorConstant<'a>>;
pub type CalculatorConstantsIter<'a> = Iter<'a, char, CalculatorConstant<'a>>;
pub type CalculatorMacros<'a> = HashMap<char, CalculatorMacro<'a>>;
pub type CalculatorMacrosIter<'a> = Iter<'a, char, CalculatorMacro<'a>>;
pub type CalculatorRegisters = HashMap<char, f64>;
pub type CalculatorRegistersIter<'a> = Iter<'a, char, f64>;

26
src/calc/errors.rs Normal file
View File

@ -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"),
}
}
}

102
src/calc/operations.rs Normal file
View File

@ -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<CalculatorOperation, ()> {
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,
}
}
}

444
src/main.rs Normal file
View File

@ -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<String>,
state: AppState,
current_macro: Option<char>,
}
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<dyn Error>> {
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<ListItem> = 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\
<ret> => 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::<f64>() {
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<T: std::io::Write>(c: ClippyRectangle, f: &mut Frame<TermionBackend<T>>) {
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::<f64>() {
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::<f64>() {
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()),
}
}
_ => {}
},
}
}

181
src/user_input.rs Normal file
View File

@ -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<String>,
}
impl Default for App {
fn default() -> App {
App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
}
}
}
fn main() -> Result<(), Box<dyn Error>> {
// 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<ListItem> = 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(())
}

120
src/util/event.rs Normal file
View File

@ -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<I> {
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<Event<Key>>,
tx: mpsc::Sender<Event<Key>>,
input_handle: thread::JoinHandle<()>,
ignore_exit_key: Arc<AtomicBool>,
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<Event<Key>, mpsc::RecvError> {
self.rx.recv()
}
pub fn try_next(&self) -> Result<Event<Key>, mpsc::TryRecvError> {
self.rx.try_recv()
}
pub fn try_iter(&self) -> TryIter<Event<Key>> {
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);
}
}

1
src/util/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod event;