diff --git a/src/app/app.rs b/src/app/app.rs index be26779..6222c29 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -7,7 +7,7 @@ use ratatui::{ }; use crate::app::{ - clipboard::Clipboard, error_msg::ErrorMessage, logic::calc::{CellType, Grid}, mode::Mode, screen::ScreenSpace + clipboard::Clipboard, error_msg::StatusMessage, logic::calc::{CellType, Grid}, mode::Mode, screen::ScreenSpace }; pub struct App { @@ -15,7 +15,7 @@ pub struct App { pub grid: Grid, pub mode: Mode, pub file: Option, - pub error_msg: ErrorMessage, + pub msg: StatusMessage, pub vars: HashMap, pub screen: ScreenSpace, // this could probably be a normal array @@ -187,7 +187,7 @@ impl App { grid: Grid::new(), mode: Mode::Normal, file: None, - error_msg: ErrorMessage::none(), + msg: StatusMessage::none(), vars: HashMap::new(), screen: ScreenSpace::new(), marks: HashMap::new(), @@ -260,7 +260,7 @@ impl App { } frame.render_widget(self, body); - frame.render_widget(&self.error_msg, cmd_line_right); + frame.render_widget(&self.msg, cmd_line_right); #[cfg(debug_assertions)] frame.render_widget( Paragraph::new(format!( diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs index 3984a8a..a6d152f 100644 --- a/src/app/clipboard.rs +++ b/src/app/clipboard.rs @@ -1,5 +1,3 @@ -use std::cmp::{max, min}; - use crate::app::logic::calc::{CellType, Grid}; #[cfg(test)] @@ -24,18 +22,21 @@ impl Clipboard { } } + /// Panics if clipboard is 0 length (if you call after you + /// just filled it with anything you are gtg). + pub fn qty(&self) -> usize { + // it will be a square + let x_len = self.clipboard.len(); + let y_len = self.clipboard[0].len(); + + x_len*y_len + } + /// 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); - + let (x, y) = self.momentum; // prevent diagonal momentum if y != 0 { (0, y) @@ -171,4 +172,27 @@ fn momentum_x_neg() { Mode::process_key(&mut app, 'p'); assert_eq!(app.clipboard.momentum(), (-1,0)); +} + +#[test] +fn diagonal_momentum() { + 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(1, 0); + assert_eq!(app.grid.cursor(), (1, 0)); + Mode::process_key(&mut app, 'p'); + + assert_eq!(app.clipboard.momentum(), (0,-1)); + assert_eq!(app.grid.cursor(), (1, 0)); + + app.grid.apply_momentum(app.clipboard.momentum()); + + assert_eq!(app.grid.cursor(), (1,0)); } \ No newline at end of file diff --git a/src/app/error_msg.rs b/src/app/error_msg.rs index f10b56f..0247c36 100644 --- a/src/app/error_msg.rs +++ b/src/app/error_msg.rs @@ -2,28 +2,44 @@ use std::time::Instant; use ratatui::{layout::Rect, prelude, style::{Color, Style}, widgets::{Paragraph, Widget}}; -pub struct ErrorMessage { - start: Instant, - error_msg: Option, +pub enum MsgType { + Error, + Info } -impl ErrorMessage { - pub fn new(msg: impl Into) -> Self { +pub struct StatusMessage { + start: Instant, + msg_type: MsgType, + msg: Option, +} + +impl StatusMessage { + pub fn info(msg: impl Into) -> Self { Self { - error_msg: Some(msg.into()), + msg: Some(msg.into()), start: Instant::now(), + msg_type: MsgType::Info, + } + } + + pub fn error(msg: impl Into) -> Self { + Self { + msg: Some(msg.into()), + start: Instant::now(), + msg_type: MsgType::Error, } } pub fn none() -> Self { Self { start: Instant::now(), - error_msg: None, + msg: None, + msg_type: MsgType::Info, } } } -impl Widget for &ErrorMessage { +impl Widget for &StatusMessage { fn render(self, area: Rect, buf: &mut prelude::Buffer) { // The screen doesn't refresh at a fixed fps like a normal GUI, // so if the user isn't moving around the timeout will *happen* but @@ -31,9 +47,14 @@ impl Widget for &ErrorMessage { let msg = if self.start.elapsed().as_secs() > 3 { String::new() } else { - self.error_msg.clone().unwrap_or(String::new()) + self.msg.clone().unwrap_or(String::new()) }; - Paragraph::new(msg).style(Style::new().fg(Color::Red)).render(area, buf); + let style = match self.msg_type { + MsgType::Error => Style::new().fg(Color::Red), + MsgType::Info => Style::new().fg(Color::LightGreen), + }; + + Paragraph::new(msg).style(style).render(area, buf); } } \ No newline at end of file diff --git a/src/app/logic/calc.rs b/src/app/logic/calc.rs index 4855383..93a72dc 100644 --- a/src/app/logic/calc.rs +++ b/src/app/logic/calc.rs @@ -1,8 +1,5 @@ use std::{ - fmt::Display, - fs, - io::{Read, Write}, - path::PathBuf, + cmp::{max, min}, fmt::Display, fs, io::{Read, Write}, path::PathBuf }; use evalexpr::*; @@ -38,6 +35,24 @@ impl Grid { self.selected_cell } + pub fn apply_momentum(&mut self, (x, y): (i32, i32)) { + let (cx, cy) = self.cursor(); + + assert_eq!(0i32.saturating_add(-1), -1); + let x = (cx as i32).saturating_add(x); + let y = (cy as i32).saturating_add(y); + + // make it positive + let x = max(x,0) as usize; + let y = max(y,0) as usize; + + // keep it in the grid + let x = min(x, LEN-1); + let y = min(y, LEN-1); + + self.mv_cursor_to(x, y); + } + pub fn mv_cursor_to(&mut self, x: usize, y: usize) { self.selected_cell = (x,y) } @@ -718,7 +733,7 @@ fn ranges() { #[test] fn recursive_ranges() { // recursive ranges causes weird behavior - // todo!(); + todo!(); } #[test] diff --git a/src/app/mode.rs b/src/app/mode.rs index 9da414a..839560e 100644 --- a/src/app/mode.rs +++ b/src/app/mode.rs @@ -1,4 +1,4 @@ -use std::{cmp::min, fmt::Display}; +use std::{cmp::min, fmt::Display, path::PathBuf}; use ratatui::{ prelude, @@ -8,7 +8,7 @@ use ratatui::{ use crate::app::{ app::App, - error_msg::ErrorMessage, + error_msg::StatusMessage, logic::calc::{CellType, LEN}, }; @@ -60,29 +60,34 @@ impl Mode { // first try the passed argument as file if let Some(arg) = args.get(1) { if let Err(e) = app.grid.save_to(arg) { - app.error_msg = ErrorMessage::new(format!("{e}")); + app.msg = StatusMessage::error(format!("{e}")); } else { // file saving was a success, adopt the provided file // if we don't already have one (this is how vim works) + let path: PathBuf = arg.into(); + app.msg = StatusMessage::info(format!("Saved file {}", path.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a"))); + if let None = app.file { - app.file = Some(arg.into()) + app.file = Some(path) } } // then try the file that we opened the program with } else if let Some(file) = &app.file { if let Err(e) = app.grid.save_to(file) { - app.error_msg = ErrorMessage::new(format!("{e}")); + app.msg = StatusMessage::error(format!("{e}")); + } else { + 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* } else { - app.error_msg = ErrorMessage::new("No file selected"); + app.msg = StatusMessage::error("No file selected"); } } // quit "q" => { if app.grid.needs_to_be_saved() { app.exit = false; - app.error_msg = ErrorMessage::new("File not saved"); + app.msg = StatusMessage::error("File not saved"); } else { app.exit = true } @@ -95,7 +100,7 @@ impl Mode { if let Some(arg) = args.get(1) { let parts: Vec<&str> = arg.split('=').collect(); if parts.len() != 2 { - app.error_msg = ErrorMessage::new("set ="); + app.msg = StatusMessage::error("set ="); return; } let key = parts[0]; @@ -103,7 +108,7 @@ impl Mode { app.vars.insert(key.to_owned(), value.to_owned()); } - app.error_msg = ErrorMessage::new("set =") + app.msg = StatusMessage::error("set =") } _ => {} } @@ -163,9 +168,7 @@ impl Mode { ':' => 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); + app.grid.apply_momentum(app.clipboard.momentum()); return; } // loose chars will put you into chord mode @@ -193,6 +196,8 @@ impl Mode { } 'y' => { app.clipboard.clipboard_copy((x1, y1), (x2, y2), &app.grid); + app.msg = StatusMessage::info(format!("Yanked {} cells", app.clipboard.qty())); + app.mode = Mode::Normal } _ => {} } @@ -264,6 +269,7 @@ impl Mode { let point = app.grid.cursor(); app.clipboard.clipboard_copy(point, point, &app.grid); app.mode = Mode::Normal; + app.msg = StatusMessage::info("Yanked 1 cell"); } _ => {} }