good start to #16

This commit is contained in:
2025-11-13 09:15:51 -07:00
parent 1288ac98d8
commit 7ab6b23f73
5 changed files with 288 additions and 62 deletions

View File

@@ -1,24 +1,13 @@
use std::{ use std::{
cmp::{max, min}, cmp::{max, min}, collections::HashMap, io, path::PathBuf
collections::HashMap,
io,
path::PathBuf,
}; };
use ratatui::{ use ratatui::{
DefaultTerminal, Frame, DefaultTerminal, Frame, crossterm::event, layout::{self, Constraint, Layout, Rect}, prelude, style::{Color, Modifier, Style}, widgets::{Paragraph, Widget}
crossterm::event,
layout::{self, Constraint, Layout, Rect},
prelude,
style::{Color, Modifier, Style},
widgets::{Paragraph, Widget},
}; };
use crate::app::{ use crate::app::{
error_msg::ErrorMessage, clipboard::Clipboard, error_msg::ErrorMessage, logic::calc::{CellType, Grid}, mode::Mode, screen::ScreenSpace
logic::calc::{CellType, Grid},
mode::Mode,
screen::ScreenSpace,
}; };
pub struct App { pub struct App {
@@ -31,6 +20,7 @@ pub struct App {
pub screen: ScreenSpace, pub screen: ScreenSpace,
// 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,
} }
impl Widget for &App { impl Widget for &App {
@@ -39,7 +29,7 @@ impl Widget for &App {
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)) = self.mode {
let (mut x2, mut y2) = self.grid.selected_cell; let (mut x2, mut y2) = self.grid.cursor();
x1 += 1; x1 += 1;
y1 += 1; y1 += 1;
x2 += 1; x2 += 1;
@@ -96,7 +86,7 @@ impl Widget for &App {
(true, false) => { (true, false) => {
display = y_idx.to_string(); display = y_idx.to_string();
let bg = if y_idx == self.grid.selected_cell.1 { let bg = if y_idx == self.grid.cursor().1 {
Color::DarkGray Color::DarkGray
} else if y_idx % 2 == 0 { } else if y_idx % 2 == 0 {
ORANGE1 ORANGE1
@@ -109,7 +99,7 @@ impl Widget for &App {
(false, true) => { (false, true) => {
display = Grid::num_to_char(x_idx); display = Grid::num_to_char(x_idx);
let bg = if x_idx == self.grid.selected_cell.0 { let bg = if x_idx == self.grid.cursor().0 {
Color::DarkGray Color::DarkGray
} else if x_idx % 2 == 0 { } else if x_idx % 2 == 0 {
ORANGE1 ORANGE1
@@ -157,7 +147,7 @@ impl Widget for &App {
} }
None => should_render = false, None => should_render = false,
} }
if (x_idx, y_idx) == self.grid.selected_cell { 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);
// modify the style of the cell you are editing // modify the style of the cell you are editing
@@ -201,6 +191,7 @@ impl App {
vars: HashMap::new(), vars: HashMap::new(),
screen: ScreenSpace::new(), screen: ScreenSpace::new(),
marks: HashMap::new(), marks: HashMap::new(),
clipboard: Clipboard::new(),
} }
} }
@@ -231,7 +222,7 @@ impl App {
let len = match &self.mode { let len = match &self.mode {
Mode::Insert(edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(), Mode::Insert(edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(),
Mode::Normal => { Mode::Normal => {
let (x, y) = self.grid.selected_cell; 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(); let cell = self.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string().len()).unwrap_or_default();
cell cell
} }
@@ -259,7 +250,7 @@ impl App {
Mode::Chord(chord) => frame.render_widget(chord, cmd_line_left), Mode::Chord(chord) => frame.render_widget(chord, cmd_line_left),
Mode::Normal => frame.render_widget( Mode::Normal => frame.render_widget(
Paragraph::new({ Paragraph::new({
let (x, y) = self.grid.selected_cell; 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(); let cell = self.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or_default();
cell cell
}), }),
@@ -274,7 +265,7 @@ impl App {
frame.render_widget( frame.render_widget(
Paragraph::new(format!( Paragraph::new(format!(
"x/w y/h: cursor{:?} scroll({}, {}) cell({}, {}) screen({}, {}) len{len}", "x/w y/h: cursor{:?} scroll({}, {}) cell({}, {}) screen({}, {}) len{len}",
self.grid.selected_cell, self.grid.cursor(),
self.screen.scroll_x(), self.screen.scroll_x(),
self.screen.scroll_y(), self.screen.scroll_y(),
self.screen.get_cell_width(&self.vars), self.screen.get_cell_width(&self.vars),
@@ -312,13 +303,13 @@ impl App {
// try to insert as a float // try to insert as a float
if let Ok(v) = v.parse::<f64>() { if let Ok(v) = v.parse::<f64>() {
self.grid.set_cell_raw(self.grid.selected_cell, Some(v)); self.grid.set_cell_raw(self.grid.cursor(), Some(v));
} else { } else {
// if you can't, then insert as a string // if you can't, then insert as a string
if !v.is_empty() { if !v.is_empty() {
self.grid.set_cell_raw(self.grid.selected_cell, Some(v)); self.grid.set_cell_raw(self.grid.cursor(), Some(v));
} else { } else {
self.grid.set_cell_raw::<CellType>(self.grid.selected_cell, None); self.grid.set_cell_raw::<CellType>(self.grid.cursor(), None);
} }
} }
@@ -378,7 +369,7 @@ impl App {
} }
// make sure cursor is inside window // make sure cursor is inside window
self.screen.scroll_based_on_cursor_location(self.grid.selected_cell, &self.vars); self.screen.scroll_based_on_cursor_location(self.grid.cursor(), &self.vars);
Ok(()) Ok(())
} }

174
src/app/clipboard.rs Normal file
View File

@@ -0,0 +1,174 @@
use std::cmp::{max, min};
use crate::app::logic::calc::{CellType, Grid};
#[cfg(test)]
use crate::app::{
app::App, mode::{Chord, Mode}
};
pub struct Clipboard {
// could this just be a grid?
clipboard: Vec<Vec<Option<CellType>>>,
// top_left_cell: (usize, usize),
last_paste_cell: (usize, usize),
momentum: (i32, i32),
}
impl Clipboard {
pub fn new() -> Self {
Self {
clipboard: Vec::new(),
last_paste_cell: (0, 0),
momentum: (0, 1),
}
}
/// After pasting you gain momentum which can be used to
/// to move the cursor in the same direction for the next
/// paste.
pub fn momentum(&self) -> (i32, i32) {
// normalize to (-1,-1) to (1,1)
let (mx, my) = self.momentum;
let x = min(mx, 1);
let x = max(x, -1);
let y = min(my, 1);
let y = max(y, -1);
// prevent diagonal momentum
if y != 0 {
(0, y)
} else {
(x,0)
}
}
pub fn paste(&mut self, into: &mut Grid) {
// cursor
let (cx, cy) = into.cursor();
for (x, row) in self.clipboard.iter().enumerate() {
for (y, cell) in row.iter().enumerate() {
into.set_cell_raw((x + cx, y + cy), cell.clone());
}
}
let (lx, ly) = self.last_paste_cell;
self.momentum = (cx as i32 - lx as i32, cy as i32 - ly as i32);
self.last_paste_cell = (cx, cy);
}
/// Clones data from Grid into self.
/// Start and end don't have to be sorted in any sort of way. The function works with
/// any two points.
pub fn clipboard_copy(&mut self, start: (usize, usize), end: (usize, usize), from: &Grid) {
let (x1, y1) = start;
let (x2, y2) = end;
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) };
// size the clipboard appropriately
self.clipboard.clear();
// clone data into clipboard
for x in low_x..=hi_x {
let mut col = Vec::new();
for y in low_y..=hi_y {
let a = from.get_cell_raw(x, y);
col.push(a.clone());
}
self.clipboard.push(col);
}
self.last_paste_cell = (low_x, low_y);
}
}
#[test]
fn copy_paste() {
let mut app = App::new();
app.grid.set_cell("A0", "hello".to_string());
app.grid.mv_cursor_to(0, 0);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
// yy will have set mode back to normal at this point
assert_eq!(app.clipboard.clipboard.len(), 1);
assert!(app.clipboard.clipboard[0][0].as_ref().is_some_and(|c| c.to_string() == "hello"));
app.grid.mv_cursor_to(1, 1);
Mode::process_key(&mut app, 'p');
let a = app.grid.get_cell("B1").as_ref().expect("Should've been set by paste");
assert_eq!(a.to_string(), "hello");
}
#[test]
fn momentum_y_pos() {
let mut app = App::new();
app.grid.set_cell("A0", "hello".to_string());
app.grid.mv_cursor_to(0, 0);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
// yy will have set mode back to normal at this point
app.grid.mv_cursor_to(0, 1);
Mode::process_key(&mut app, 'p');
assert_eq!(app.clipboard.momentum(), (0,1));
}
#[test]
fn momentum_y_neg() {
let mut app = App::new();
app.grid.set_cell("A1", "hello".to_string());
app.grid.mv_cursor_to(0, 1);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
// yy will have set mode back to normal at this point
app.grid.mv_cursor_to(0, 0);
Mode::process_key(&mut app, 'p');
assert_eq!(app.clipboard.momentum(), (0,-1));
}
#[test]
fn momentum_x_pos() {
let mut app = App::new();
app.grid.set_cell("A0", "hello".to_string());
app.grid.mv_cursor_to(0, 0);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
// yy will have set mode back to normal at this point
app.grid.mv_cursor_to(1, 0);
Mode::process_key(&mut app, 'p');
assert_eq!(app.clipboard.momentum(), (1,0));
}
#[test]
fn momentum_x_neg() {
let mut app = App::new();
app.grid.set_cell("B0", "hello".to_string());
app.grid.mv_cursor_to(1, 0);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
// yy will have set mode back to normal at this point
app.grid.mv_cursor_to(0, 0);
Mode::process_key(&mut app, 'p');
assert_eq!(app.clipboard.momentum(), (-1,0));
}

View File

@@ -7,7 +7,7 @@ use std::{
use evalexpr::*; use evalexpr::*;
use crate::app::logic::ctx; use crate::app::{app::App, logic::ctx};
pub const LEN: usize = 1000; pub const LEN: usize = 1000;
@@ -19,7 +19,7 @@ pub struct Grid {
// ... // ...
cells: Vec<Vec<Option<CellType>>>, cells: Vec<Vec<Option<CellType>>>,
/// (X, Y) /// (X, Y)
pub selected_cell: (usize, usize), selected_cell: (usize, usize),
/// Have unsaved modifications been made? /// Have unsaved modifications been made?
dirty: bool, dirty: bool,
} }
@@ -34,6 +34,14 @@ const CSV_DELIMITER: char = ',';
const CSV_ESCAPE: char = '"'; const CSV_ESCAPE: char = '"';
impl Grid { impl Grid {
pub fn cursor(&self) -> (usize, usize) {
self.selected_cell
}
pub fn mv_cursor_to(&mut self, x: usize, y: usize) {
self.selected_cell = (x,y)
}
pub fn needs_to_be_saved(&self) -> bool { pub fn needs_to_be_saved(&self) -> bool {
self.dirty self.dirty
} }
@@ -311,7 +319,7 @@ impl Grid {
/// Helper for tests /// Helper for tests
#[cfg(test)] #[cfg(test)]
fn set_cell<T: Into<CellType>>(&mut self, cell_id: &str, val: T) { pub fn set_cell<T: Into<CellType>>(&mut self, cell_id: &str, val: T) {
if let Some(loc) = Self::parse_to_idx(cell_id) { if let Some(loc) = Self::parse_to_idx(cell_id) {
self.set_cell_raw(loc, Some(val)); self.set_cell_raw(loc, Some(val));
} }
@@ -365,7 +373,7 @@ impl Default for Grid {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum CellType { pub enum CellType {
Number(f64), Number(f64),
String(String), String(String),
@@ -710,5 +718,16 @@ fn ranges() {
#[test] #[test]
fn recursive_ranges() { fn recursive_ranges() {
// recursive ranges causes weird behavior // recursive ranges causes weird behavior
todo!(); // todo!();
}
#[test]
fn cursor_fns() {
// surprisingly, this test was needed
let mut app = App::new();
let c = app.grid.cursor();
assert_eq!(c, (0,0));
app.grid.mv_cursor_to(1, 0);
assert_eq!(app.grid.cursor(), (1,0));
} }

View File

@@ -3,3 +3,4 @@ mod mode;
mod error_msg; mod error_msg;
mod screen; mod screen;
mod logic; mod logic;
mod clipboard;

View File

@@ -116,31 +116,36 @@ impl Mode {
match key { match key {
// < // <
'h' => { 'h' => {
app.grid.selected_cell.0 = app.grid.selected_cell.0.saturating_sub(1); let (x, y) = app.grid.cursor();
app.grid.mv_cursor_to(x.saturating_sub(1), y);
return; return;
} }
// v // v
'j' => { 'j' => {
app.grid.selected_cell.1 = min(app.grid.selected_cell.1.saturating_add(1), LEN - 1); let (x, y) = app.grid.cursor();
app.grid.mv_cursor_to(x, min(y.saturating_add(1), LEN - 1));
return; return;
} }
// ^ // ^
'k' => { 'k' => {
app.grid.selected_cell.1 = app.grid.selected_cell.1.saturating_sub(1); let (x, y) = app.grid.cursor();
app.grid.mv_cursor_to(x, y.saturating_sub(1));
return; return;
} }
// > // >
'l' => { 'l' => {
app.grid.selected_cell.0 = min(app.grid.selected_cell.0.saturating_add(1), LEN - 1); let (x, y) = app.grid.cursor();
app.grid.mv_cursor_to(min(x.saturating_add(1), LEN - 1), y);
return; return;
} }
'0' => { '0' => {
app.grid.selected_cell.0 = 0; let (_, y) = app.grid.cursor();
app.grid.mv_cursor_to(0, y);
return; return;
} }
// edit cell // edit cell
'i' | 'a' => { 'i' | 'a' => {
let (x, y) = app.grid.selected_cell; let (x, y) = app.grid.cursor();
let val = app.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or(String::new()); let val = app.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or(String::new());
@@ -154,8 +159,15 @@ impl Mode {
'A' => { /* insert col after */ } 'A' => { /* insert col after */ }
'o' => { /* insert row below */ } 'o' => { /* insert row below */ }
'O' => { /* insert row above */ } 'O' => { /* insert row above */ }
'v' => app.mode = Mode::Visual(app.grid.selected_cell), 'v' => app.mode = Mode::Visual(app.grid.cursor()),
':' => app.mode = Mode::Command(Chord::new(':')), ':' => app.mode = Mode::Command(Chord::new(':')),
'p' => {
app.clipboard.paste(&mut app.grid);
let (cx, cy) = app.grid.cursor();
let (mx, my) = app.clipboard.momentum();
app.grid.mv_cursor_to((cx as i32 + mx) as usize, (cy as i32 + my) as usize);
return;
}
// loose chars will put you into chord mode // loose chars will put you into chord mode
c => { c => {
if let Mode::Normal = app.mode { if let Mode::Normal = app.mode {
@@ -165,12 +177,13 @@ impl Mode {
} }
if let Mode::Visual((x1, y1)) = app.mode { if let Mode::Visual((x1, y1)) = app.mode {
// TODO visual copy, paste, etc // TODO visual copy, paste, etc
let (x2, y2) = app.grid.selected_cell; let (x2, y2) = app.grid.cursor();
let (low_x, hi_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) }; 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 (low_y, hi_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
if key == 'd' { match key {
'd' => {
for x in low_x..=hi_x { for x in low_x..=hi_x {
for y in low_y..=hi_y { for y in low_y..=hi_y {
app.grid.set_cell_raw::<CellType>((x, y), None); app.grid.set_cell_raw::<CellType>((x, y), None);
@@ -178,6 +191,11 @@ impl Mode {
} }
app.mode = Mode::Normal app.mode = Mode::Normal
} }
'y' => {
app.clipboard.clipboard_copy((x1, y1), (x2, y2), &app.grid);
}
_ => {}
}
} }
} }
Mode::Chord(chord) => { Mode::Chord(chord) => {
@@ -194,8 +212,8 @@ impl Mode {
// For chords that can take a numeric input // For chords that can take a numeric input
Ok(num) => match key { Ok(num) => match key {
'G' => { 'G' => {
let sel = app.grid.selected_cell; let (x, _) = app.grid.cursor();
app.grid.selected_cell = (sel.0, num); app.grid.mv_cursor_to(x, num);
app.mode = Mode::Normal; app.mode = Mode::Normal;
} }
_ => { _ => {
@@ -213,33 +231,40 @@ impl Mode {
match (&c[..c.len() - 1], key) { match (&c[..c.len() - 1], key) {
// delete cell under cursor // delete cell under cursor
("d", ' ') | ("d", 'w') => { ("d", ' ') | ("d", 'w') => {
let loc = app.grid.selected_cell; let loc = app.grid.cursor();
app.grid.set_cell_raw::<CellType>(loc, None); app.grid.set_cell_raw::<CellType>(loc, None);
app.mode = Mode::Normal; app.mode = Mode::Normal;
} }
// go to top of row // go to top of row
("g", 'g') => { ("g", 'g') => {
app.grid.selected_cell.1 = 0; let (x, _) = app.grid.cursor();
app.grid.mv_cursor_to(x, 0);
app.mode = Mode::Normal; app.mode = Mode::Normal;
} }
// center screen to cursor // center screen to cursor
("z", 'z') => { ("z", 'z') => {
app.screen.center_x(app.grid.selected_cell, &app.vars); app.screen.center_x(app.grid.cursor(), &app.vars);
app.screen.center_y(app.grid.selected_cell, &app.vars); app.screen.center_y(app.grid.cursor(), &app.vars);
app.mode = Mode::Normal; app.mode = Mode::Normal;
} }
// mark cell // mark cell
("m", i) => { ("m", i) => {
app.marks.insert(i, app.grid.selected_cell); app.marks.insert(i, app.grid.cursor());
app.mode = Mode::Normal; app.mode = Mode::Normal;
} }
// goto marked cell // goto marked cell
("'", i) => { ("'", i) => {
if let Some(coords) = app.marks.get(&i) { if let Some((cx, cy)) = app.marks.get(&i) {
app.grid.selected_cell = *coords; app.grid.mv_cursor_to(*cx, *cy);
} }
app.mode = Mode::Normal; app.mode = Mode::Normal;
} }
// copy 1 cell
("y", 'y') => {
let point = app.grid.cursor();
app.clipboard.clipboard_copy(point, point, &app.grid);
app.mode = Mode::Normal;
}
_ => {} _ => {}
} }
} }
@@ -295,31 +320,47 @@ impl Widget for &Chord {
} }
} }
#[test]
fn movement_keybinds() {
let mut app = App::new();
assert_eq!(app.grid.cursor(), (0, 0));
Mode::process_key(&mut app, 'j');
assert_eq!(app.grid.cursor(), (0, 1));
Mode::process_key(&mut app, 'l');
assert_eq!(app.grid.cursor(), (1, 1));
Mode::process_key(&mut app, 'k');
assert_eq!(app.grid.cursor(), (1, 0));
Mode::process_key(&mut app, 'h');
assert_eq!(app.grid.cursor(), (0, 0));
}
#[test] #[test]
fn keybinds() { fn keybinds() {
let mut app = App::new(); let mut app = App::new();
assert_eq!(app.grid.selected_cell, (0,0));
// start at B1 // start at B1
app.grid.selected_cell = (1,1); app.grid.mv_cursor_to(1, 1);
assert_eq!(app.grid.selected_cell, (1,1)); assert_eq!(app.grid.cursor(), (1, 1));
// gg // gg
app.mode = Mode::Chord(Chord::new('g')); app.mode = Mode::Chord(Chord::new('g'));
Mode::process_key(&mut app, 'g'); Mode::process_key(&mut app, 'g');
assert_eq!(app.grid.selected_cell, (1,0)); assert_eq!(app.grid.cursor(), (1, 0));
// 0 // 0
app.mode = Mode::Normal; app.mode = Mode::Normal;
Mode::process_key(&mut app, '0'); Mode::process_key(&mut app, '0');
assert_eq!(app.grid.selected_cell, (0,0)); assert_eq!(app.grid.cursor(), (0, 0));
// 10l // 10l
// this should mean all the directions work // this should mean all the directions work
app.grid.selected_cell = (0,0); app.grid.mv_cursor_to(0, 0);
app.mode = Mode::Chord(Chord::new('1')); app.mode = Mode::Chord(Chord::new('1'));
Mode::process_key(&mut app, '0'); Mode::process_key(&mut app, '0');
Mode::process_key(&mut app, 'l'); Mode::process_key(&mut app, 'l');
assert_eq!(app.grid.selected_cell, (10,0)); assert_eq!(app.grid.cursor(), (10, 0));
} }