summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock23
-rw-r--r--Cargo.toml7
-rw-r--r--src/main.rs336
4 files changed, 367 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b83d222
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..0c293ce
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,23 @@
+[root]
+name = "rcalc"
+version = "0.1.0"
+dependencies = [
+ "termion 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "termion"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[metadata]
+"checksum libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)" = "e7eb6b826bfc1fdea7935d46556250d1799b7fe2d9f7951071f4291710665e3e"
+"checksum termion 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8b41865823fb8c7873ff869893219b3188e7fcd66c10effb97f2b2f63ea98681"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..a7c86ea
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "rcalc"
+version = "0.1.0"
+authors = ["tomsmeding <tom.smeding@gmail.com>"]
+
+[dependencies]
+termion = "1"
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..90ba496
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,336 @@
+extern crate termion;
+
+use termion::event::Key;
+use termion::input::TermRead;
+use termion::raw::IntoRawMode;
+use std::io::Write;
+use std::io;
+// use std::fs;
+use std::collections::HashMap;
+
+type Entry = f64;
+
+const PROMPT_STR: &str = "> ";
+
+const BEEP_STR: &str = "\x07";
+
+fn min<T: Ord>(a: T, b: T) -> T {
+ if b < a {b} else {a}
+}
+
+fn draw_entries(entries: &[Entry], up: isize) {
+ assert!(up < 1<<16);
+ if up < 0 {
+ print!("{}", termion::cursor::Down((-up) as u16));
+ } else if up > 0 {
+ print!("{}", termion::cursor::Up(up as u16));
+ }
+ print!("\r");
+ let mut first = true;
+ for entry in entries {
+ if first {first = false;}
+ else {print!("\n\r");}
+ print!("{}{}{}", " ".repeat(PROMPT_STR.len()), entry, termion::clear::UntilNewline);
+ }
+}
+
+fn valid_input_char(c: char) -> bool {
+ c.is_digit(10) || c == '.' || c == 'e'
+}
+
+type Callback = Box<(Fn(&mut Vec<Entry>) -> usize + 'static)>;
+
+fn mk_callback<F>(f: F) -> Callback
+ where F: Fn(&mut Vec<Entry>) -> usize + 'static {
+ Box::new(f) as Callback
+}
+
+fn stk_binop<F>(f: F) -> Callback
+ where F: Fn(Entry, Entry) -> Entry + 'static {
+ mk_callback(move |stk| {
+ let v2 = stk.pop().unwrap();
+ let v1 = stk.pop().unwrap();
+ stk.push(f(v1,v2));
+ 1
+ })
+}
+
+struct State<'a> {
+ stk: Vec<Entry>,
+ entries_drawn: usize,
+ term_height: usize,
+ input_buffer: String,
+ input_cursor: usize,
+ command_map: &'a HashMap<String, (usize, Callback)>,
+}
+
+fn handle_key(key: Key, st: &mut State) -> Option<bool> {
+ match key {
+ // Quitting
+ Key::Ctrl('c') | Key::Ctrl('d') => {
+ if st.input_buffer.len() > 0 {
+ print!("\n\r");
+ } else {
+ print!("\r{}", termion::clear::CurrentLine);
+ }
+ return Some(false);
+ }
+
+ // Moving around
+ Key::Ctrl('a') => {
+ print!("\r{}", termion::cursor::Right(PROMPT_STR.len() as u16));
+ st.input_cursor = 0;
+ }
+ Key::Ctrl('e') => {
+ print!("\r{}",
+ termion::cursor::Right((PROMPT_STR.len() + st.input_buffer.len()) as u16));
+ st.input_cursor = st.input_buffer.len();
+ }
+ Key::Left => {
+ if st.input_cursor > 0 {
+ print!("{}", termion::cursor::Left(1));
+ st.input_cursor -= 1;
+ } else {
+ return None;
+ }
+ }
+ Key::Right => {
+ if st.input_cursor < st.input_buffer.len() {
+ print!("{}", termion::cursor::Right(1));
+ st.input_cursor += 1;
+ } else {
+ return None;
+ }
+ }
+
+ // Editing
+ Key::Ctrl('u') => {
+ st.input_buffer.clear();
+ print!("\r{}{}", termion::clear::CurrentLine, PROMPT_STR);
+ st.input_cursor = 0;
+ }
+ Key::Backspace => {
+ if st.input_cursor == 0 {
+ return None;
+ }
+ st.input_cursor -= 1;
+ st.input_buffer.remove(st.input_cursor);
+ let rest = &st.input_buffer[st.input_cursor..];
+ print!("{}{}{}", termion::cursor::Left(1), rest,
+ termion::clear::UntilNewline);
+ if rest.len() > 0 {
+ print!("{}", termion::cursor::Left(rest.len() as u16));
+ }
+ }
+ Key::Delete => {
+ if st.input_buffer.len() == 0 {
+ if st.stk.len() == 0 {
+ return None;
+ }
+ if st.entries_drawn < st.stk.len() {
+ draw_entries(&st.stk[(st.stk.len() - st.entries_drawn - 1)..(st.stk.len() - 1)],
+ st.entries_drawn as isize);
+ st.input_buffer = match st.stk.last() {
+ None => String::new(),
+ Some(val) => val.to_string(),
+ };
+ print!("\n\r{}{}", PROMPT_STR, st.input_buffer);
+ st.input_cursor = st.input_buffer.len();
+ } else {
+ st.entries_drawn -= 1;
+ st.input_buffer = match st.stk.last() {
+ None => String::new(),
+ Some(val) => val.to_string(),
+ };
+ st.input_cursor = st.input_buffer.len();
+ print!("{}{}\r{}{}", termion::clear::CurrentLine,
+ termion::cursor::Up(1),
+ PROMPT_STR,
+ termion::cursor::Right(st.input_cursor as u16))
+ }
+ st.stk.pop();
+ } else if st.input_cursor == st.input_buffer.len() {
+ return None;
+ } else {
+ st.input_buffer.remove(st.input_cursor);
+ let rest = &st.input_buffer[st.input_cursor..];
+ print!("{}{}", rest, termion::clear::UntilNewline);
+ if rest.len() > 0 {
+ print!("{}", termion::cursor::Left(rest.len() as u16));
+ }
+ }
+ }
+
+ // Inputting
+ Key::Char(c) if valid_input_char(c) => {
+ st.input_buffer.insert(st.input_cursor, c);
+ let rest = &st.input_buffer[st.input_cursor..];
+ print!("{}", rest);
+ if rest.len() > 1 {
+ print!("{}", termion::cursor::Left((rest.len() - 1) as u16));
+ }
+ st.input_cursor += 1;
+ }
+
+ // Submitting
+ Key::Char('\n') | Key::Char('\r') => {
+ match st.input_buffer.parse::<Entry>() {
+ Ok(val) => {
+ st.stk.push(val);
+ print!("{}\r{}{}\n\r{}",
+ termion::clear::CurrentLine, " ".repeat(PROMPT_STR.len()),
+ val, PROMPT_STR);
+ st.input_buffer.clear();
+ st.input_cursor = 0;
+ if st.entries_drawn < st.term_height - 1 {
+ st.entries_drawn += 1;
+ }
+ }
+ Err(_) => return None,
+ }
+ }
+
+ // Command
+ Key::Char(c) | Key::Alt(c) => {
+ let mut delay_lf = 0;
+ if st.input_buffer.len() != 0 {
+ match st.input_buffer.parse::<Entry>() {
+ Ok(val) => {
+ st.stk.push(val);
+ print!("{}\r{}{}",
+ termion::clear::CurrentLine, " ".repeat(PROMPT_STR.len()),
+ val);
+ st.input_buffer.clear();
+ st.input_cursor = 0;
+ if st.entries_drawn < st.term_height - 1 {
+ st.entries_drawn += 1;
+ }
+ delay_lf = 1;
+ }
+ Err(_) => return None,
+ }
+ }
+
+ let name = String::from(c.to_string());
+ if let Some(&(nargs, ref f)) = st.command_map.get(&name) {
+ if st.stk.len() < nargs {
+ return None;
+ }
+ let prevlen = st.stk.len();
+ let npushed = f(&mut st.stk);
+ let sizeincr = st.stk.len() as isize - prevlen as isize;
+ assert!(sizeincr < 0 || npushed >= sizeincr as usize);
+ let mut print_newline = true;
+ if sizeincr >= 0 {
+ // Stack grew
+ let usizeincr = sizeincr as usize;
+ draw_entries(&st.stk[(st.stk.len() - npushed)..],
+ npushed as isize - sizeincr - delay_lf);
+ st.entries_drawn = min(st.entries_drawn + usizeincr,
+ st.term_height - 1);
+ } else if prevlen >= st.term_height && st.stk.len() < st.term_height {
+ // Didn't fit before, but fits now
+ draw_entries(&st.stk[..], st.stk.len() as isize);
+ st.entries_drawn = st.stk.len();
+ } else if prevlen < st.term_height {
+ // Fit before, and still fits
+ assert!(st.stk.len() < st.term_height);
+ draw_entries(&st.stk[(st.stk.len() - npushed)..],
+ (-sizeincr) + npushed as isize - delay_lf);
+ st.entries_drawn = st.stk.len();
+ if npushed == 0 {print_newline = false;}
+ } else {
+ // Didn't fit before, and still doesn't fit
+ assert!(prevlen >= st.term_height && st.stk.len() >= st.term_height);
+ if sizeincr + delay_lf == 0 {
+ let up = min(npushed, st.term_height - 1);
+ draw_entries(&st.stk[(st.stk.len() - up)..], up as isize);
+ } else {
+ draw_entries(&st.stk[(st.stk.len() - st.term_height + 1)..],
+ st.term_height as isize - 1);
+ }
+ st.entries_drawn = st.term_height - 1;
+ }
+ if print_newline {print!("\n\r");}
+ print!("{}{}", termion::clear::AfterCursor, PROMPT_STR);
+ } else {
+ return None;
+ }
+ }
+
+ // Otherwise
+ _ => return None,
+ }
+
+ Some(true)
+}
+
+fn main() {
+ let command_map = {
+ let mut h = HashMap::new();
+
+ h.insert(String::from("+"), (2, stk_binop(|a,b| a+b)));
+ h.insert(String::from("-"), (2, stk_binop(|a,b| a-b)));
+ h.insert(String::from("*"), (2, stk_binop(|a,b| a*b)));
+ h.insert(String::from("/"), (2, stk_binop(|a,b| a/b)));
+ h.insert(String::from("m"), (1, mk_callback(|stk| {
+ let v = stk.pop().unwrap();
+ stk.push(-v);
+ 1
+ })));
+ h.insert(String::from("s"), (2, mk_callback(|stk| {
+ let v = stk.pop().unwrap();
+ let len = stk.len();
+ stk.insert(len - 1, v);
+ 2
+ })));
+ h.insert(String::from("d"), (1, mk_callback(|stk| {
+ let v = stk[stk.len() - 1];
+ stk.push(v);
+ 2
+ })));
+ h.insert(String::from("D"), (1, mk_callback(|stk| {
+ stk.pop();
+ 0
+ })));
+
+ h
+ };
+
+ let mut st = State {
+ stk: Vec::<Entry>::new(),
+ entries_drawn: 0,
+ term_height: 0,
+ input_buffer: String::new(),
+ input_cursor: 0,
+ command_map: &command_map,
+ };
+
+ let stdin = io::stdin();
+ let mut stdout = io::stdout().into_raw_mode().unwrap();
+
+ print!("{}", PROMPT_STR);
+ stdout.flush().unwrap();
+
+ for key in stdin.keys() {
+ let new_term_height = termion::terminal_size().unwrap().1 as usize;
+ if new_term_height < st.term_height {
+ st.entries_drawn = min(new_term_height - 1, st.stk.len());
+ } else if new_term_height > st.term_height {
+ let extra_num = min(new_term_height - st.term_height, st.stk.len() - st.entries_drawn);
+ if extra_num > 0 {
+ let idx = st.stk.len() - st.entries_drawn - extra_num;
+ draw_entries(&st.stk[idx..], (st.stk.len() - st.entries_drawn) as isize);
+ st.entries_drawn += extra_num;
+ }
+ }
+ st.term_height = new_term_height;
+
+ match handle_key(key.unwrap(), &mut st) {
+ None => print!("{}", BEEP_STR),
+ Some(true) => {},
+ Some(false) => break,
+ }
+ stdout.flush().unwrap();
+ }
+}