diff --git a/src/calc.rs b/src/calc.rs index 3208a2b..2e7e931 100644 --- a/src/calc.rs +++ b/src/calc.rs @@ -2,16 +2,21 @@ pub mod constants; pub mod errors; pub mod operations; +use confy::{load, store}; use constants::{ - CalculatorAngleMode, CalculatorConstant, CalculatorConstants, CalculatorConstantsIter, - CalculatorDisplayMode, CalculatorMacro, CalculatorMacros, CalculatorMacrosIter, - CalculatorRegisters, CalculatorRegistersIter, CalculatorState, RegisterState, + CalculatorAngleMode, CalculatorConstants, CalculatorConstantsIter, CalculatorDisplayMode, + CalculatorMacros, CalculatorMacrosIter, CalculatorRegisters, CalculatorRegistersIter, + CalculatorSettings, CalculatorState, RegisterState, }; use errors::{CalculatorError, CalculatorResult}; use operations::{CalculatorOperation, CalculatorStateChange, MacroState, OpArgs}; +use serde::ser::{SerializeStruct, Serializer}; use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; use std::collections::{HashSet, VecDeque}; +const APP_NAME: &str = "rpn_rs"; + #[derive(PartialEq, Debug, Serialize, Deserialize)] enum HistoryMode { One, @@ -24,122 +29,133 @@ impl CalculatorStateChange { } } -#[derive(Serialize, Deserialize)] +#[derive(Deserialize)] pub struct Calculator { + #[serde(skip)] l: String, stack: VecDeque, + #[serde(serialize_with = "ordered_char_map")] macros: CalculatorMacros, + #[serde(skip)] active_macros: HashSet, constants: CalculatorConstants, registers: CalculatorRegisters, + #[serde(skip)] undo_buf: Vec, + #[serde(skip)] redo_buf: Vec, + #[serde(skip)] state: CalculatorState, + // TODO: + #[serde(skip)] angle_mode: CalculatorAngleMode, + // TODO: + #[serde(skip)] display_mode: CalculatorDisplayMode, } +fn ordered_char_map(value: &HashMap, serializer: S) -> Result +where + T: Serialize, + S: Serializer, +{ + // let ordered: HashMap<_, _> = value + // .iter() + // .map(|(key, t)| (String::from(*key), t.clone())) + // .collect(); + let ordered: BTreeMap<_, _> = value + .iter() + .map(|(key, t)| (String::from(*key), t.clone())) + .collect(); + ordered.serialize(serializer) +} + impl Default for Calculator { - fn default() -> Calculator { - Calculator { + fn default() -> Self { + Calculator::with_settings(CalculatorSettings::default()) + .expect("Default calculator config is corrupt! Cannot start.") + } +} + +impl Serialize for Calculator { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut s = serializer.serialize_struct("Calculator", 2)?; + s.serialize_field("l", &self.l)?; + s.serialize_field("state", &self.state)?; + // s.serialize_field("k", &self.registers)?; // map key was not a string + //s.serialize_field("t", &self.undo_buf)?; // unsupported Rust type + s.end() + } +} + +impl Calculator { + pub fn with_settings(settings: CalculatorSettings) -> CalculatorResult { + let angle_mode = settings.angle_mode; + let display_mode = settings.display_mode; + Ok(Calculator { l: String::new(), - stack: vec![1000.0, 1200.32].into_iter().collect(), state: CalculatorState::Normal, undo_buf: vec![], redo_buf: vec![], active_macros: HashSet::new(), registers: CalculatorRegisters::new(), - macros: [ - ( - 'e', - CalculatorMacro { - help: String::from("Empty"), - value: String::from(""), - }, - ), - ( - '1', - CalculatorMacro { - help: String::from("Push 1"), - value: String::from("1 "), - }, - ), - ( - '0', - CalculatorMacro { - help: String::from("Push 0"), - value: String::from("0 "), - }, - ), - ( - 't', - CalculatorMacro { - help: String::from("Test"), - value: String::from("m1m0\\m1m1\\\\"), - }, - ), - ( - 'm', - CalculatorMacro { - help: String::from("64?>64%"), - value: String::from(" 64?>64%"), - }, - ), - ( - 'u', - CalculatorMacro { - help: String::from("Quadratic Formula"), - value: String::from( - "RcRbRarbnrb2 ^4 rarc**-v+2 ra*/rbnrb2^4 rarc**-v-2 ra*/", - ), - }, - ), - ( - 's', - CalculatorMacro { - help: String::from("Sample data"), - value: String::from("\\\\2 5 3n"), - }, - ), - ] - .iter() - .cloned() - .collect(), - constants: [ - ( - 't', - CalculatorConstant { - help: String::from("Tau (2pi)"), - value: std::f64::consts::TAU, - }, - ), - ( - 'e', - CalculatorConstant { - help: String::from("Euler's Number e"), - value: std::f64::consts::E, - }, - ), - ( - 'p', - CalculatorConstant { - help: String::from("Pi"), - value: std::f64::consts::PI, - }, - ), - ] - .iter() - .cloned() - .collect(), - angle_mode: CalculatorAngleMode::Degrees, - display_mode: CalculatorDisplayMode::Default(None), - } + stack: settings.stack.into(), + macros: settings + .macros + .into_iter() + .map(|(key, mac)| { + Ok(( + key.chars() + .next() + .ok_or_else(|| CalculatorError::LoadError(None))?, + mac, + )) + }) + .collect::>()?, + constants: settings + .constants + .into_iter() + .map(|(key, constant)| { + Ok(( + key.chars() + .next() + .ok_or_else(|| CalculatorError::LoadError(None))?, + constant, + )) + }) + .collect::>()?, + angle_mode, + display_mode, + }) + } + + pub fn load_config() -> CalculatorResult { + let settings = load(APP_NAME).map_err(|e| CalculatorError::LoadError(Some(e)))?; + Calculator::with_settings(settings) + } + pub fn save_config(&self) -> CalculatorResult<()> { + let settings = CalculatorSettings { + angle_mode: self.angle_mode.clone(), + display_mode: self.display_mode.clone(), + stack: self.stack.iter().map(|f| *f).collect(), + macros: self + .macros + .iter() + .map(|(key, mac)| (String::from(*key), mac.clone())) + .collect(), + constants: self + .constants + .iter() + .map(|(key, constant)| (String::from(*key), constant.clone())) + .collect(), + }; + store(APP_NAME, settings).map_err(|e| CalculatorError::SaveError(Some(e))) } -} -impl Calculator { pub fn take_input(&mut self, c: char) -> CalculatorResult<()> { - //for c in input.chars() { match &self.state { CalculatorState::Normal => match c { c @ '0'..='9' | c @ '.' | c @ 'e' => self.entry(c), diff --git a/src/calc/constants.rs b/src/calc/constants.rs index 1553be1..8d1f260 100644 --- a/src/calc/constants.rs +++ b/src/calc/constants.rs @@ -1,14 +1,15 @@ use serde::{Deserialize, Serialize}; use std::collections::hash_map::Iter; use std::collections::HashMap; +use std::fmt; -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub enum RegisterState { Save, Load, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub enum CalculatorState { Normal, WaitingForConstant, @@ -17,13 +18,21 @@ pub enum CalculatorState { WaitingForSetting, } +impl Default for CalculatorState { + fn default() -> Self { + CalculatorState::Normal + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "constant")] pub struct CalculatorConstant { pub help: String, pub value: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "macro")] pub struct CalculatorMacro { pub help: String, pub value: String, @@ -38,16 +47,154 @@ pub type CalculatorMacrosIter<'a> = Iter<'a, char, CalculatorMacro>; pub type CalculatorRegisters = HashMap; pub type CalculatorRegistersIter<'a> = Iter<'a, char, f64>; -#[derive(Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "angle_mode")] pub enum CalculatorAngleMode { Degrees, Radians, Grads, } -#[derive(Serialize, Deserialize)] +impl fmt::Display for CalculatorAngleMode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CalculatorAngleMode::Degrees => write!(f, "DEG"), + CalculatorAngleMode::Radians => write!(f, "RAD"), + CalculatorAngleMode::Grads => write!(f, "GRD"), + } + } +} + +impl Default for CalculatorAngleMode { + fn default() -> Self { + CalculatorAngleMode::Degrees + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "display_mode")] pub enum CalculatorDisplayMode { Default(Option), Scientific(usize), Engineering(usize), } + +impl fmt::Display for CalculatorDisplayMode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CalculatorDisplayMode::Default(None) => write!(f, "DEF"), + CalculatorDisplayMode::Default(Some(c)) => write!(f, "DEF({})", c), + CalculatorDisplayMode::Scientific(p) => write!(f, "SCI({})", p), + CalculatorDisplayMode::Engineering(p) => write!(f, "ENG({})", p), + } + } +} + +impl Default for CalculatorDisplayMode { + fn default() -> Self { + CalculatorDisplayMode::Default(None) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CalculatorSettings { + // Order matters here as macros/constants cannot come first + pub stack: Vec, + #[serde(skip)] + pub display_mode: CalculatorDisplayMode, + pub angle_mode: CalculatorAngleMode, + pub macros: HashMap, + pub constants: HashMap, +} + +impl Default for CalculatorSettings { + fn default() -> CalculatorSettings { + CalculatorSettings { + stack: vec![1.0, 2.0].into_iter().collect(), + macros: [ + ( + String::from('e'), + CalculatorMacro { + help: String::from("Empty"), + value: String::from(""), + }, + ), + ( + String::from('1'), + CalculatorMacro { + help: String::from("Push 1"), + value: String::from("1 "), + }, + ), + ( + String::from('0'), + CalculatorMacro { + help: String::from("Push 0"), + value: String::from("0 "), + }, + ), + ( + String::from('t'), + CalculatorMacro { + help: String::from("Test"), + value: String::from("m1m0\\m1m1\\\\"), + }, + ), + ( + String::from('m'), + CalculatorMacro { + help: String::from("64?>64%"), + value: String::from(" 64?>64%"), + }, + ), + ( + String::from('u'), + CalculatorMacro { + help: String::from("Quadratic Formula"), + value: String::from( + "RcRbRarbnrb2 ^4 rarc**-v+2 ra*/rbnrb2^4 rarc**-v-2 ra*/", + ), + }, + ), + ( + String::from('s'), + CalculatorMacro { + help: String::from("Sample data"), + value: String::from("\\\\2 5 3n"), + }, + ), + ] + .iter() + .cloned() + .collect(), + constants: [ + ( + String::from('t'), + CalculatorConstant { + help: String::from("Tau (2pi)"), + value: std::f64::consts::TAU, + }, + ), + ( + String::from('e'), + CalculatorConstant { + help: String::from("Euler's Number e"), + value: std::f64::consts::E, + }, + ), + ( + String::from('p'), + CalculatorConstant { + help: String::from("Pi"), + value: std::f64::consts::PI, + }, + ), + ] + .iter() + .cloned() + .collect(), + angle_mode: CalculatorAngleMode::Degrees, + display_mode: CalculatorDisplayMode::Default(None), + } + } +} diff --git a/src/calc/errors.rs b/src/calc/errors.rs index 2006e7d..9f537c2 100644 --- a/src/calc/errors.rs +++ b/src/calc/errors.rs @@ -1,7 +1,9 @@ +use confy::ConfyError; use std::fmt; pub type CalculatorResult = Result; +#[derive(Debug)] pub enum CalculatorError { ArithmeticError, NotEnoughStackEntries, @@ -15,6 +17,8 @@ pub enum CalculatorError { RecursiveMacro(char), ParseError, PrecisionTooHigh, + SaveError(Option), + LoadError(Option), } impl fmt::Display for CalculatorError { @@ -34,6 +38,10 @@ impl fmt::Display for CalculatorError { CalculatorError::RecursiveMacro(c) => write!(f, "Recursive macro '{}'", c), CalculatorError::ParseError => write!(f, "Parse error"), CalculatorError::PrecisionTooHigh => write!(f, "Precision too high"), + CalculatorError::SaveError(None) => write!(f, "Could not save"), + CalculatorError::SaveError(Some(e)) => write!(f, "Could not save: {}", e), + CalculatorError::LoadError(None) => write!(f, "Could not load"), + CalculatorError::LoadError(Some(e)) => write!(f, "Could not load: {}", e), } } } diff --git a/src/main.rs b/src/main.rs index d00be5a..24c6017 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ mod calc; mod util; -use calc::constants::{CalculatorAngleMode, CalculatorDisplayMode, CalculatorState, RegisterState}; +use calc::constants::{CalculatorDisplayMode, CalculatorState, RegisterState}; use calc::errors::CalculatorResult; use calc::Calculator; use std::cmp; @@ -41,9 +41,13 @@ struct App { impl Default for App { fn default() -> App { + let (calculator, error_msg) = match Calculator::load_config() { + Ok(c) => (c, None), + Err(e) => (Calculator::default(), Some(format!("{}", e))), + }; App { - calculator: Calculator::default(), - error_msg: None, + calculator, + error_msg, state: AppState::Calculator, current_macro: None, } @@ -81,20 +85,6 @@ fn main() -> Result<(), Box> { Span::styled(e, Style::default().add_modifier(Modifier::RAPID_BLINK)), ], (None, AppState::Calculator) => { - // TODO: There has to be a better way than making strings each time - let display_mode_str = match app.calculator.get_display_mode() { - CalculatorDisplayMode::Default(None) => String::from("DEF"), - CalculatorDisplayMode::Default(Some(c)) => format!("DEF({})", c), - CalculatorDisplayMode::Scientific(p) => format!("SCI({})", p), - CalculatorDisplayMode::Engineering(p) => format!("ENG({})", p), - }; - - let angle_mode_str = match app.calculator.get_angle_mode() { - CalculatorAngleMode::Degrees => String::from("DEG"), - CalculatorAngleMode::Radians => String::from("RAD"), - CalculatorAngleMode::Grads => String::from("GRD"), - }; - vec![ Span::raw("Press "), Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), @@ -102,7 +92,8 @@ fn main() -> Result<(), Box> { Span::styled("h", Style::default().add_modifier(Modifier::BOLD)), Span::raw(format!( " for help - [{}] [{}]", - display_mode_str, angle_mode_str + app.calculator.get_display_mode(), + app.calculator.get_angle_mode() )), ] } @@ -293,6 +284,12 @@ fn handle_key(app: &mut App, events: &Events, key: Key) -> CalculatorResult { return Ok(true); } + Key::Ctrl('s') => { + app.calculator.save_config()?; + } + Key::Ctrl('l') => { + app.calculator = Calculator::load_config()?; + } Key::Char('h') => { app.state = AppState::Help; }