Compare commits

..

3 Commits

Author SHA1 Message Date
dee88c916e wip 2025-11-19 09:41:55 -07:00
0c527bb3cb add gnuplot plotting 2025-11-18 13:02:29 -07:00
6ca21b407d close #36 2025-11-18 12:33:34 -07:00
6 changed files with 179 additions and 22 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/target /target
/.vscode /.vscode
/*.csv /*.csv
/*.nscim /*.nscim
/plot.png

View File

@@ -8,11 +8,11 @@ use std::{
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::{
@@ -20,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,
}; };
@@ -35,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 {
@@ -42,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;
@@ -80,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);
@@ -171,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);
@@ -217,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,
} }
} }
@@ -315,9 +318,46 @@ 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()? { Mode::VisualCmd(pos, chord) => match event::read()? {
event::Event::Key(key) => match key.code { event::Event::Key(key) => match key.code {

View File

@@ -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;

View File

@@ -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 {
@@ -146,14 +149,81 @@ impl Mode {
_ => {} _ => {}
} }
} }
if let Mode::VisualCmd(pos, editor ) = &mut app.mode { if let Mode::VisualCmd(pos, editor) = &mut app.mode {
let cmd = &editor.as_string()[1..]; let cmd = &editor.as_string()[1..];
let args = cmd.split_ascii_whitespace().collect::<Vec<&str>>(); let args = cmd.split_ascii_whitespace().collect::<Vec<&str>>();
if args.is_empty() { if args.is_empty() {
return; 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] { match args[0] {
"foo" => {} "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
}
_ => {} _ => {}
} }
} }
@@ -343,10 +413,10 @@ impl Mode {
} }
} }
} }
// Keys are process in the handle_event method in App for these // 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 ) => {} Mode::VisualCmd(_pos, _chord) => {}
} }
} }

45
src/app/plot.rs Normal file
View 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];
}
}

View File

@@ -1,10 +1,10 @@
datafile = 'data.csv' datafile = '$FILE'
set datafile separator ',' set datafile separator ','
set title 'Probably the filename' set title '$TITLE'
set key autotitle columnhead set key autotitle columnhead
set xlabel "x" set xlabel "$XLABEL"
set ylabel "y" set ylabel "$YLABEL"
set style line 1 linewidth 2 linecolor 1 pointtype 7 pointsize 1.5 set style line 1 linewidth 2 linecolor 1 pointtype 7 pointsize 1.5
@@ -12,6 +12,6 @@ set autoscale
set grid set grid
set term png size 1280, 720 set term png size 1280, 720
set output 'output.png' set output '$OUTPUT'
plot datafile using 1:2 with linespoints linestyle 1 plot datafile using 1:2 with linespoints linestyle 1
replot replot