diff --git a/bash_complitions b/bash_complitions new file mode 100644 index 0000000..c419a10 --- /dev/null +++ b/bash_complitions @@ -0,0 +1,10 @@ +# This is only WIP, idk how to make it only take +# 1 argument yet +_complete() { + local cur="${COMP_WORDS[COMP_CWORD]}" + + # Only suggest *.csv and *.neoscim + COMPREPLY=($(compgen -f -X '!*.@(csv|neoscim)' -- "$cur")) +} + +complete -F _complete sc diff --git a/example.gnuplot b/example.gnuplot new file mode 100644 index 0000000..694716b --- /dev/null +++ b/example.gnuplot @@ -0,0 +1,17 @@ +datafile = 'data.csv' +set datafile separator ',' + +set title 'Probably the filename' +set key autotitle columnhead +set xlabel "x" +set ylabel "y" + +set style line 1 linewidth 2 linecolor 1 pointtype 7 pointsize 1.5 + +set autoscale +set grid + +set term png size 1280, 720 +set output 'output.png' +plot datafile using 1:2 with linespoints linestyle 1 +replot diff --git a/src/app/app.rs b/src/app/app.rs index 92b0d8b..9a0c245 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -1,5 +1,9 @@ use std::{ - cmp::{max, min}, collections::HashMap, fs, io, path::PathBuf, time::SystemTime + cmp::{max, min}, + collections::HashMap, + fs, io, + path::PathBuf, + time::SystemTime, }; use ratatui::{ @@ -315,6 +319,21 @@ impl App { fn handle_events(&mut self) -> io::Result<()> { match &mut self.mode { + Mode::VisualCmd(pos, chord) => match event::read()? { + event::Event::Key(key) => match key.code { + event::KeyCode::Esc => self.mode = Mode::Visual(*pos), + event::KeyCode::Backspace => chord.backspace(), + event::KeyCode::Char(c) => chord.add_char(c), + event::KeyCode::Enter => { + // tmp is to get around reference issues. + let tmp = pos.clone(); + Mode::process_cmd(self); + self.mode = Mode::Visual(tmp) + } + _ => {} + }, + _ => {} + }, Mode::Chord(chord) => match event::read()? { event::Event::Key(key) => match key.code { event::KeyCode::Esc => self.mode = Mode::Normal, @@ -384,20 +403,13 @@ impl App { } Mode::Command(editor) => match event::read()? { event::Event::Key(key) => match key.code { - event::KeyCode::Esc => { - // just cancel the operation - self.mode = Mode::Normal; - } + event::KeyCode::Esc => self.mode = Mode::Normal, + event::KeyCode::Backspace => editor.backspace(), + event::KeyCode::Char(c) => editor.add_char(c), event::KeyCode::Enter => { Mode::process_cmd(self); self.mode = Mode::Normal; } - event::KeyCode::Backspace => { - editor.backspace(); - } - event::KeyCode::Char(c) => { - editor.add_char(c); - } _ => {} }, _ => {} diff --git a/src/app/logic/calc.rs b/src/app/logic/calc.rs index dd8b42e..c1616b0 100644 --- a/src/app/logic/calc.rs +++ b/src/app/logic/calc.rs @@ -11,7 +11,7 @@ use crate::app::{ logic::{ cell::{CSV_DELIMITER, CellType}, ctx, - }, + }, mode::Mode, }; #[cfg(test)] @@ -110,21 +110,28 @@ impl Grid { for x in 0..=mx { let cell = &self.cells[x][y]; + // newline after the cell, because it's end of line. + // else, just put a comma after the cell. + let is_last = x==mx; + let delim = if is_last { + '\n' + } else { + CSV_DELIMITER + }; + let data = if let Some(cell) = cell { if let Ok(val) = self.evaluate(&cell.to_string()) && resolve_values { - val.to_string() + format!("{}{}", val.to_string(), delim) } else { - cell.escaped_csv_string() + format!("{}{}", cell.escaped_csv_string(), delim) } } else { - CSV_DELIMITER.to_string() + delim.to_string() }; - write!(f, "{data}")?; } - write!(f, "\n")?; } f.flush()?; @@ -459,6 +466,92 @@ impl Default for Grid { } } +#[test] +fn saving_csv() { + // setup grid + // This should be 1..10 in the A column, then + // 1^2..10^2 in the B column. + let mut app = App::new(); + app.grid.set_cell("A0", 1.); + app.grid.set_cell("B0", "=A0^2".to_string()); + + app.grid.set_cell("A1", "=A0+A$0".to_string()); + app.grid.set_cell("B1", "=A1^2".to_string()); + + app.grid.mv_cursor_to(0, 1); + app.mode = Mode::Visual(app.grid.cursor()); + Mode::process_key(&mut app, 'l'); + Mode::process_key(&mut app, 'y'); + app.mode = Mode::Normal; + app.grid.mv_cursor_to(0, 2); + for _ in 0..10 { + Mode::process_key(&mut app, 'p'); + } + // setup done + + // insure that the cells are there + let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set"); + let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate"); + assert_eq!(res, 11.0); + assert_eq!(cell.escaped_csv_string(), "=A9+A$0"); + let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set"); + let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate"); + assert_eq!(res, 121.0); + assert_eq!(cell.escaped_csv_string(), "=A10^2"); + + // set saving the file + let filename = "/tmp/file.csv"; + app.grid.save_to(filename).expect("This will only work on linux systems"); + let mut file = fs::OpenOptions::new().read(true).open(filename).expect("Just wrote the file"); + let mut buf = String::new(); + file.read_to_string(&mut buf).expect("Just opened the file"); + let line = buf.lines().skip(10).next(); + assert_eq!(line, Some("11,121")); +} + +#[test] +fn saving_neoscim() { + // setup grid + // This should be 1..10 in the A column, then + // 1^2..10^2 in the B column. + let mut app = App::new(); + app.grid.set_cell("A0", 1.); + app.grid.set_cell("B0", "=A0^2".to_string()); + + app.grid.set_cell("A1", "=A0+A$0".to_string()); + app.grid.set_cell("B1", "=A1^2".to_string()); + + app.grid.mv_cursor_to(0, 1); + app.mode = Mode::Visual(app.grid.cursor()); + Mode::process_key(&mut app, 'l'); + Mode::process_key(&mut app, 'y'); + app.mode = Mode::Normal; + app.grid.mv_cursor_to(0, 2); + for _ in 0..10 { + Mode::process_key(&mut app, 'p'); + } + // setup done + + // insure that the cells are there + let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set"); + let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate"); + assert_eq!(res, 11.0); + assert_eq!(cell.escaped_csv_string(), "=A9+A$0"); + let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set"); + let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate"); + assert_eq!(res, 121.0); + assert_eq!(cell.escaped_csv_string(), "=A10^2"); + + // set saving the file + let filename= "/tmp/file.neoscim"; + app.grid.save_to(filename).expect("This will only work on linux systems"); + let mut file = fs::OpenOptions::new().read(true).open(filename).expect("Just wrote the file"); + let mut buf = String::new(); + file.read_to_string(&mut buf).expect("Just opened the file"); + let line = buf.lines().skip(10).next(); + assert_eq!(line, Some("=A9+A$0,=A10^2")); +} + // Do cells hold strings? #[test] fn cell_strings() { diff --git a/src/app/logic/cell.rs b/src/app/logic/cell.rs index 8543085..a193550 100644 --- a/src/app/logic/cell.rs +++ b/src/app/logic/cell.rs @@ -38,9 +38,9 @@ impl CellType { // escape string of it has a comma if display.contains(CSV_DELIMITER) { - format!("\"{display}\"{CSV_DELIMITER}") + format!("\"{display}\"") } else { - format!("{display}{CSV_DELIMITER}") + display } } diff --git a/src/app/mode.rs b/src/app/mode.rs index a516003..025b8fe 100644 --- a/src/app/mode.rs +++ b/src/app/mode.rs @@ -23,6 +23,7 @@ pub enum Mode { Normal, Command(Chord), Visual((usize, usize)), + VisualCmd((usize, usize), Chord), } impl Display for Mode { @@ -33,6 +34,7 @@ impl Display for Mode { Mode::Chord(_) => write!(f, "CHORD"), Mode::Command(_) => write!(f, "COMMAND"), Mode::Visual(_) => write!(f, "VISUAL"), + Mode::VisualCmd(_, _) => write!(f, "V-CMD"), } } } @@ -43,6 +45,7 @@ impl Mode { // Where you are typing Mode::Insert(_) => Style::new().fg(Color::White).bg(Color::Blue), Mode::Command(_) => Style::new().fg(Color::Black).bg(Color::Magenta), + Mode::VisualCmd(_, _) => Style::new().fg(Color::Black).bg(Color::Yellow), Mode::Chord(_) => Style::new().fg(Color::Black).bg(Color::LightBlue), // Movement-based modes Mode::Visual(_) => Style::new().fg(Color::Yellow), @@ -143,6 +146,17 @@ impl Mode { _ => {} } } + if let Mode::VisualCmd(pos, editor ) = &mut app.mode { + let cmd = &editor.as_string()[1..]; + let args = cmd.split_ascii_whitespace().collect::>(); + if args.is_empty() { + return; + } + match args[0] { + "foo" => {} + _ => {} + } + } } pub fn process_key(app: &mut App, key: char) { @@ -211,7 +225,13 @@ impl Mode { app.grid.insert_row_above(app.grid.cursor()); } 'v' => app.mode = Mode::Visual(app.grid.cursor()), - ':' => app.mode = Mode::Command(Chord::new(':')), + ':' => { + if let Self::Visual(pos) = app.mode { + app.mode = Mode::VisualCmd(pos, Chord::new(':')); + } else { + app.mode = Mode::Command(Chord::new(':')) + } + } 'p' => { app.clipboard.paste(&mut app.grid, true); app.grid.apply_momentum(app.clipboard.momentum()); @@ -323,15 +343,16 @@ impl Mode { } } } - // IDK why it works but it does. Keystrokes are process somewhere else? + // Keys are process in the handle_event method in App for these Mode::Insert(_chord) => {} Mode::Command(_chord) => {} + Mode::VisualCmd(_pos, _chord ) => {} } } pub fn chars_to_display(&self, cell: &Option) -> u16 { let len = match &self { - Mode::Insert(edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(), + Mode::Insert(edit) | Mode::VisualCmd(_, edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(), Mode::Normal => { let len = cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default(); len @@ -360,6 +381,7 @@ impl Mode { area, ), Mode::Visual(_) => {} + Mode::VisualCmd(_, editor) => f.render_widget(editor, area), } } }