Compare commits
15 Commits
ef4429a38f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c2588a299 | |||
| 0c527bb3cb | |||
| 6ca21b407d | |||
| 1323d15333 | |||
| d69966bc01 | |||
| dae3b57f73 | |||
| ee1cac0522 | |||
| 712d75b256 | |||
| d5ccb9b8c6 | |||
| a03794e69f | |||
| 98215e42af | |||
| b8fd938120 | |||
| 3be92aea3c | |||
| 902af1311d | |||
| 9c44db0d92 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
/.vscode
|
/.vscode
|
||||||
/*.csv
|
/*.csv
|
||||||
/*.nscim
|
/*.nscim
|
||||||
|
/plot.png
|
||||||
29
README.md
29
README.md
@@ -3,22 +3,6 @@
|
|||||||
*New* Spreadsheet Calculator Improved
|
*New* Spreadsheet Calculator Improved
|
||||||
|
|
||||||
Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keybinds and not many features.
|
Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keybinds and not many features.
|
||||||
## Improvements from sc-im
|
|
||||||
|
|
||||||
* Cell type (string, function, value) are all "the same" and use polymorphism to detect what you are trying to do.
|
|
||||||
The logic is more or less:
|
|
||||||
```cpp
|
|
||||||
if value.can_be_a_number() {
|
|
||||||
return value.as_number()
|
|
||||||
} else if value.starts_with('=') {
|
|
||||||
return value.as_equation()
|
|
||||||
} else {
|
|
||||||
return value.as_string()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Keybinds are closer to vim keying (i/a start inserting into a cell)
|
|
||||||
|
|
||||||
|
|
||||||
## Keybinds
|
## Keybinds
|
||||||
|
|
||||||
@@ -141,6 +125,19 @@ if value.can_be_a_number() {
|
|||||||
|
|
||||||
> Any time a function takes a variable amount of inputs, a range can be specified: `avg(B:B)`.
|
> Any time a function takes a variable amount of inputs, a range can be specified: `avg(B:B)`.
|
||||||
|
|
||||||
|
## Improvements from sc-im
|
||||||
|
|
||||||
|
* Cell type (string, function, value) are all "the same" and use polymorphism to detect what you are trying to do.
|
||||||
|
If the cell can be a number (parsed into a float) it will be, if not, then if the string starts with "=", then treats it as an equation, otherwise, it's a string.
|
||||||
|
|
||||||
|
* Cell references move with copy / paste
|
||||||
|
* =A0: Will translate
|
||||||
|
* =$A0: Will translate Y only
|
||||||
|
* =$A0: Will translate X only
|
||||||
|
* =$A$0: No translation
|
||||||
|
|
||||||
|
* Keybinds are closer to vim keying (i/a start inserting into a cell)
|
||||||
|
|
||||||
## FAQ:
|
## FAQ:
|
||||||
|
|
||||||
* Every number is a float (or will end up as a float)
|
* Every number is a float (or will end up as a float)
|
||||||
|
|||||||
10
bash_complitions
Normal file
10
bash_complitions
Normal file
@@ -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|nscim)' -- "$cur"))
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -F _complete sc
|
||||||
152
src/app/app.rs
152
src/app/app.rs
@@ -1,13 +1,26 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cmp::{max, min}, collections::HashMap, io, path::PathBuf
|
cmp::{max, min},
|
||||||
|
collections::HashMap,
|
||||||
|
fs, io,
|
||||||
|
path::PathBuf,
|
||||||
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
DefaultTerminal, Frame, crossterm::event, layout::{self, Constraint, Layout, Rect}, prelude, style::{Color, Modifier, Style}, widgets::{Paragraph, Widget}
|
DefaultTerminal, Frame,
|
||||||
|
crossterm::event,
|
||||||
|
layout::{self, Constraint, Layout, Rect},
|
||||||
|
prelude,
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
widgets::{Paragraph, Widget},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
clipboard::Clipboard, error_msg::StatusMessage, logic::{calc::Grid, cell::CellType}, mode::Mode, screen::ScreenSpace
|
clipboard::Clipboard,
|
||||||
|
error_msg::StatusMessage,
|
||||||
|
logic::{calc::Grid, cell::CellType},
|
||||||
|
mode::Mode,
|
||||||
|
screen::ScreenSpace,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
@@ -15,6 +28,7 @@ pub struct App {
|
|||||||
pub grid: Grid,
|
pub grid: Grid,
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
pub file: Option<PathBuf>,
|
pub file: Option<PathBuf>,
|
||||||
|
file_modified_date: SystemTime,
|
||||||
pub msg: StatusMessage,
|
pub msg: StatusMessage,
|
||||||
pub vars: HashMap<String, String>,
|
pub vars: HashMap<String, String>,
|
||||||
pub screen: ScreenSpace,
|
pub screen: ScreenSpace,
|
||||||
@@ -28,7 +42,7 @@ impl Widget for &App {
|
|||||||
let (x_max, y_max) = self.screen.how_many_cells_fit_in(&area, &self.vars);
|
let (x_max, y_max) = self.screen.how_many_cells_fit_in(&area, &self.vars);
|
||||||
|
|
||||||
let is_selected = |x: usize, y: usize| -> bool {
|
let is_selected = |x: usize, y: usize| -> bool {
|
||||||
if let Mode::Visual((mut x1, mut y1)) = self.mode {
|
if let Mode::Visual((mut x1, mut y1)) | Mode::VisualCmd((mut x1, mut y1), _)= self.mode {
|
||||||
let (mut x2, mut y2) = self.grid.cursor();
|
let (mut x2, mut y2) = self.grid.cursor();
|
||||||
x1 += 1;
|
x1 += 1;
|
||||||
y1 += 1;
|
y1 += 1;
|
||||||
@@ -66,10 +80,6 @@ impl Widget for &App {
|
|||||||
y_idx = y as usize - 1 + self.screen.scroll_y();
|
y_idx = y as usize - 1 + self.screen.scroll_y();
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_selected(x.into(), y.into()) {
|
|
||||||
style = style.fg(Color::LightMagenta).bg(Color::Blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ORANGE1: Color = Color::Rgb(200, 160, 0);
|
const ORANGE1: Color = Color::Rgb(200, 160, 0);
|
||||||
const ORANGE2: Color = Color::Rgb(180, 130, 0);
|
const ORANGE2: Color = Color::Rgb(180, 130, 0);
|
||||||
|
|
||||||
@@ -115,7 +125,10 @@ impl Widget for &App {
|
|||||||
Some(cell) => {
|
Some(cell) => {
|
||||||
match cell {
|
match cell {
|
||||||
CellType::Number(c) => display = c.to_string(),
|
CellType::Number(c) => display = c.to_string(),
|
||||||
CellType::String(s) => display = s.to_owned(),
|
CellType::String(s) => {
|
||||||
|
display = s.to_owned();
|
||||||
|
style = Style::new().fg(Color::LightMagenta)
|
||||||
|
}
|
||||||
CellType::Equation(e) => {
|
CellType::Equation(e) => {
|
||||||
match self.grid.evaluate(e) {
|
match self.grid.evaluate(e) {
|
||||||
Ok(val) => {
|
Ok(val) => {
|
||||||
@@ -144,9 +157,20 @@ impl Widget for &App {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(bound) = suggest_upper_bound {
|
||||||
|
let bound = bound as usize;
|
||||||
|
if bound < display.len() {
|
||||||
|
display.truncate(bound - 2);
|
||||||
|
display.push('…');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => should_render = false,
|
None => should_render = false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if is_selected(x.into(), y.into()) {
|
||||||
|
style = style.bg(Color::Blue);
|
||||||
|
}
|
||||||
if (x_idx, y_idx) == self.grid.cursor() {
|
if (x_idx, y_idx) == self.grid.cursor() {
|
||||||
should_render = true;
|
should_render = true;
|
||||||
style = Style::new().fg(Color::Black).bg(Color::White);
|
style = Style::new().fg(Color::Black).bg(Color::White);
|
||||||
@@ -192,13 +216,24 @@ impl App {
|
|||||||
screen: ScreenSpace::new(),
|
screen: ScreenSpace::new(),
|
||||||
marks: HashMap::new(),
|
marks: HashMap::new(),
|
||||||
clipboard: Clipboard::new(),
|
clipboard: Clipboard::new(),
|
||||||
|
file_modified_date: SystemTime::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_file(file: impl Into<PathBuf> + Clone) -> std::io::Result<Self> {
|
pub fn new_with_file(file: impl Into<PathBuf> + Clone) -> std::io::Result<Self> {
|
||||||
let mut app = Self::new();
|
let mut app = Self::new();
|
||||||
app.file = Some(file.clone().into());
|
app.file = Some(file.clone().into());
|
||||||
app.grid = Grid::new_from_file(file.into())?;
|
|
||||||
|
let mut file = fs::OpenOptions::new().read(true).open(file.into())?;
|
||||||
|
let metadata = file.metadata()?;
|
||||||
|
// Not all systems support this, apparently.
|
||||||
|
if let Ok(time) = metadata.modified() {
|
||||||
|
app.file_modified_date = time;
|
||||||
|
} else {
|
||||||
|
// Default is to just assume it was modified when we opened it.
|
||||||
|
}
|
||||||
|
|
||||||
|
app.grid = Grid::new_from_file(&mut file)?;
|
||||||
Ok(app)
|
Ok(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,27 +245,7 @@ impl App {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, frame: &mut Frame) {
|
fn file_name_display(&self) -> String {
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(layout::Direction::Vertical)
|
|
||||||
.constraints([Constraint::Length(1), Constraint::Min(1)])
|
|
||||||
.split(frame.area());
|
|
||||||
|
|
||||||
let cmd_line = layout[0];
|
|
||||||
let body = layout[1];
|
|
||||||
|
|
||||||
let len = match &self.mode {
|
|
||||||
Mode::Insert(edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(),
|
|
||||||
Mode::Normal => {
|
|
||||||
let (x, y) = self.grid.cursor();
|
|
||||||
let cell = self.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string().len()).unwrap_or_default();
|
|
||||||
cell
|
|
||||||
}
|
|
||||||
Mode::Visual(_) => 0,
|
|
||||||
};
|
|
||||||
// min 20 chars, expand if needed
|
|
||||||
let len = max(len as u16 + 1, 20);
|
|
||||||
|
|
||||||
let file_name_status = {
|
let file_name_status = {
|
||||||
let mut file_name = "[No Name]";
|
let mut file_name = "[No Name]";
|
||||||
let mut icon = "";
|
let mut icon = "";
|
||||||
@@ -243,42 +258,49 @@ impl App {
|
|||||||
}
|
}
|
||||||
if self.grid.needs_to_be_saved() {
|
if self.grid.needs_to_be_saved() {
|
||||||
icon = "[+]";
|
icon = "[+]";
|
||||||
}
|
}
|
||||||
format!("{file_name}{icon}")
|
format!("{file_name}{icon}")
|
||||||
};
|
};
|
||||||
|
file_name_status
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, frame: &mut Frame) {
|
||||||
|
let (x, y) = self.grid.cursor();
|
||||||
|
let current_cell = self.grid.get_cell_raw(x, y);
|
||||||
|
let len = self.mode.chars_to_display(current_cell);
|
||||||
|
let file_name_status = self.file_name_display();
|
||||||
|
|
||||||
|
// layout
|
||||||
|
// ======================================================
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(layout::Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(1), Constraint::Min(1)])
|
||||||
|
.split(frame.area());
|
||||||
|
let cmd_line = layout[0];
|
||||||
|
let body = layout[1];
|
||||||
|
|
||||||
let cmd_line_split = Layout::default()
|
let cmd_line_split = Layout::default()
|
||||||
.direction(layout::Direction::Horizontal)
|
.direction(layout::Direction::Horizontal)
|
||||||
.constraints([Constraint::Length(len), Constraint::Length(file_name_status.len() as u16 + 1), Constraint::Percentage(50), Constraint::Percentage(50)])
|
.constraints([
|
||||||
|
Constraint::Length(len),
|
||||||
|
Constraint::Length(file_name_status.len() as u16 + 1),
|
||||||
|
Constraint::Percentage(50),
|
||||||
|
Constraint::Percentage(50),
|
||||||
|
])
|
||||||
.split(cmd_line);
|
.split(cmd_line);
|
||||||
|
|
||||||
let cmd_line_left = cmd_line_split[0];
|
let cmd_line_left = cmd_line_split[0];
|
||||||
let cmd_line_status = cmd_line_split[1];
|
let cmd_line_status = cmd_line_split[1];
|
||||||
let cmd_line_right = cmd_line_split[2];
|
let cmd_line_right = cmd_line_split[2];
|
||||||
let cmd_line_debug = cmd_line_split[3];
|
let cmd_line_debug = cmd_line_split[3];
|
||||||
|
// ======================================================
|
||||||
|
|
||||||
match &self.mode {
|
self.mode.render(frame, cmd_line_left, current_cell);
|
||||||
Mode::Insert(editor) => {
|
|
||||||
frame.render_widget(editor, cmd_line_left);
|
|
||||||
}
|
|
||||||
Mode::Command(editor) => {
|
|
||||||
frame.render_widget(editor, cmd_line_left);
|
|
||||||
}
|
|
||||||
Mode::Chord(chord) => frame.render_widget(chord, cmd_line_left),
|
|
||||||
Mode::Normal => frame.render_widget(
|
|
||||||
Paragraph::new({
|
|
||||||
let (x, y) = self.grid.cursor();
|
|
||||||
let cell = self.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or_default();
|
|
||||||
cell
|
|
||||||
}),
|
|
||||||
cmd_line_left,
|
|
||||||
),
|
|
||||||
Mode::Visual(_) => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.render_widget(self, body);
|
frame.render_widget(self, body);
|
||||||
frame.render_widget(&self.msg, cmd_line_right);
|
frame.render_widget(&self.msg, cmd_line_right);
|
||||||
frame.render_widget(Paragraph::new(file_name_status), cmd_line_status);
|
frame.render_widget(Paragraph::new(file_name_status), cmd_line_status);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(format!(
|
Paragraph::new(format!(
|
||||||
@@ -297,6 +319,21 @@ impl App {
|
|||||||
|
|
||||||
fn handle_events(&mut self) -> io::Result<()> {
|
fn handle_events(&mut self) -> io::Result<()> {
|
||||||
match &mut self.mode {
|
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()? {
|
Mode::Chord(chord) => match event::read()? {
|
||||||
event::Event::Key(key) => match key.code {
|
event::Event::Key(key) => match key.code {
|
||||||
event::KeyCode::Esc => self.mode = Mode::Normal,
|
event::KeyCode::Esc => self.mode = Mode::Normal,
|
||||||
@@ -366,20 +403,13 @@ impl App {
|
|||||||
}
|
}
|
||||||
Mode::Command(editor) => match event::read()? {
|
Mode::Command(editor) => match event::read()? {
|
||||||
event::Event::Key(key) => match key.code {
|
event::Event::Key(key) => match key.code {
|
||||||
event::KeyCode::Esc => {
|
event::KeyCode::Esc => self.mode = Mode::Normal,
|
||||||
// just cancel the operation
|
event::KeyCode::Backspace => editor.backspace(),
|
||||||
self.mode = Mode::Normal;
|
event::KeyCode::Char(c) => editor.add_char(c),
|
||||||
}
|
|
||||||
event::KeyCode::Enter => {
|
event::KeyCode::Enter => {
|
||||||
Mode::process_cmd(self);
|
Mode::process_cmd(self);
|
||||||
self.mode = Mode::Normal;
|
self.mode = Mode::Normal;
|
||||||
}
|
}
|
||||||
event::KeyCode::Backspace => {
|
|
||||||
editor.backspace();
|
|
||||||
}
|
|
||||||
event::KeyCode::Char(c) => {
|
|
||||||
editor.add_char(c);
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -296,3 +296,57 @@ fn copy_paste_vars_translate() {
|
|||||||
let a = app.grid.get_cell("A0").as_ref().expect("Should've been set by paste");
|
let a = app.grid.get_cell("A0").as_ref().expect("Should've been set by paste");
|
||||||
assert_eq!(a.to_string(), "=A1");
|
assert_eq!(a.to_string(), "=A1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_paste_double_locked_var() {
|
||||||
|
let mut app = App::new();
|
||||||
|
|
||||||
|
app.grid.set_cell("A0", 0.);
|
||||||
|
app.grid.set_cell("A1", "=$A$0".to_string());
|
||||||
|
|
||||||
|
// Copy A0
|
||||||
|
app.grid.mv_cursor_to(0, 1);
|
||||||
|
app.mode = super::mode::Mode::Chord(Chord::new('y'));
|
||||||
|
Mode::process_key(&mut app, 'y');
|
||||||
|
|
||||||
|
app.grid.mv_cursor_to(1, 0);
|
||||||
|
Mode::process_key(&mut app, 'p');
|
||||||
|
let c = app.grid.get_cell("B0").as_ref().expect("Just set it");
|
||||||
|
assert_eq!(c.to_string(), "=$A$0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_paste_x_locked_var() {
|
||||||
|
let mut app = App::new();
|
||||||
|
|
||||||
|
app.grid.set_cell("A0", 0.);
|
||||||
|
app.grid.set_cell("A1", "=$A0".to_string());
|
||||||
|
|
||||||
|
// Copy A0
|
||||||
|
app.grid.mv_cursor_to(0, 1);
|
||||||
|
app.mode = super::mode::Mode::Chord(Chord::new('y'));
|
||||||
|
Mode::process_key(&mut app, 'y');
|
||||||
|
|
||||||
|
app.grid.mv_cursor_to(1, 2);
|
||||||
|
Mode::process_key(&mut app, 'p');
|
||||||
|
let c = app.grid.get_cell("B2").as_ref().expect("Just set it");
|
||||||
|
assert_eq!(c.to_string(), "=$A1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_paste_y_locked_var() {
|
||||||
|
let mut app = App::new();
|
||||||
|
|
||||||
|
app.grid.set_cell("A0", 0.);
|
||||||
|
app.grid.set_cell("A1", "=A$0".to_string());
|
||||||
|
|
||||||
|
// Copy A0
|
||||||
|
app.grid.mv_cursor_to(0, 1);
|
||||||
|
app.mode = super::mode::Mode::Chord(Chord::new('y'));
|
||||||
|
Mode::process_key(&mut app, 'y');
|
||||||
|
|
||||||
|
app.grid.mv_cursor_to(1, 2);
|
||||||
|
Mode::process_key(&mut app, 'p');
|
||||||
|
let c = app.grid.get_cell("B2").as_ref().expect("Just set it");
|
||||||
|
assert_eq!(c.to_string(), "=B$0");
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cmp::{max, min},
|
cmp::{max, min},
|
||||||
fs,
|
fs::{self, File},
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
path::PathBuf,
|
path::PathBuf
|
||||||
};
|
};
|
||||||
|
|
||||||
use evalexpr::*;
|
use evalexpr::*;
|
||||||
@@ -11,14 +11,15 @@ use crate::app::{
|
|||||||
logic::{
|
logic::{
|
||||||
cell::{CSV_DELIMITER, CellType},
|
cell::{CSV_DELIMITER, CellType},
|
||||||
ctx,
|
ctx,
|
||||||
},
|
}, mode::Mode,
|
||||||
mode::Mode,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::app::app::App;
|
use crate::app::app::App;
|
||||||
|
|
||||||
pub const LEN: usize = 1000;
|
pub const LEN: usize = 1000;
|
||||||
|
pub const CSV_EXT: &str = "csv";
|
||||||
|
pub const CUSTOM_EXT: &str = "nscim";
|
||||||
|
|
||||||
pub struct Grid {
|
pub struct Grid {
|
||||||
// a b c ...
|
// a b c ...
|
||||||
@@ -57,10 +58,9 @@ impl Grid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_from_file(path: impl Into<PathBuf>) -> std::io::Result<Self> {
|
pub fn new_from_file(file: &mut File) -> std::io::Result<Self> {
|
||||||
let mut grid = Self::new();
|
let mut grid = Self::new();
|
||||||
|
|
||||||
let mut file = fs::OpenOptions::new().read(true).open(path.into())?;
|
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
file.read_to_string(&mut buf)?;
|
file.read_to_string(&mut buf)?;
|
||||||
for (yi, line) in buf.lines().enumerate() {
|
for (yi, line) in buf.lines().enumerate() {
|
||||||
@@ -81,29 +81,26 @@ impl Grid {
|
|||||||
/// Save file to `path` as a csv. Path with have `csv` appended to it if it
|
/// Save file to `path` as a csv. Path with have `csv` appended to it if it
|
||||||
/// does not already have the extension.
|
/// does not already have the extension.
|
||||||
pub fn save_to(&mut self, path: impl Into<PathBuf>) -> std::io::Result<()> {
|
pub fn save_to(&mut self, path: impl Into<PathBuf>) -> std::io::Result<()> {
|
||||||
let mut path = path.into();
|
let path = path.into();
|
||||||
|
|
||||||
const CSV: &str = "csv";
|
|
||||||
const CUSTOM_EXT: &str = "nscim";
|
|
||||||
|
|
||||||
let resolve_values;
|
let resolve_values;
|
||||||
|
|
||||||
match path.extension() {
|
match path.extension() {
|
||||||
Some(ext) => match ext.to_str() {
|
Some(ext) => match ext.to_str() {
|
||||||
Some(CSV) => {
|
Some(CSV_EXT) => {
|
||||||
resolve_values = true;
|
resolve_values = true;
|
||||||
}
|
}
|
||||||
Some(CUSTOM_EXT) => {
|
Some(CUSTOM_EXT) => {
|
||||||
resolve_values = true;
|
resolve_values = false;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
resolve_values = false;
|
resolve_values = false;
|
||||||
path.add_extension(CUSTOM_EXT);
|
// path.add_extension(CUSTOM_EXT);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
resolve_values = false;
|
resolve_values = false;
|
||||||
path.add_extension(CUSTOM_EXT);
|
// path.add_extension(CUSTOM_EXT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,21 +110,28 @@ impl Grid {
|
|||||||
for x in 0..=mx {
|
for x in 0..=mx {
|
||||||
let cell = &self.cells[x][y];
|
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 {
|
let data = if let Some(cell) = cell {
|
||||||
if let Ok(val) = self.evaluate(&cell.to_string())
|
if let Ok(val) = self.evaluate(&cell.to_string())
|
||||||
&& resolve_values
|
&& resolve_values
|
||||||
{
|
{
|
||||||
val.to_string()
|
format!("{}{}", val.to_string(), delim)
|
||||||
} else {
|
} else {
|
||||||
cell.escaped_csv_string()
|
format!("{}{}", cell.escaped_csv_string(), delim)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
CSV_DELIMITER.to_string()
|
delim.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
write!(f, "{data}")?;
|
write!(f, "{data}")?;
|
||||||
}
|
}
|
||||||
write!(f, "\n")?;
|
|
||||||
}
|
}
|
||||||
f.flush()?;
|
f.flush()?;
|
||||||
|
|
||||||
@@ -381,6 +385,8 @@ impl Grid {
|
|||||||
/// Parse values in the format of A0, C10 ZZ99, etc, and
|
/// Parse values in the format of A0, C10 ZZ99, etc, and
|
||||||
/// turn them into an X,Y index.
|
/// turn them into an X,Y index.
|
||||||
pub fn parse_to_idx(i: &str) -> Option<(usize, usize)> {
|
pub fn parse_to_idx(i: &str) -> Option<(usize, usize)> {
|
||||||
|
let i = i.replace('$', "");
|
||||||
|
|
||||||
let chars = i.chars().take_while(|c| c.is_alphabetic()).collect::<Vec<char>>();
|
let chars = i.chars().take_while(|c| c.is_alphabetic()).collect::<Vec<char>>();
|
||||||
let nums = i.chars().skip(chars.len()).take_while(|c| c.is_numeric()).collect::<String>();
|
let nums = i.chars().skip(chars.len()).take_while(|c| c.is_numeric()).collect::<String>();
|
||||||
|
|
||||||
@@ -460,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?
|
// Do cells hold strings?
|
||||||
#[test]
|
#[test]
|
||||||
fn cell_strings() {
|
fn cell_strings() {
|
||||||
@@ -476,6 +568,9 @@ fn cell_strings() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn alphanumeric_indexing() {
|
fn alphanumeric_indexing() {
|
||||||
assert_eq!(Grid::parse_to_idx("A0"), Some((0, 0)));
|
assert_eq!(Grid::parse_to_idx("A0"), Some((0, 0)));
|
||||||
|
assert_eq!(Grid::parse_to_idx("$A0"), Some((0, 0)));
|
||||||
|
assert_eq!(Grid::parse_to_idx("A$0"), Some((0, 0)));
|
||||||
|
assert_eq!(Grid::parse_to_idx("$A$0"), Some((0, 0)));
|
||||||
assert_eq!(Grid::parse_to_idx("AA0"), Some((26, 0)));
|
assert_eq!(Grid::parse_to_idx("AA0"), Some((26, 0)));
|
||||||
assert_eq!(Grid::parse_to_idx("A1"), Some((0, 1)));
|
assert_eq!(Grid::parse_to_idx("A1"), Some((0, 1)));
|
||||||
assert_eq!(Grid::parse_to_idx("A10"), Some((0, 10)));
|
assert_eq!(Grid::parse_to_idx("A10"), Some((0, 10)));
|
||||||
@@ -800,8 +895,16 @@ fn ranges() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn recursive_ranges() {
|
fn recursive_ranges() {
|
||||||
// recursive ranges causes weird behavior
|
let mut grid = Grid::new();
|
||||||
// todo!();
|
|
||||||
|
grid.set_cell("A1", 1.);
|
||||||
|
grid.set_cell("B1", 2.);
|
||||||
|
grid.set_cell("B0", "=sum(A:A)".to_string());
|
||||||
|
grid.set_cell("A0", "=sum(B:B)".to_string());
|
||||||
|
|
||||||
|
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
||||||
|
let res = grid.evaluate(&cell.to_string());
|
||||||
|
assert!(res.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -934,3 +1037,29 @@ fn insert_row_above_3() {
|
|||||||
let cell = grid.get_cell("B1").as_ref().expect("Just set it");
|
let cell = grid.get_cell("B1").as_ref().expect("Just set it");
|
||||||
assert_eq!(cell.to_string(), "=A1");
|
assert_eq!(cell.to_string(), "=A1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_eval_depth() {
|
||||||
|
use crate::app::mode::*;
|
||||||
|
let mut app= App::new();
|
||||||
|
|
||||||
|
app.grid.set_cell("A0", 1.);
|
||||||
|
app.grid.set_cell("A1", "=A0+$A$0".to_string());
|
||||||
|
|
||||||
|
app.grid.mv_cursor_to(0, 1);
|
||||||
|
app.mode = Mode::Chord(Chord::new('y'));
|
||||||
|
Mode::process_key(&mut app, 'y');
|
||||||
|
Mode::process_key(&mut app, 'j');
|
||||||
|
app.mode = Mode::Chord(Chord::new('5'));
|
||||||
|
Mode::process_key(&mut app, 'p');
|
||||||
|
|
||||||
|
assert_eq!(app.grid.cursor(), (0, 7));
|
||||||
|
|
||||||
|
let c = app.grid.get_cell("A6").as_ref().expect("Just set it");
|
||||||
|
|
||||||
|
assert_eq!(c.to_string(), "=A5+$A$0");
|
||||||
|
|
||||||
|
let res = app.grid.evaluate(&c.to_string()).expect("Should evaluate");
|
||||||
|
assert_eq!(res, 7.);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ impl CellType {
|
|||||||
|
|
||||||
// escape string of it has a comma
|
// escape string of it has a comma
|
||||||
if display.contains(CSV_DELIMITER) {
|
if display.contains(CSV_DELIMITER) {
|
||||||
format!("\"{display}\"{CSV_DELIMITER}")
|
format!("\"{display}\"")
|
||||||
} else {
|
} else {
|
||||||
format!("{display}{CSV_DELIMITER}")
|
display
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +66,37 @@ impl CellType {
|
|||||||
let mut rolling = eq.clone();
|
let mut rolling = eq.clone();
|
||||||
// translate standard vars A0 -> A1
|
// translate standard vars A0 -> A1
|
||||||
for old_var in ctx.dump_vars() {
|
for old_var in ctx.dump_vars() {
|
||||||
|
let mut lock_x = false;
|
||||||
|
let mut lock_y = false;
|
||||||
|
|
||||||
|
if old_var.contains('$') {
|
||||||
|
let locations = old_var.char_indices().filter(|(_, c)| *c == '$').map(|(i, _)| i).collect::<Vec<usize>>();
|
||||||
|
match locations.len() {
|
||||||
|
1 => {
|
||||||
|
if locations[0] == 0 {
|
||||||
|
// locking the X axis (A,B,C...)
|
||||||
|
lock_x = true;
|
||||||
|
} else if locations[0] < old_var.len() {
|
||||||
|
// inside the string somewhere, gonna assume this means to lock Y (1,2,3...)
|
||||||
|
lock_y = true;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// where tf is this dollar sign?
|
||||||
|
// (It's somewhere malformed, like A0$ or something)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
// YOLO, lock both X & Y
|
||||||
|
continue; // just pretend you never even saw this var
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Could probably optimize the code or something so you only go over the string
|
||||||
|
// once, instead of contains() then getting the indexes of where it is.
|
||||||
|
// You could then put your no-$ code here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) {
|
if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) {
|
||||||
let (x1, y1) = from;
|
let (x1, y1) = from;
|
||||||
let x1 = x1 as i32;
|
let x1 = x1 as i32;
|
||||||
@@ -74,12 +105,29 @@ impl CellType {
|
|||||||
let x2 = x2 as i32;
|
let x2 = x2 as i32;
|
||||||
let y2 = y2 as i32;
|
let y2 = y2 as i32;
|
||||||
|
|
||||||
let dest_x = (src_x as i32 + (x2 - x1)) as usize;
|
let dest_x = if lock_x {
|
||||||
let dest_y = (src_y as i32 + (y2 - y1)) as usize;
|
src_x as usize
|
||||||
|
} else {
|
||||||
|
(src_x as i32 + (x2 - x1)) as usize
|
||||||
|
};
|
||||||
|
|
||||||
|
let dest_y = if lock_y {
|
||||||
|
src_y as usize
|
||||||
|
} else {
|
||||||
|
(src_y as i32 + (y2 - y1)) as usize
|
||||||
|
};
|
||||||
|
|
||||||
let alpha = Grid::num_to_char(dest_x);
|
let alpha = Grid::num_to_char(dest_x);
|
||||||
let alpha = alpha.trim();
|
let alpha = alpha.trim();
|
||||||
let new_var = format!("{alpha}{dest_y}");
|
|
||||||
|
let new_var = if lock_x {
|
||||||
|
format!("${alpha}{dest_y}")
|
||||||
|
} else if lock_y {
|
||||||
|
format!("{alpha}${dest_y}")
|
||||||
|
} else {
|
||||||
|
format!("{alpha}{dest_y}")
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// swap out vars
|
// swap out vars
|
||||||
rolling = replace_fn(&rolling, &old_var, &new_var);
|
rolling = replace_fn(&rolling, &old_var, &new_var);
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ use crate::app::logic::{calc::Grid, cell::CellType};
|
|||||||
|
|
||||||
pub struct CallbackContext<'a> {
|
pub struct CallbackContext<'a> {
|
||||||
variables: &'a Grid,
|
variables: &'a Grid,
|
||||||
eval_depth: RwLock<usize>,
|
eval_breadcrumbs: RwLock<Vec<String>>,
|
||||||
|
compute_cache: RwLock<HashMap<String, Value>>,
|
||||||
functions: HashMap<String, Function<DefaultNumericTypes>>,
|
functions: HashMap<String, Function<DefaultNumericTypes>>,
|
||||||
|
|
||||||
/// True if builtin functions are disabled.
|
/// True if builtin functions are disabled.
|
||||||
@@ -139,10 +140,11 @@ impl<'a> CallbackContext<'a> {
|
|||||||
|
|
||||||
pub fn new(grid: &'a Grid) -> Self {
|
pub fn new(grid: &'a Grid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
eval_depth: RwLock::new(0),
|
|
||||||
variables: grid,
|
variables: grid,
|
||||||
functions: Self::get_functions(),
|
functions: Self::get_functions(),
|
||||||
without_builtin_functions: false,
|
without_builtin_functions: false,
|
||||||
|
eval_breadcrumbs: RwLock::new(Vec::new()),
|
||||||
|
compute_cache: RwLock::new(HashMap::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,27 +153,46 @@ impl<'a> Context for CallbackContext<'a> {
|
|||||||
type NumericTypes = DefaultNumericTypes;
|
type NumericTypes = DefaultNumericTypes;
|
||||||
|
|
||||||
fn get_value(&self, identifier: &str) -> Option<Value<Self::NumericTypes>> {
|
fn get_value(&self, identifier: &str) -> Option<Value<Self::NumericTypes>> {
|
||||||
const RECURSION_DEPTH_LIMIT: usize = 20;
|
|
||||||
|
// check cache
|
||||||
|
if let Ok(cc) = self.compute_cache.read() {
|
||||||
|
if let Some(hit) = cc.get(identifier) {
|
||||||
|
return Some(hit.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(mut trail) = self.eval_breadcrumbs.write() {
|
||||||
|
let find = trail.iter().filter(|id| *id == identifier).collect::<Vec<&String>>();
|
||||||
|
if find.len() > 0 {
|
||||||
|
// recursion detected
|
||||||
|
return None;
|
||||||
|
} else {
|
||||||
|
trail.push(identifier.to_owned(), );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pre_return = |v: Value| {
|
||||||
|
if let Ok(mut cc) = self.compute_cache.write() {
|
||||||
|
cc.insert(identifier.to_owned(), v.clone());
|
||||||
|
}
|
||||||
|
Some(v)
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(v) = self.variables.get_cell(identifier) {
|
if let Some(v) = self.variables.get_cell(identifier) {
|
||||||
match v {
|
match v {
|
||||||
CellType::Number(n) => return Some(Value::Float(n.to_owned())),
|
CellType::Number(n) => {
|
||||||
CellType::String(s) => return Some(Value::String(s.to_owned())),
|
return pre_return(Value::Float(n.to_owned()));
|
||||||
CellType::Equation(eq) => {
|
},
|
||||||
if let Ok(mut depth) = self.eval_depth.write() {
|
CellType::String(s) => {
|
||||||
*depth += 1;
|
return pre_return(Value::String(s.to_owned()));
|
||||||
if *depth > RECURSION_DEPTH_LIMIT {
|
},
|
||||||
return None;
|
CellType::Equation(eq) => {
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// It would be unsafe to continue to process without knowing how
|
|
||||||
// deep we've gone.
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
// remove the equals sign from the beginning, as that
|
// remove the equals sign from the beginning, as that
|
||||||
// tries to set variables with our evaluation lib
|
// tries to set variables with our evaluation lib
|
||||||
match eval_with_context(&eq[1..], self) {
|
match eval_with_context(&eq[1..], self) {
|
||||||
Ok(e) => return Some(e),
|
Ok(e) => {
|
||||||
|
return pre_return(e)
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
match e {
|
match e {
|
||||||
EvalexprError::VariableIdentifierNotFound(_) => {
|
EvalexprError::VariableIdentifierNotFound(_) => {
|
||||||
@@ -196,14 +217,6 @@ impl<'a> Context for CallbackContext<'a> {
|
|||||||
CellType::Number(e) => vals.push(Value::Float(*e)),
|
CellType::Number(e) => vals.push(Value::Float(*e)),
|
||||||
CellType::String(s) => vals.push(Value::String(s.to_owned())),
|
CellType::String(s) => vals.push(Value::String(s.to_owned())),
|
||||||
CellType::Equation(eq) => {
|
CellType::Equation(eq) => {
|
||||||
if let Ok(mut depth) = self.eval_depth.write() {
|
|
||||||
*depth += 1;
|
|
||||||
if *depth > RECURSION_DEPTH_LIMIT {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if let Ok(val) = eval_with_context(&eq[1..], self) {
|
if let Ok(val) = eval_with_context(&eq[1..], self) {
|
||||||
vals.push(val);
|
vals.push(val);
|
||||||
}
|
}
|
||||||
@@ -239,10 +252,8 @@ impl<'a> Context for CallbackContext<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// DOES NOT EVALUATE EQUATIONS!!
|
/// DOES NOT EVALUATE EQUATIONS!!
|
||||||
///
|
///
|
||||||
/// This is used as a pseudo-context, just used for
|
/// This is used as a pseudo-context, just used for
|
||||||
/// learning all the variables in an expression.
|
/// learning all the variables in an expression.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -259,18 +270,10 @@ impl ExtractionContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn dump_vars(&self) -> Vec<String> {
|
pub fn dump_vars(&self) -> Vec<String> {
|
||||||
if let Ok(r) = self.var_registry.read() {
|
if let Ok(r) = self.var_registry.read() { r.clone() } else { Vec::new() }
|
||||||
r.clone()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub fn dump_fns(&self) -> Vec<String> {
|
pub fn dump_fns(&self) -> Vec<String> {
|
||||||
if let Ok(r) = self.fn_registry.read() {
|
if let Ok(r) = self.fn_registry.read() { r.clone() } else { Vec::new() }
|
||||||
r.clone()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +283,9 @@ impl Context for ExtractionContext {
|
|||||||
fn get_value(&self, identifier: &str) -> Option<Value<Self::NumericTypes>> {
|
fn get_value(&self, identifier: &str) -> Option<Value<Self::NumericTypes>> {
|
||||||
if let Ok(mut registry) = self.var_registry.write() {
|
if let Ok(mut registry) = self.var_registry.write() {
|
||||||
registry.push(identifier.to_owned());
|
registry.push(identifier.to_owned());
|
||||||
} else { panic!("The RwLock should always be write-able") }
|
} else {
|
||||||
|
panic!("The RwLock should always be write-able")
|
||||||
|
}
|
||||||
|
|
||||||
Some(Value::Int(1))
|
Some(Value::Int(1))
|
||||||
}
|
}
|
||||||
@@ -293,9 +298,11 @@ impl Context for ExtractionContext {
|
|||||||
let _ = argument;
|
let _ = argument;
|
||||||
if let Ok(mut registry) = self.fn_registry.write() {
|
if let Ok(mut registry) = self.fn_registry.write() {
|
||||||
registry.push(identifier.to_owned())
|
registry.push(identifier.to_owned())
|
||||||
} else { panic!("The RwLock should always be write-able") }
|
} else {
|
||||||
|
panic!("The RwLock should always be write-able")
|
||||||
|
}
|
||||||
// Ok(Value::Int(1))
|
// Ok(Value::Int(1))
|
||||||
todo!();
|
unimplemented!("Extracting function identifier not implemented yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn are_builtin_functions_disabled(&self) -> bool {
|
fn are_builtin_functions_disabled(&self) -> bool {
|
||||||
|
|||||||
184
src/app/mode.rs
184
src/app/mode.rs
@@ -1,4 +1,9 @@
|
|||||||
use std::{cmp::min, fmt::Display, path::PathBuf};
|
use std::{
|
||||||
|
cmp::{max, min},
|
||||||
|
fmt::Display,
|
||||||
|
fs,
|
||||||
|
path::PathBuf, process::Command,
|
||||||
|
};
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
prelude,
|
prelude,
|
||||||
@@ -9,7 +14,10 @@ use ratatui::{
|
|||||||
use crate::app::{
|
use crate::app::{
|
||||||
app::App,
|
app::App,
|
||||||
error_msg::StatusMessage,
|
error_msg::StatusMessage,
|
||||||
logic::calc::LEN,
|
logic::{
|
||||||
|
calc::{CSV_EXT, CUSTOM_EXT, Grid, LEN},
|
||||||
|
cell::CellType,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
@@ -18,6 +26,7 @@ pub enum Mode {
|
|||||||
Normal,
|
Normal,
|
||||||
Command(Chord),
|
Command(Chord),
|
||||||
Visual((usize, usize)),
|
Visual((usize, usize)),
|
||||||
|
VisualCmd((usize, usize), Chord),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Mode {
|
impl Display for Mode {
|
||||||
@@ -28,6 +37,7 @@ impl Display for Mode {
|
|||||||
Mode::Chord(_) => write!(f, "CHORD"),
|
Mode::Chord(_) => write!(f, "CHORD"),
|
||||||
Mode::Command(_) => write!(f, "COMMAND"),
|
Mode::Command(_) => write!(f, "COMMAND"),
|
||||||
Mode::Visual(_) => write!(f, "VISUAL"),
|
Mode::Visual(_) => write!(f, "VISUAL"),
|
||||||
|
Mode::VisualCmd(_, _) => write!(f, "V-CMD"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,6 +48,7 @@ impl Mode {
|
|||||||
// Where you are typing
|
// Where you are typing
|
||||||
Mode::Insert(_) => Style::new().fg(Color::White).bg(Color::Blue),
|
Mode::Insert(_) => Style::new().fg(Color::White).bg(Color::Blue),
|
||||||
Mode::Command(_) => Style::new().fg(Color::Black).bg(Color::Magenta),
|
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),
|
Mode::Chord(_) => Style::new().fg(Color::Black).bg(Color::LightBlue),
|
||||||
// Movement-based modes
|
// Movement-based modes
|
||||||
Mode::Visual(_) => Style::new().fg(Color::Yellow),
|
Mode::Visual(_) => Style::new().fg(Color::Yellow),
|
||||||
@@ -59,13 +70,35 @@ impl Mode {
|
|||||||
"w" => {
|
"w" => {
|
||||||
// first try the passed argument as file
|
// first try the passed argument as file
|
||||||
if let Some(arg) = args.get(1) {
|
if let Some(arg) = args.get(1) {
|
||||||
if let Err(e) = app.grid.save_to(arg) {
|
let mut path: PathBuf = arg.into();
|
||||||
|
match path.extension() {
|
||||||
|
Some(s) => {
|
||||||
|
match s.to_str() {
|
||||||
|
// leave the file alone, it already has
|
||||||
|
// a valid extension
|
||||||
|
Some(CSV_EXT) | Some(CUSTOM_EXT) => {}
|
||||||
|
_ => {
|
||||||
|
path.add_extension(CUSTOM_EXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
path.add_extension(CUSTOM_EXT);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO Check if the file exists, but the program wasn't opened with it. We might be accidentally overwriting something else.
|
||||||
|
// let mut file = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?;
|
||||||
|
|
||||||
|
if let Err(e) = app.grid.save_to(&path) {
|
||||||
app.msg = StatusMessage::error(format!("{e}"));
|
app.msg = StatusMessage::error(format!("{e}"));
|
||||||
} else {
|
} else {
|
||||||
// file saving was a success, adopt the provided file
|
// file saving was a success, adopt the provided file
|
||||||
// if we don't already have one (this is how vim works)
|
// if we don't already have one (this is how vim works)
|
||||||
let path: PathBuf = arg.into();
|
app.msg = StatusMessage::info(format!(
|
||||||
app.msg = StatusMessage::info(format!("Saved file {}", path.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a")));
|
"Saved file {}",
|
||||||
|
path.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a")
|
||||||
|
));
|
||||||
|
|
||||||
if let None = app.file {
|
if let None = app.file {
|
||||||
app.file = Some(path)
|
app.file = Some(path)
|
||||||
@@ -76,7 +109,10 @@ impl Mode {
|
|||||||
if let Err(e) = app.grid.save_to(file) {
|
if let Err(e) = app.grid.save_to(file) {
|
||||||
app.msg = StatusMessage::error(format!("{e}"));
|
app.msg = StatusMessage::error(format!("{e}"));
|
||||||
} else {
|
} else {
|
||||||
app.msg = StatusMessage::info(format!("Saved file {}", file.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a")));
|
app.msg = StatusMessage::info(format!(
|
||||||
|
"Saved file {}",
|
||||||
|
file.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a")
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// you need to provide a file from *somewhere*
|
// you need to provide a file from *somewhere*
|
||||||
} else {
|
} else {
|
||||||
@@ -113,6 +149,82 @@ impl Mode {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Mode::VisualCmd(pos, editor) = &mut app.mode {
|
||||||
|
let cmd = &editor.as_string()[1..];
|
||||||
|
let args = cmd.split_ascii_whitespace().collect::<Vec<&str>>();
|
||||||
|
if args.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These values are going to be used in probably all
|
||||||
|
// the commands related to ranges, we will just write
|
||||||
|
// logic here first, once.
|
||||||
|
let (x1, y1) = pos;
|
||||||
|
let (x1, y1) = (*x1, *y1);
|
||||||
|
let (x2, y2) = app.grid.cursor();
|
||||||
|
let (low_x, hi_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
|
||||||
|
let (low_y, hi_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
|
||||||
|
|
||||||
|
let mut save_range = |to: &str| {
|
||||||
|
let mut g = Grid::new();
|
||||||
|
for (i, x) in (low_x..=hi_x).enumerate() {
|
||||||
|
for (j, y) in (low_y..=hi_y).enumerate() {
|
||||||
|
g.set_cell_raw((i, j), app.grid.get_cell_raw(x, y).clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(_e) = g.save_to(to) {
|
||||||
|
app.msg = StatusMessage::error("Failed to save file");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let get_project_name = || {
|
||||||
|
if let Some(file) = &app.file {
|
||||||
|
if let Some(name) = file.file_name() {
|
||||||
|
if let Some(name) = name.to_str() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
match args[0] {
|
||||||
|
"export" => {
|
||||||
|
if let Some(arg1) = args.get(1) {
|
||||||
|
save_range(&arg1);
|
||||||
|
} else {
|
||||||
|
app.msg = StatusMessage::error("export <path.csv>")
|
||||||
|
}
|
||||||
|
app.mode = Mode::Normal
|
||||||
|
}
|
||||||
|
"plot" => {
|
||||||
|
// Use gnuplot to plot the selected data.
|
||||||
|
// * Temp data will be stored in /tmp/
|
||||||
|
// * Output will either be plot.png or a name that you pass in
|
||||||
|
let output_filename = if let Some(arg1) = args.get(1) {
|
||||||
|
arg1
|
||||||
|
} else {
|
||||||
|
"plot.png"
|
||||||
|
};
|
||||||
|
|
||||||
|
save_range("/tmp/plot.csv");
|
||||||
|
let plot = include_str!("../../template.gnuplot");
|
||||||
|
let s = plot.replace("$FILE", "/tmp/plot.csv");
|
||||||
|
let s = s.replace("$TITLE", get_project_name());
|
||||||
|
let s = s.replace("$XLABEL", "hard-coded x");
|
||||||
|
let s = s.replace("$YLABEL", "hard-coded y");
|
||||||
|
let s = s.replace("$OUTPUT", "/tmp/output.png");
|
||||||
|
let _ = fs::write("/tmp/plot.p", s);
|
||||||
|
|
||||||
|
let _ = Command::new("gnuplot").arg("/tmp/plot.p").output();
|
||||||
|
let _ = fs::copy("/tmp/output.png", output_filename);
|
||||||
|
|
||||||
|
app.msg = StatusMessage::info("Wrote gnuplot data to /tmp");
|
||||||
|
app.mode = Mode::Normal
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_key(app: &mut App, key: char) {
|
pub fn process_key(app: &mut App, key: char) {
|
||||||
@@ -168,20 +280,26 @@ impl Mode {
|
|||||||
'A' => {
|
'A' => {
|
||||||
let c = app.grid.cursor();
|
let c = app.grid.cursor();
|
||||||
app.grid.insert_column_after(c);
|
app.grid.insert_column_after(c);
|
||||||
app.grid.mv_cursor_to(c.0+1, c.1);
|
app.grid.mv_cursor_to(c.0 + 1, c.1);
|
||||||
}
|
}
|
||||||
// insert row below
|
// insert row below
|
||||||
'o' => {
|
'o' => {
|
||||||
let c = app.grid.cursor();
|
let c = app.grid.cursor();
|
||||||
app.grid.insert_row_below(c);
|
app.grid.insert_row_below(c);
|
||||||
app.grid.mv_cursor_to(c.0, c.1+1);
|
app.grid.mv_cursor_to(c.0, c.1 + 1);
|
||||||
}
|
}
|
||||||
// insert row above
|
// insert row above
|
||||||
'O' => {
|
'O' => {
|
||||||
app.grid.insert_row_above(app.grid.cursor());
|
app.grid.insert_row_above(app.grid.cursor());
|
||||||
}
|
}
|
||||||
'v' => app.mode = Mode::Visual(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' => {
|
'p' => {
|
||||||
app.clipboard.paste(&mut app.grid, true);
|
app.clipboard.paste(&mut app.grid, true);
|
||||||
app.grid.apply_momentum(app.clipboard.momentum());
|
app.grid.apply_momentum(app.clipboard.momentum());
|
||||||
@@ -200,7 +318,7 @@ impl Mode {
|
|||||||
|
|
||||||
match key {
|
match key {
|
||||||
'd' | 'x' => {
|
'd' | 'x' => {
|
||||||
app.clipboard.clipboard_cut((x1,y1), (x2,y2), &mut app.grid);
|
app.clipboard.clipboard_cut((x1, y1), (x2, y2), &mut app.grid);
|
||||||
app.mode = Mode::Normal
|
app.mode = Mode::Normal
|
||||||
}
|
}
|
||||||
'y' => {
|
'y' => {
|
||||||
@@ -284,18 +402,54 @@ impl Mode {
|
|||||||
app.clipboard.paste(&mut app.grid, false);
|
app.clipboard.paste(&mut app.grid, false);
|
||||||
app.grid.apply_momentum(app.clipboard.momentum());
|
app.grid.apply_momentum(app.clipboard.momentum());
|
||||||
app.mode = Mode::Normal;
|
app.mode = Mode::Normal;
|
||||||
let plural = if app.clipboard.qty() > 1 {"cells"} else {"cell"};
|
let plural = if app.clipboard.qty() > 1 { "cells" } else { "cell" };
|
||||||
app.msg = StatusMessage::info(format!("Pasted {plural}, no formatting"));
|
app.msg = StatusMessage::info(format!("Pasted {plural}, no formatting"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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::Insert(_chord) => {}
|
||||||
Mode::Command(_chord) => {},
|
Mode::Command(_chord) => {}
|
||||||
|
Mode::VisualCmd(_pos, _chord) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chars_to_display(&self, cell: &Option<CellType>) -> u16 {
|
||||||
|
let len = match &self {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
Mode::Visual(_) => 0,
|
||||||
|
};
|
||||||
|
// min 20 chars, expand if needed
|
||||||
|
let len = max(len as u16 + 1, 20);
|
||||||
|
len
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, f: &mut ratatui::Frame, area: prelude::Rect, cell: &Option<CellType>) {
|
||||||
|
match &self {
|
||||||
|
Mode::Insert(editor) => {
|
||||||
|
f.render_widget(editor, area);
|
||||||
|
}
|
||||||
|
Mode::Command(editor) => {
|
||||||
|
f.render_widget(editor, area);
|
||||||
|
}
|
||||||
|
Mode::Chord(chord) => f.render_widget(chord, area),
|
||||||
|
Mode::Normal => f.render_widget(
|
||||||
|
Paragraph::new({
|
||||||
|
let cell = cell.as_ref().map(|f| f.to_string()).unwrap_or_default();
|
||||||
|
cell
|
||||||
|
}),
|
||||||
|
area,
|
||||||
|
),
|
||||||
|
Mode::Visual(_) => {}
|
||||||
|
Mode::VisualCmd(_, editor) => f.render_widget(editor, area),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
template.gnuplot
Normal file
18
template.gnuplot
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
datafile = '$FILE'
|
||||||
|
set datafile separator ','
|
||||||
|
|
||||||
|
set title '$TITLE'
|
||||||
|
set key autotitle columnhead
|
||||||
|
set xlabel "$XLABEL"
|
||||||
|
set ylabel "$YLABEL"
|
||||||
|
|
||||||
|
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'
|
||||||
|
plot datafile using 1:2 with linespoints linestyle 1
|
||||||
|
# plot datafile using 1:2 with linespoints linestyle 1, datafile using 1:3, etc...
|
||||||
|
replot
|
||||||
Reference in New Issue
Block a user