Compare commits
8 Commits
d5ccb9b8c6
...
plot-popup
| Author | SHA1 | Date | |
|---|---|---|---|
| dee88c916e | |||
| 0c527bb3cb | |||
| 6ca21b407d | |||
| 1323d15333 | |||
| d69966bc01 | |||
| dae3b57f73 | |||
| ee1cac0522 | |||
| 712d75b256 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
/.vscode
|
/.vscode
|
||||||
/*.csv
|
/*.csv
|
||||||
/*.nscim
|
/*.nscim
|
||||||
|
/plot.png
|
||||||
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
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
use std::{
|
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::{
|
use ratatui::{
|
||||||
DefaultTerminal, Frame,
|
DefaultTerminal, Frame,
|
||||||
crossterm::event,
|
crossterm::event::{self, KeyCode},
|
||||||
layout::{self, Constraint, Layout, Rect},
|
layout::{self, Constraint, Layout, Margin, Rect},
|
||||||
prelude,
|
prelude,
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
widgets::{Paragraph, Widget},
|
widgets::{Block, Borders, Paragraph, Widget},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
@@ -16,6 +20,7 @@ use crate::app::{
|
|||||||
error_msg::StatusMessage,
|
error_msg::StatusMessage,
|
||||||
logic::{calc::Grid, cell::CellType},
|
logic::{calc::Grid, cell::CellType},
|
||||||
mode::Mode,
|
mode::Mode,
|
||||||
|
plot::Plot,
|
||||||
screen::ScreenSpace,
|
screen::ScreenSpace,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,6 +36,7 @@ pub struct App {
|
|||||||
// this could probably be a normal array
|
// this could probably be a normal array
|
||||||
pub marks: HashMap<char, (usize, usize)>,
|
pub marks: HashMap<char, (usize, usize)>,
|
||||||
pub clipboard: Clipboard,
|
pub clipboard: Clipboard,
|
||||||
|
pub plot_popup: Option<Plot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for &App {
|
impl Widget for &App {
|
||||||
@@ -38,7 +44,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;
|
||||||
@@ -76,10 +82,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);
|
||||||
|
|
||||||
@@ -125,7 +127,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) => {
|
||||||
@@ -164,6 +169,10 @@ impl Widget for &App {
|
|||||||
}
|
}
|
||||||
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);
|
||||||
@@ -210,6 +219,7 @@ impl App {
|
|||||||
marks: HashMap::new(),
|
marks: HashMap::new(),
|
||||||
clipboard: Clipboard::new(),
|
clipboard: Clipboard::new(),
|
||||||
file_modified_date: SystemTime::now(),
|
file_modified_date: SystemTime::now(),
|
||||||
|
plot_popup: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,10 +318,62 @@ impl App {
|
|||||||
)),
|
)),
|
||||||
cmd_line_debug,
|
cmd_line_debug,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// popups
|
||||||
|
if let Some(plot) = &self.plot_popup {
|
||||||
|
let block = Block::default()
|
||||||
|
.title("Plot Editor")
|
||||||
|
.title_alignment(layout::Alignment::Center)
|
||||||
|
.border_type(ratatui::widgets::BorderType::Rounded)
|
||||||
|
.borders(Borders::all())
|
||||||
|
.style(Style::default().fg(Color::White));
|
||||||
|
let popup_y = 20;
|
||||||
|
let popup_x = 40;
|
||||||
|
|
||||||
|
let a = frame.area();
|
||||||
|
let xpos = (a.width / 2) - (popup_x / 2);
|
||||||
|
let ypos = (a.height / 2) - (popup_y / 2);
|
||||||
|
let area = Rect::new(xpos, ypos, popup_x, popup_y);
|
||||||
|
|
||||||
|
frame.render_widget(ratatui::widgets::Clear, area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
|
let area = area.inner(Margin::new(1, 1));
|
||||||
|
frame.render_widget(plot, area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_events(&mut self) -> io::Result<()> {
|
fn handle_events(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(plot) = &mut self.plot_popup {
|
||||||
|
if let event::Event::Key(key) = event::read()? {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc => self.plot_popup = None,
|
||||||
|
KeyCode::PrintScreen | KeyCode::Enter => unimplemented!("Generate plot data"),
|
||||||
|
KeyCode::Delete => plot.del_column(),
|
||||||
|
KeyCode::Insert => plot.add_column(),
|
||||||
|
KeyCode::Char(c) => plot.process_key(c),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -381,20 +443,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);
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::app::{
|
|||||||
logic::{
|
logic::{
|
||||||
cell::{CSV_DELIMITER, CellType},
|
cell::{CSV_DELIMITER, CellType},
|
||||||
ctx,
|
ctx,
|
||||||
},
|
}, mode::Mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -91,7 +91,7 @@ impl Grid {
|
|||||||
resolve_values = true;
|
resolve_values = true;
|
||||||
}
|
}
|
||||||
Some(CUSTOM_EXT) => {
|
Some(CUSTOM_EXT) => {
|
||||||
resolve_values = true;
|
resolve_values = false;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
resolve_values = false;
|
resolve_values = false;
|
||||||
@@ -110,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()?;
|
||||||
|
|
||||||
@@ -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?
|
// Do cells hold strings?
|
||||||
#[test]
|
#[test]
|
||||||
fn cell_strings() {
|
fn cell_strings() {
|
||||||
@@ -802,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]
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ mod mode;
|
|||||||
mod error_msg;
|
mod error_msg;
|
||||||
mod screen;
|
mod screen;
|
||||||
mod logic;
|
mod logic;
|
||||||
mod clipboard;
|
mod clipboard;
|
||||||
|
mod plot;
|
||||||
104
src/app/mode.rs
104
src/app/mode.rs
@@ -1,5 +1,8 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cmp::{max, min}, fmt::Display, fs, path::PathBuf
|
cmp::{max, min},
|
||||||
|
fmt::Display,
|
||||||
|
fs,
|
||||||
|
path::PathBuf, process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@@ -12,9 +15,9 @@ use crate::app::{
|
|||||||
app::App,
|
app::App,
|
||||||
error_msg::StatusMessage,
|
error_msg::StatusMessage,
|
||||||
logic::{
|
logic::{
|
||||||
calc::{CSV_EXT, CUSTOM_EXT, LEN},
|
calc::{CSV_EXT, CUSTOM_EXT, Grid, LEN},
|
||||||
cell::CellType,
|
cell::CellType,
|
||||||
},
|
}, plot::Plot,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
@@ -23,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 {
|
||||||
@@ -33,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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,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),
|
||||||
@@ -143,6 +149,84 @@ 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" => {
|
||||||
|
app.plot_popup = Some(Plot::new(0, 1));
|
||||||
|
return;
|
||||||
|
// 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) {
|
||||||
@@ -211,7 +295,13 @@ impl Mode {
|
|||||||
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());
|
||||||
@@ -323,15 +413,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::Insert(_chord) => {}
|
||||||
Mode::Command(_chord) => {}
|
Mode::Command(_chord) => {}
|
||||||
|
Mode::VisualCmd(_pos, _chord) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn chars_to_display(&self, cell: &Option<CellType>) -> u16 {
|
pub fn chars_to_display(&self, cell: &Option<CellType>) -> u16 {
|
||||||
let len = match &self {
|
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 => {
|
Mode::Normal => {
|
||||||
let len = cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default();
|
let len = cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default();
|
||||||
len
|
len
|
||||||
@@ -360,6 +451,7 @@ impl Mode {
|
|||||||
area,
|
area,
|
||||||
),
|
),
|
||||||
Mode::Visual(_) => {}
|
Mode::Visual(_) => {}
|
||||||
|
Mode::VisualCmd(_, editor) => f.render_widget(editor, area),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/app/plot.rs
Normal file
45
src/app/plot.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use ratatui::{layout::{Constraint, Direction, Layout}, widgets::{Paragraph, Widget}};
|
||||||
|
|
||||||
|
pub struct Plot {
|
||||||
|
x: usize,
|
||||||
|
y: Vec<usize>,
|
||||||
|
}
|
||||||
|
impl Plot {
|
||||||
|
pub fn new(x: usize, y: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
x,
|
||||||
|
y: vec![y],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_column(&mut self) {
|
||||||
|
self.y.push(1)
|
||||||
|
}
|
||||||
|
pub fn del_column(&mut self) {
|
||||||
|
self.y.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_key(&mut self, c: char) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &Plot {
|
||||||
|
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) {
|
||||||
|
// plus 1 for x
|
||||||
|
let columns = self.y.len() + 1;
|
||||||
|
|
||||||
|
let mut constraints = Vec::new();
|
||||||
|
for _ in 0..=columns {
|
||||||
|
constraints.push(Constraint::Min(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Paragraph::new("Foobar").render(area, buf);
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(constraints)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let x_space = layout[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
17
template.gnuplot
Normal file
17
template.gnuplot
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
|
replot
|
||||||
Reference in New Issue
Block a user