Compare commits
1 Commits
052828c89c
...
plot-popup
| Author | SHA1 | Date | |
|---|---|---|---|
| dee88c916e |
@@ -1,21 +0,0 @@
|
||||
name: Test Rust project
|
||||
on: [push]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
rust: [stable]
|
||||
|
||||
steps:
|
||||
- uses: https://git.oliveratkinson.net/Oliver/setup-rust-action@master
|
||||
with:
|
||||
rust-version: ${{ matrix.rust }}
|
||||
- uses: actions/checkout@master
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
|
||||
|
||||
34
README.md
34
README.md
@@ -14,13 +14,8 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
|
||||
| `k` | Move up |
|
||||
| `h` | Move left |
|
||||
| `l` | Move right |
|
||||
| `0` | Go to beginning of the row |
|
||||
| `g0` | Go to beginning of the visual row |
|
||||
| `$` | Go to end of the row |
|
||||
| `g$` | Go to end of the visual row |
|
||||
| `gg` | Go to beginning of the column |
|
||||
| `G` | Go to end of column |
|
||||
| `gG` | Go to end of the the visual column |
|
||||
| `0` | Go to beginning of row |
|
||||
| `gg` | Go to beginning of column |
|
||||
| `i`/`a` | Enter insert mode on current cell |
|
||||
| `r` | Enter insert mode on current cell, deleting contents |
|
||||
| `v` | Enter visual mode |
|
||||
@@ -56,15 +51,6 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
|
||||
| `:q` | Quit program |
|
||||
| `:q!` | Quit program, even if the file isn't saved |
|
||||
|
||||
#### Visual mode commands
|
||||
|
||||
These commands operate on selections.
|
||||
|
||||
| Commmand | Description |
|
||||
| - | - |
|
||||
| `:plot` | Plots the current selection to `plot.png` using gnuplot |
|
||||
| `:export <filename>` | Exports the current selection to a new file |
|
||||
|
||||
## Math / Functions
|
||||
|
||||
### Math
|
||||
@@ -82,11 +68,11 @@ These commands operate on selections.
|
||||
|
||||
| Identifier | Arg Qty | Argument Types | Description |
|
||||
| - | - | - | - |
|
||||
| `avg` | ≥ 1 | Numeric | Returns the average of the arguments |
|
||||
| `sum` | ≥ 1 | Numbers | Returns the sum of the arguments |
|
||||
| `xlookup` | 3 | Number/String, Range, Range | Searches for the number/string in the first range, returning the item at the same index in the second range |
|
||||
| `min` | ≥ 1 | Numeric | Returns the minimum of the arguments |
|
||||
| `max` | ≥ 1 | Numeric | Returns the maximum of the arguments |
|
||||
| `avg` | >= 1 | Numeric | Returns the average of the arguments |
|
||||
| `sum` | >= 1 | Numbers | Returns the sum of the arguments |
|
||||
| `xlookup` | 3 | Range, Number/String, Range | Searches for the number/string in the first range, returning the item at the same index in the second range |
|
||||
| `min` | >= 1 | Numeric | Returns the minimum of the arguments |
|
||||
| `max` | >= 1 | Numeric | Returns the maximum of the arguments |
|
||||
| `len` | 1 | String/Tuple | Returns the character length of a string, or the amount of elements in a tuple (not recursively) |
|
||||
| `floor` | 1 | Numeric | Returns the largest integer less than or equal to a number |
|
||||
| `round` | 1 | Numeric | Returns the nearest integer to a number. Rounds half-way cases away from 0.0 |
|
||||
@@ -126,7 +112,7 @@ These commands operate on selections.
|
||||
| `str::to_lowercase` | 1 | String | Returns the lower-case version of the string |
|
||||
| `str::to_uppercase` | 1 | String | Returns the upper-case version of the string |
|
||||
| `str::trim` | 1 | String | Strips whitespace from the start and the end of the string |
|
||||
| `str::from` | ≥ 0 | Any | Returns passed value as string |
|
||||
| `str::from` | >= 0 | Any | Returns passed value as string |
|
||||
| `str::substring` | 3 | String, Int, Int | Returns a substring of the first argument, starting at the second argument and ending at the third argument. If the last argument is omitted, the substring extends to the end of the string |
|
||||
| `bitand` | 2 | Int | Computes the bitwise and of the given integers |
|
||||
| `bitor` | 2 | Int | Computes the bitwise or of the given integers |
|
||||
@@ -146,8 +132,8 @@ If the cell can be a number (parsed into a float) it will be, if not, then if th
|
||||
|
||||
* Cell references move with copy / paste
|
||||
* =A0: Will translate
|
||||
* =$A0: Will translate Y only (numbers)
|
||||
* =A$0: Will translate X only (letters)
|
||||
* =$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)
|
||||
|
||||
@@ -8,18 +8,19 @@ use std::{
|
||||
|
||||
use ratatui::{
|
||||
DefaultTerminal, Frame,
|
||||
crossterm::event,
|
||||
layout::{self, Constraint, Layout, Rect},
|
||||
crossterm::event::{self, KeyCode},
|
||||
layout::{self, Constraint, Layout, Margin, Rect},
|
||||
prelude,
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Paragraph, Widget},
|
||||
widgets::{Block, Borders, Paragraph, Widget},
|
||||
};
|
||||
|
||||
use crate::app::{
|
||||
clipboard::Clipboard,
|
||||
error_msg::StatusMessage,
|
||||
logic::{self, calc::{Grid, get_header_size}, cell::CellType},
|
||||
logic::{calc::Grid, cell::CellType},
|
||||
mode::Mode,
|
||||
plot::Plot,
|
||||
screen::ScreenSpace,
|
||||
};
|
||||
|
||||
@@ -35,6 +36,7 @@ pub struct App {
|
||||
// this could probably be a normal array
|
||||
pub marks: HashMap<char, (usize, usize)>,
|
||||
pub clipboard: Clipboard,
|
||||
pub plot_popup: Option<Plot>,
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
@@ -42,7 +44,7 @@ impl Widget for &App {
|
||||
let (x_max, y_max) = self.screen.how_many_cells_fit_in(&area, &self.vars);
|
||||
|
||||
let is_selected = |x: usize, y: usize| -> bool {
|
||||
if let Mode::Visual((mut x1, mut y1)) | Mode::VisualCmd((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();
|
||||
x1 += 1;
|
||||
y1 += 1;
|
||||
@@ -65,10 +67,6 @@ impl Widget for &App {
|
||||
let mut display = String::new();
|
||||
let mut style = Style::new().fg(Color::White);
|
||||
|
||||
// Custom width for the header of each row
|
||||
let row_header_width = get_header_size() as u16;
|
||||
// ^^ Feels like it oculd be static but evaluating string lens doesn't work at
|
||||
// compile time. Thus cannot be static.
|
||||
let cell_width = self.screen.get_cell_width(&self.vars) as u16;
|
||||
let cell_height = self.screen.get_cell_height(&self.vars) as u16;
|
||||
|
||||
@@ -78,7 +76,7 @@ impl Widget for &App {
|
||||
let mut x_idx: usize = 0;
|
||||
let mut y_idx: usize = 0;
|
||||
if x != 0 {
|
||||
x_idx = x as usize -1 + self.screen.scroll_x();
|
||||
x_idx = x as usize - 1 + self.screen.scroll_x();
|
||||
}
|
||||
if y != 0 {
|
||||
y_idx = y as usize - 1 + self.screen.scroll_y();
|
||||
@@ -90,15 +88,6 @@ impl Widget for &App {
|
||||
let mut should_render = true;
|
||||
let mut suggest_upper_bound = None;
|
||||
|
||||
/// Center the text "99 " -> " 99 "
|
||||
fn center_text(text: &str, avaliable_space: i32) -> String {
|
||||
let margin = avaliable_space - text.len() as i32;
|
||||
let margin = margin/2;
|
||||
let l_margin = (0..margin).into_iter().map(|_| ' ').collect::<String>();
|
||||
let r_margin = (0..(margin-(l_margin.len() as i32))).into_iter().map(|_| ' ').collect::<String>();
|
||||
format!("{l_margin}{text}{r_margin}")
|
||||
}
|
||||
|
||||
match (x == 0, y == 0) {
|
||||
// 0,0 vi mode
|
||||
(true, true) => {
|
||||
@@ -107,7 +96,7 @@ impl Widget for &App {
|
||||
}
|
||||
// row names
|
||||
(true, false) => {
|
||||
display = center_text(&y_idx.to_string(), row_header_width as i32);
|
||||
display = y_idx.to_string();
|
||||
|
||||
let bg = if y_idx == self.grid.cursor().1 {
|
||||
Color::DarkGray
|
||||
@@ -120,7 +109,7 @@ impl Widget for &App {
|
||||
}
|
||||
// column names
|
||||
(false, true) => {
|
||||
display = center_text(&Grid::num_to_char(x_idx), cell_width as i32);
|
||||
display = Grid::num_to_char(x_idx);
|
||||
|
||||
let bg = if x_idx == self.grid.cursor().0 {
|
||||
Color::DarkGray
|
||||
@@ -136,7 +125,6 @@ impl Widget for &App {
|
||||
(false, false) => {
|
||||
match self.grid.get_cell_raw(x_idx, y_idx) {
|
||||
Some(cell) => {
|
||||
// Render in different colors based on type of contents
|
||||
match cell {
|
||||
CellType::Number(c) => display = c.to_string(),
|
||||
CellType::String(s) => {
|
||||
@@ -163,7 +151,6 @@ impl Widget for &App {
|
||||
}
|
||||
}
|
||||
|
||||
// Allow for text in one cell to visually overflow into empty cells
|
||||
suggest_upper_bound = Some(display.len() as u16);
|
||||
// check for cells to the right, see if we should truncate the cell width
|
||||
for i in 1..(display.len() as f32 / cell_width as f32).ceil() as usize {
|
||||
@@ -180,14 +167,11 @@ impl Widget for &App {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't render blank cells
|
||||
None => should_render = false,
|
||||
}
|
||||
|
||||
if is_selected(x.into(), y.into()) {
|
||||
style = style.bg(Color::Blue);
|
||||
// Make it so that cells render when selected. This fixes issue #32
|
||||
should_render = true;
|
||||
}
|
||||
if (x_idx, y_idx) == self.grid.cursor() {
|
||||
should_render = true;
|
||||
@@ -199,21 +183,11 @@ impl Widget for &App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_render {
|
||||
let mut x_off = area.x + (x * cell_width);
|
||||
let x_off = area.x + (x * cell_width);
|
||||
let y_off = area.y + (y * cell_height);
|
||||
|
||||
// Adjust for the fact that the first column
|
||||
// is smaller, since it is just headers
|
||||
if x > 0 {
|
||||
x_off = x_off - (cell_width - row_header_width);
|
||||
}
|
||||
|
||||
// If this is the row header column
|
||||
let area = if x==0 && y != 0 {
|
||||
Rect::new(x_off, y_off, row_header_width, cell_height)
|
||||
} else if let Some(suggestion) = suggest_upper_bound {
|
||||
let area = if let Some(suggestion) = suggest_upper_bound {
|
||||
let max_available_width = area.width - x_off;
|
||||
// draw the biggest cell possible, without going OOB off the screen
|
||||
let width = min(max_available_width, suggestion as u16);
|
||||
@@ -245,6 +219,7 @@ impl App {
|
||||
marks: HashMap::new(),
|
||||
clipboard: Clipboard::new(),
|
||||
file_modified_date: SystemTime::now(),
|
||||
plot_popup: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,9 +318,46 @@ impl App {
|
||||
)),
|
||||
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<()> {
|
||||
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 {
|
||||
Mode::VisualCmd(pos, chord) => match event::read()? {
|
||||
event::Event::Key(key) => match key.code {
|
||||
|
||||
@@ -49,7 +49,6 @@ impl Clipboard {
|
||||
// cursor
|
||||
let (cx, cy) = into.cursor();
|
||||
|
||||
// iterate thru the clipbaord's cells
|
||||
for (x, row) in self.clipboard.iter().enumerate() {
|
||||
for (y, cell) in row.iter().enumerate() {
|
||||
let idx = (x + cx, y + cy);
|
||||
@@ -59,7 +58,7 @@ impl Clipboard {
|
||||
let trans = cell.translate_cell(self.source_cell, into.cursor());
|
||||
into.set_cell_raw(idx, Some(trans));
|
||||
} else {
|
||||
// The cell at this location doesn't exist (empty)
|
||||
// cell doesn't exist, no need to translate
|
||||
into.set_cell_raw::<CellType>(idx, None);
|
||||
}
|
||||
} else {
|
||||
@@ -351,19 +350,3 @@ fn copy_paste_y_locked_var() {
|
||||
let c = app.grid.get_cell("B2").as_ref().expect("Just set it");
|
||||
assert_eq!(c.to_string(), "=B$0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_47() {
|
||||
let mut app = App::new();
|
||||
app.grid.set_cell("A0", 4.to_string());
|
||||
Mode::process_key(&mut app, 'j');
|
||||
|
||||
app.grid.set_cell("A1", "=math::log2(A0)".to_string());
|
||||
app.mode = super::mode::Mode::Chord(Chord::new('y'));
|
||||
Mode::process_key(&mut app, 'y');
|
||||
Mode::process_key(&mut app, 'j');
|
||||
Mode::process_key(&mut app, 'p');
|
||||
|
||||
let a = app.grid.get_cell("A2").as_ref().expect("Should've been set by paste");
|
||||
assert_eq!(a.to_string(), "=math::log2(A1)");
|
||||
}
|
||||
|
||||
@@ -17,12 +17,7 @@ use crate::app::{
|
||||
#[cfg(test)]
|
||||
use crate::app::app::App;
|
||||
|
||||
pub fn get_header_size() -> usize {
|
||||
let row_header_width = LEN.to_string().len();
|
||||
row_header_width
|
||||
}
|
||||
|
||||
pub const LEN: usize = 1001;
|
||||
pub const LEN: usize = 1000;
|
||||
pub const CSV_EXT: &str = "csv";
|
||||
pub const CUSTOM_EXT: &str = "nscim";
|
||||
|
||||
@@ -86,7 +81,7 @@ impl Grid {
|
||||
/// Save file to `path` as a csv. Path with have `csv` appended to it if it
|
||||
/// does not already have the extension.
|
||||
pub fn save_to(&mut self, path: impl Into<PathBuf>) -> std::io::Result<()> {
|
||||
let mut path = path.into();
|
||||
let path = path.into();
|
||||
|
||||
let resolve_values;
|
||||
|
||||
@@ -99,15 +94,13 @@ impl Grid {
|
||||
resolve_values = false;
|
||||
}
|
||||
_ => {
|
||||
// File as an extension but isn't ours.
|
||||
// Save as csv-like
|
||||
resolve_values = true;
|
||||
resolve_values = false;
|
||||
// path.add_extension(CUSTOM_EXT);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
// File has no extension. Save it as our file type
|
||||
resolve_values = false;
|
||||
path.add_extension(CUSTOM_EXT);
|
||||
// path.add_extension(CUSTOM_EXT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,8 +543,8 @@ fn saving_neoscim() {
|
||||
assert_eq!(cell.escaped_csv_string(), "=A10^2");
|
||||
|
||||
// set saving the file
|
||||
let filename = format!("/tmp/file.{CUSTOM_EXT}");
|
||||
app.grid.save_to(&filename).expect("This will only work on linux systems");
|
||||
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");
|
||||
@@ -793,7 +786,7 @@ fn xlookup_function() {
|
||||
grid.set_cell("A1", "Sarah".to_string());
|
||||
grid.set_cell("C0", 31.);
|
||||
grid.set_cell("C1", 41.);
|
||||
grid.set_cell("B0", "=xlookup(\"Bobby\",A:A,C:C)".to_string());
|
||||
grid.set_cell("B0", "=xlookup(A:A,\"Bobby\",C:C)".to_string());
|
||||
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
||||
let res = grid.evaluate(&cell.to_string());
|
||||
assert!(res.is_ok());
|
||||
|
||||
@@ -54,20 +54,17 @@ impl CellType {
|
||||
}
|
||||
}
|
||||
|
||||
/// `replace_fn` takes the string, the old value, and then the new value.
|
||||
/// It can be thought of as `echo $1 | sed s/$2/$3/g`
|
||||
pub fn custom_translate_cell(&self, from: (usize, usize), to: (usize, usize), replace_fn: impl Fn(&str, &str, &str) -> String) -> CellType {
|
||||
match self {
|
||||
// don't translate non-equations
|
||||
CellType::Number(_) | CellType::String(_) => return self.clone(),
|
||||
CellType::Equation(eq) => {
|
||||
// Populate the context
|
||||
// extract all the variables
|
||||
let ctx = ExtractionContext::new();
|
||||
let _ = eval_with_context(eq, &ctx);
|
||||
|
||||
let mut equation = eq.clone();
|
||||
let mut rolling = eq.clone();
|
||||
// translate standard vars A0 -> A1
|
||||
// extract all the variables
|
||||
for old_var in ctx.dump_vars() {
|
||||
let mut lock_x = false;
|
||||
let mut lock_y = false;
|
||||
@@ -79,18 +76,20 @@ impl CellType {
|
||||
if locations[0] == 0 {
|
||||
// locking the X axis (A,B,C...)
|
||||
lock_x = true;
|
||||
} else {
|
||||
} 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 => {
|
||||
// Ignore this variable all together, effectively lockng X & Y
|
||||
continue;
|
||||
// YOLO, lock both X & Y
|
||||
continue; // just pretend you never even saw this var
|
||||
}
|
||||
_ => {
|
||||
// There are 0 or >2 "$" in this string.
|
||||
//
|
||||
// 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.
|
||||
@@ -99,7 +98,6 @@ impl CellType {
|
||||
}
|
||||
|
||||
if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) {
|
||||
// Use i32s instead of usize in case of negative numbers
|
||||
let (x1, y1) = from;
|
||||
let x1 = x1 as i32;
|
||||
let y1 = y1 as i32;
|
||||
@@ -122,7 +120,6 @@ impl CellType {
|
||||
let alpha = Grid::num_to_char(dest_x);
|
||||
let alpha = alpha.trim();
|
||||
|
||||
// Persist the "$" locking
|
||||
let new_var = if lock_x {
|
||||
format!("${alpha}{dest_y}")
|
||||
} else if lock_y {
|
||||
@@ -131,14 +128,15 @@ impl CellType {
|
||||
format!("{alpha}{dest_y}")
|
||||
};
|
||||
|
||||
|
||||
// swap out vars
|
||||
equation = replace_fn(&equation, &old_var, &new_var);
|
||||
rolling = replace_fn(&rolling, &old_var, &new_var);
|
||||
// rolling = rolling.replace(&old_var, &new_var);
|
||||
} else {
|
||||
// why you coping invalid stuff, nerd?
|
||||
}
|
||||
}
|
||||
return equation.into();
|
||||
return rolling.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,13 +104,13 @@ impl<'a> CallbackContext<'a> {
|
||||
functions.insert(
|
||||
"xlookup".to_string(),
|
||||
Function::new(|arg| {
|
||||
let expected = vec![ValueType::String, ValueType::Tuple, ValueType::Tuple];
|
||||
let expected = vec![ValueType::Tuple, ValueType::String, ValueType::Tuple];
|
||||
if arg.is_tuple() {
|
||||
let args = arg.as_tuple()?;
|
||||
|
||||
if args.len() == 3 {
|
||||
let lookup_value = &args[0];
|
||||
let lookup_array = &args[1];
|
||||
let lookup_array = &args[0];
|
||||
let lookup_value = &args[1];
|
||||
let return_array = &args[2];
|
||||
|
||||
if lookup_array.is_tuple() && return_array.is_tuple() {
|
||||
|
||||
@@ -4,3 +4,4 @@ mod error_msg;
|
||||
mod screen;
|
||||
mod logic;
|
||||
mod clipboard;
|
||||
mod plot;
|
||||
@@ -17,7 +17,7 @@ use crate::app::{
|
||||
logic::{
|
||||
calc::{CSV_EXT, CUSTOM_EXT, Grid, LEN},
|
||||
cell::CellType,
|
||||
},
|
||||
}, plot::Plot,
|
||||
};
|
||||
|
||||
pub enum Mode {
|
||||
@@ -70,11 +70,25 @@ impl Mode {
|
||||
"w" => {
|
||||
// first try the passed argument as file
|
||||
if let Some(arg) = args.get(1) {
|
||||
let path: PathBuf = arg.into();
|
||||
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 we are writing to exists, since
|
||||
// this code path already knows that we are writing to a new file.
|
||||
// We might be accidentally overwriting something else.
|
||||
// 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}"));
|
||||
@@ -184,6 +198,8 @@ impl Mode {
|
||||
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
|
||||
@@ -202,16 +218,10 @@ impl Mode {
|
||||
let s = s.replace("$OUTPUT", "/tmp/output.png");
|
||||
let _ = fs::write("/tmp/plot.p", s);
|
||||
|
||||
let cmd_res= Command::new("gnuplot").arg("/tmp/plot.p").output();
|
||||
if let Err(err) = cmd_res {
|
||||
match err.kind() {
|
||||
std::io::ErrorKind::NotFound => app.msg = StatusMessage::error("Error - Is gnuplot installed?"),
|
||||
_ => app.msg = StatusMessage::error(format!("{err}")),
|
||||
};
|
||||
} else {
|
||||
let _ = Command::new("gnuplot").arg("/tmp/plot.p").output();
|
||||
let _ = fs::copy("/tmp/output.png", output_filename);
|
||||
app.msg = StatusMessage::info(format!("Created {output_filename}. Artifacts are in /tmp"));
|
||||
}
|
||||
|
||||
app.msg = StatusMessage::info("Wrote gnuplot data to /tmp");
|
||||
app.mode = Mode::Normal
|
||||
}
|
||||
_ => {}
|
||||
@@ -252,18 +262,6 @@ impl Mode {
|
||||
app.grid.mv_cursor_to(0, y);
|
||||
return;
|
||||
}
|
||||
// Go to end of row
|
||||
'$' => {
|
||||
let (_, y) = app.grid.cursor();
|
||||
app.grid.mv_cursor_to(super::logic::calc::LEN, y);
|
||||
return;
|
||||
}
|
||||
// Go to bottom of column
|
||||
'G' => {
|
||||
let (x, _) = app.grid.cursor();
|
||||
app.grid.mv_cursor_to(x, super::logic::calc::LEN,);
|
||||
return;
|
||||
}
|
||||
// edit cell
|
||||
'i' | 'a' => {
|
||||
let (x, y) = app.grid.cursor();
|
||||
@@ -377,34 +375,6 @@ impl Mode {
|
||||
app.grid.mv_cursor_to(x, 0);
|
||||
app.mode = Mode::Normal;
|
||||
}
|
||||
// Go to the bottom of the current window
|
||||
("g", 'G') => {
|
||||
let (x, _) = app.grid.cursor();
|
||||
let (_, y_height) = app.screen.get_screen_size(&app.vars);
|
||||
let y_origin = app.screen.scroll_y();
|
||||
|
||||
app.grid.mv_cursor_to(x, y_origin+y_height);
|
||||
app.mode = Mode::Normal;
|
||||
return;
|
||||
}
|
||||
// Go to the right edge of the current window
|
||||
("g", '$') => {
|
||||
let (_, y) = app.grid.cursor();
|
||||
let (x_width, _) = app.screen.get_screen_size(&app.vars);
|
||||
let x_origin = app.screen.scroll_x();
|
||||
|
||||
app.grid.mv_cursor_to(x_origin+x_width, y);
|
||||
app.mode = Mode::Normal;
|
||||
}
|
||||
// Go to the left edge of the current window
|
||||
("g", '0') => {
|
||||
let (_, y) = app.grid.cursor();
|
||||
let x_origin = app.screen.scroll_x();
|
||||
|
||||
app.grid.mv_cursor_to(x_origin, y);
|
||||
app.mode = Mode::Normal;
|
||||
return;
|
||||
}
|
||||
// center screen to cursor
|
||||
("z", 'z') => {
|
||||
app.screen.center_x(app.grid.cursor(), &app.vars);
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,12 @@ use std::{collections::HashMap, sync::RwLock};
|
||||
|
||||
use ratatui::prelude;
|
||||
|
||||
use crate::app::logic::calc::{self, LEN};
|
||||
use crate::app::logic::calc::LEN;
|
||||
|
||||
pub struct ScreenSpace {
|
||||
/// This is measured in cells.
|
||||
/// This is the top-left cell index.
|
||||
/// This is measured in cells
|
||||
scroll: (usize, usize),
|
||||
/// In chars
|
||||
default_cell_len: usize,
|
||||
/// In chars
|
||||
default_cell_hight: usize,
|
||||
/// This is measured in chars
|
||||
last_seen_screen_size: RwLock<(usize, usize)>
|
||||
@@ -20,49 +17,37 @@ impl ScreenSpace {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
scroll: (0, 0),
|
||||
default_cell_len: 9,
|
||||
default_cell_len: 10,
|
||||
default_cell_hight: 1,
|
||||
last_seen_screen_size: RwLock::new((0,0))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_x(&mut self, (cursor_x, _): (usize, usize), vars: &HashMap<String, String>) {
|
||||
let (x_cells, _) = self.get_screen_size(vars);
|
||||
if let Ok(screen_size) = self.last_seen_screen_size.read() {
|
||||
let x_cells = (screen_size.0 / self.get_cell_width(vars) as usize) -2;
|
||||
let x_center = self.scroll_x() + (x_cells/2);
|
||||
|
||||
let delta = cursor_x as isize - x_center as isize;
|
||||
self.scroll.0 = self.scroll.0.saturating_add_signed(delta);
|
||||
}
|
||||
|
||||
}
|
||||
pub fn center_y(&mut self, (_, cursor_y): (usize, usize), vars: &HashMap<String, String>) {
|
||||
let (_, y_cells) = self.get_screen_size(vars);
|
||||
if let Ok(screen_size) = self.last_seen_screen_size.read() {
|
||||
let y_cells = (screen_size.1 / self.get_cell_height(vars) as usize) -2;
|
||||
let y_center = self.scroll_y() + (y_cells/2);
|
||||
|
||||
let delta = cursor_y as isize - y_center as isize;
|
||||
self.scroll.1 = self.scroll.1.saturating_add_signed(delta);
|
||||
}
|
||||
}
|
||||
|
||||
/// In chars
|
||||
pub fn get_screen_size(&self, vars: &HashMap<String, String>) -> (usize, usize) {
|
||||
pub fn scroll_based_on_cursor_location(&mut self, (cursor_x, cursor_y): (usize, usize), vars: &HashMap<String, String>) {
|
||||
if let Ok(screen_size) = self.last_seen_screen_size.read() {
|
||||
// ======= X =======
|
||||
// screen seems to be 2 cells smaller than it should be
|
||||
// this is probably related to issue #6
|
||||
let x_cells = (screen_size.0 / self.get_cell_width(vars) as usize) -2;
|
||||
// ======= Y =======
|
||||
// screen seems to be 2 cells smaller than it should be
|
||||
// this is probably related to issue #6
|
||||
let y_cells = (screen_size.1 / self.get_cell_height(vars) as usize) -2;
|
||||
(x_cells,y_cells)
|
||||
} else {
|
||||
(0,0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_based_on_cursor_location(&mut self, (cursor_x, cursor_y): (usize, usize), vars: &HashMap<String, String>) {
|
||||
let (x_cells, y_cells) = self.get_screen_size(vars);
|
||||
|
||||
|
||||
let lower_x = self.scroll_x();
|
||||
let upper_x = self.scroll_x() + x_cells;
|
||||
|
||||
@@ -75,7 +60,10 @@ impl ScreenSpace {
|
||||
self.scroll.0 = self.scroll.0.saturating_add(delta);
|
||||
}
|
||||
|
||||
|
||||
// ======= Y =======
|
||||
// screen seems to be 2 cells smaller than it should be
|
||||
// this is probably related to issue #6
|
||||
let y_cells = (screen_size.1 / self.get_cell_height(vars) as usize) -2;
|
||||
let lower_y = self.scroll_y();
|
||||
let upper_y = self.scroll_y() + y_cells;
|
||||
|
||||
@@ -89,15 +77,14 @@ impl ScreenSpace {
|
||||
self.scroll.1 = self.scroll.1.saturating_add(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_x(&self) -> usize {
|
||||
self.scroll.0
|
||||
}
|
||||
|
||||
pub fn scroll_y(&self) -> usize{
|
||||
self.scroll.1
|
||||
}
|
||||
|
||||
pub fn get_cell_height(&self, vars: &HashMap<String, String>) -> usize {
|
||||
if let Some(h) = vars.get("height") {
|
||||
if let Ok(p) = h.parse::<usize>() {
|
||||
@@ -106,7 +93,6 @@ impl ScreenSpace {
|
||||
}
|
||||
return self.default_cell_hight
|
||||
}
|
||||
|
||||
pub fn get_cell_width(&self, vars: &HashMap<String, String>) -> usize {
|
||||
if let Some(h) = vars.get("length") {
|
||||
if let Ok(p) = h.parse::<usize>() {
|
||||
@@ -114,34 +100,19 @@ impl ScreenSpace {
|
||||
}
|
||||
}
|
||||
self.default_cell_len
|
||||
|
||||
}
|
||||
|
||||
pub fn how_many_cells_fit_in(&self, area: &prelude::Rect, vars: &HashMap<String, String>) -> (u16, u16) {
|
||||
if let Ok(mut l) = self.last_seen_screen_size.write() {
|
||||
l.0 = area.width as usize;
|
||||
l.1 = area.height as usize;
|
||||
}
|
||||
|
||||
// let width = (area.width as usize + calc::get_header_size() -1) / self.get_cell_width(vars);
|
||||
let width = area.width as usize / self.get_cell_width(vars);
|
||||
let height = area.height as usize / self.get_cell_height(vars);
|
||||
|
||||
let x_max =
|
||||
if width > LEN {
|
||||
LEN - 1
|
||||
} else {
|
||||
width
|
||||
};
|
||||
if area.width as usize / self.get_cell_width(vars) > LEN { LEN - 1 } else { area.width as usize / self.get_cell_width(vars)};
|
||||
let y_max =
|
||||
if height > LEN {
|
||||
LEN - 1
|
||||
} else {
|
||||
height
|
||||
};
|
||||
if area.height as usize / self.get_cell_height(vars) > LEN { LEN - 1 } else { area.height as usize / self.get_cell_height(vars)};
|
||||
(x_max as u16, y_max as u16)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -14,5 +14,4 @@ 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