diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.lock | 23 | ||||
-rw-r--r-- | Cargo.toml | 7 | ||||
-rw-r--r-- | src/main.rs | 336 |
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(); + } +} |