Compare commits

11 Commits

Author SHA1 Message Date
5fbff13428 close #6
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 1m15s
2026-02-09 13:24:09 -07:00
7a23ee5bc0 don't let user go oob 2026-02-09 13:14:44 -07:00
ea2e633d7d Merge pull request 'Close #43' (#54) from issue-43 into master
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 48s
Reviewed-on: #54
2026-02-09 20:00:38 +00:00
d9f29434e9 Close #43
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 49s
This takes rendering time from ~0.5ms to ~0.5ms.
When highlighting a range it may get up to 1.5ms.
2026-02-09 12:58:49 -07:00
4239844e0e clippy styling
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 2m29s
2026-02-09 11:54:22 -07:00
5f9cd85faf work on #53
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 1m5s
fix adding rows with ranges
2026-02-09 11:11:24 -07:00
74955032cc document keybinds
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 47s
2026-02-08 22:52:16 -07:00
7b2eb751ab ranges now move correctly with column inserts
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 1m1s
2026-02-08 22:48:12 -07:00
b41a69781c merge cleanup
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 39s
2026-02-07 20:55:05 -07:00
f654ce37a6 Merge branch 'full-copy-history' 2026-02-07 20:52:14 -07:00
1825460074 add failing test
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 48s
2026-02-07 19:30:08 -07:00
9 changed files with 280 additions and 166 deletions

View File

@@ -25,6 +25,8 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
| `gg` | Go to beginning of the column |
| `G` | Go to end of column |
| `gG` | Go to end of the the visual column |
| `u`/`Page Down` | Undo[^undo] |
| `Page Up` | Redo[^undo] |
| `i`/`a` | Enter insert mode on current cell |
| `r` | Enter insert mode on current cell, deleting contents |
| `v` | Enter visual mode |
@@ -43,6 +45,8 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
| n`G` | Jump to row "n" |
| nX | Press "X", "n" times |
[^undo]: Page up/down keybinds are (probably) temporary until I can get ctrl+r to work for redo, which is the propper key for this action. See issue #25 for the status on this.
### Visual mode
| Key | Action |

View File

@@ -19,7 +19,7 @@ use crate::app::{
clipboard::Clipboard,
error_msg::StatusMessage,
logic::{
calc::{Grid, get_header_size},
calc::{Grid, LEN, get_header_size},
cell::CellType,
},
mode::Mode,
@@ -42,9 +42,11 @@ pub struct App {
impl Widget for &App {
fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) {
// let now = std::time::Instant::now();
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_visually_selected = |x: usize, y: usize| -> bool {
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;
@@ -63,18 +65,53 @@ impl Widget for &App {
false
};
// cells that are related by reference to the cursor's cell
// (inputs to formulas and such)
let cells_of_interest: Vec<(usize, usize)> = {
let ctx = crate::app::logic::ctx::ExtractionContext::new();
let (x, y) = self.grid.cursor();
if let Some(cell) = self.grid.get_cell_raw(x, y) {
if let CellType::Equation(eq) = cell {
let _ = evalexpr::eval_with_context(&eq[1..], &ctx);
let vars = ctx.dump_vars();
let mut interest = Vec::new();
for var in vars {
if let Some(a) = Grid::parse_to_idx(&var) {
interest.push(a);
} else if let Some((start, end)) = Grid::range_as_indices(&var) {
// insert coords:
// (start, 0..len)
// ..
// (end, 0..len)
for x in start..=end {
for y in 0..=super::logic::calc::LEN {
interest.push((x,y))
}
}
}
}
interest
} else {
Vec::new()
}
} else {
Vec::new()
}
};
// 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;
for x in 0..x_max {
for y in 0..y_max {
let mut display = String::new();
let mut style = Style::new();
// 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;
// Minus 1 because of header cells,
// the grid is shifted over (1,1), so if you need
// to index the grid, these are you values.
@@ -87,6 +124,11 @@ impl Widget for &App {
y_idx = y as usize - 1 + self.screen.scroll_y();
}
// don't render non-accessible cells
if x_idx > LEN-1 {
continue;
}
const ORANGE1: Color = Color::Rgb(200, 160, 0);
const ORANGE2: Color = Color::Rgb(180, 130, 0);
@@ -97,8 +139,8 @@ impl Widget for &App {
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>();
let l_margin = (0..margin).map(|_| ' ').collect::<String>();
let r_margin = (0..(margin - (l_margin.len() as i32))).map(|_| ' ').collect::<String>();
format!("{l_margin}{text}{r_margin}")
}
@@ -114,7 +156,7 @@ impl Widget for &App {
let bg = if y_idx == self.grid.cursor().1 {
Color::DarkGray
} else if y_idx % 2 == 0 {
} else if y_idx.is_multiple_of(2) {
ORANGE1
} else {
ORANGE2
@@ -127,7 +169,7 @@ impl Widget for &App {
let bg = if x_idx == self.grid.cursor().0 {
Color::DarkGray
} else if x_idx % 2 == 0 {
} else if x_idx.is_multiple_of(2) {
ORANGE1
} else {
ORANGE2
@@ -169,11 +211,12 @@ 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 {
if let Some(_) = self.grid.get_cell_raw(x_idx + i, y_idx) {
if self.grid.get_cell_raw(x_idx + i, y_idx).is_some() {
suggest_upper_bound = Some(cell_width * i as u16);
break;
}
@@ -185,18 +228,25 @@ impl Widget for &App {
display.push('…');
}
}
// ===================================================
}
// Don't render blank cells
None => should_render = false,
}
if is_selected(x.into(), y.into()) {
if cells_of_interest.contains(&(x_idx, y_idx)) {
style = style.fg(Color::Yellow);
should_render = true;
}
if is_visually_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;
style = Style::new().fg(Color::Black).bg(Color::White);
// modify the style of the cell you are editing
if let Mode::Insert(_) = self.mode {
@@ -222,7 +272,7 @@ impl Widget for &App {
} else 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);
let width = min(max_available_width, suggestion);
// Don't draw too small tho, we want full-sized cells, minium
let width = max(cell_width, width);
@@ -235,6 +285,9 @@ impl Widget for &App {
}
}
}
// let ns = now.elapsed().as_nanos() as f64;
// eprintln!("Rendered in {}ms", ns/1_000_000.);
}
}
@@ -280,22 +333,19 @@ impl App {
}
fn file_name_display(&self) -> String {
let file_name_status = {
let mut file_name = "[No Name]";
let mut icon = "";
if let Some(file) = &self.file {
if let Some(f) = file.file_name() {
if let Some(f) = f.to_str() {
file_name = f;
}
let mut file_name = "[No Name]";
let mut icon = "";
if let Some(file) = &self.file {
if let Some(f) = file.file_name() {
if let Some(f) = f.to_str() {
file_name = f;
}
}
if self.grid.needs_to_be_saved() {
icon = "[+]";
}
format!("{file_name}{icon}")
};
file_name_status
}
if self.grid.needs_to_be_saved() {
icon = "[+]";
}
format!("{file_name}{icon}")
}
fn draw(&self, frame: &mut Frame) {
@@ -326,6 +376,8 @@ impl App {
let cmd_line_left = cmd_line_split[0];
let cmd_line_status = cmd_line_split[1];
let cmd_line_right = cmd_line_split[2];
#[cfg(debug_assertions)]
let cmd_line_debug = cmd_line_split[3];
// ======================================================
@@ -360,7 +412,7 @@ impl App {
event::KeyCode::Char(c) => chord.add_char(c),
event::KeyCode::Enter => {
// tmp is to get around reference issues.
let tmp = pos.clone();
let tmp = *pos;
Mode::process_cmd(self);
self.mode = Mode::Visual(tmp)
}
@@ -419,7 +471,7 @@ impl App {
},
Mode::Normal => match event::read()? {
event::Event::Key(key_event) => match key_event.code {
event::KeyCode::F(n) => {},
event::KeyCode::F(_n) => {}
event::KeyCode::Char(c) => Mode::process_key(self, c),
// Pretend that the arrow keys are vim movement keys
event::KeyCode::Left => Mode::process_key(self, 'h'),
@@ -430,12 +482,14 @@ impl App {
event::KeyCode::PageUp => self.grid.redo(),
event::KeyCode::PageDown => self.grid.undo(),
event::KeyCode::Modifier(modifier_key_code) => {
if let event::ModifierKeyCode::LeftControl | event::ModifierKeyCode::RightControl = modifier_key_code {
if let event::ModifierKeyCode::LeftControl | event::ModifierKeyCode::RightControl =
modifier_key_code
{
// TODO my terminal (alacritty) isn't showing me ctrl presses. I know
// that they work tho, since ctrl+r works here in neovim.
// panic!("heard ctrl");
}
},
}
_ => {}
},
_ => {}

View File

@@ -47,7 +47,7 @@ impl Widget for &StatusMessage {
let msg = if self.start.elapsed().as_secs() > 3 {
String::new()
} else {
self.msg.clone().unwrap_or(String::new())
self.msg.clone().unwrap_or_default()
};
let style = match self.msg_type {
@@ -57,4 +57,4 @@ impl Widget for &StatusMessage {
Paragraph::new(msg).style(style).render(area, buf);
}
}
}

View File

@@ -19,8 +19,7 @@ use crate::app::app::App;
use crate::app::mode::Mode;
pub fn get_header_size() -> usize {
let row_header_width = LEN.to_string().len();
row_header_width
LEN.to_string().len()
}
pub const LEN: usize = 1001;
@@ -77,6 +76,7 @@ mod internal {
}
&self.cells[x][y]
}
pub fn set_cell_raw<T: Into<CellType>>(&mut self, (x, y): (usize, usize), val: Option<T>) {
// TODO check oob
self.cells[x][y] = val.map(|v| v.into());
@@ -215,7 +215,7 @@ impl Grid {
if let Ok(val) = self.evaluate(&cell.to_string())
&& resolve_values
{
format!("{}{}", val.to_string(), delim)
format!("{val}{delim}")
} else {
format!("{}{}", cell.escaped_csv_string(), delim)
}
@@ -231,24 +231,21 @@ impl Grid {
Ok(())
}
pub fn get_grid<'a>(&'a self) -> &'a CellGrid {
pub fn get_grid(&self) -> &CellGrid {
&self.grid_history[self.current_grid]
}
fn get_grid_mut<'a>(&'a mut self) -> &'a mut CellGrid {
&mut self.grid_history[self.current_grid]
}
pub fn undo(&mut self) {
self.current_grid = self.current_grid.saturating_sub(1);
}
pub fn redo(&mut self) {
self.current_grid = min(self.grid_history.len() - 1, self.current_grid + 1);
}
pub fn transact_on_grid<F>(&mut self, mut action: F)
where
F: FnMut(&mut CellGrid) -> (),
F: FnMut(&mut CellGrid),
{
// push on a new reality
let new = self.get_grid().clone();
@@ -379,8 +376,15 @@ impl Grid {
f.custom_translate_cell((0, 0), (0, 1), |rolling, old, new| {
if let Some((_, arg_y)) = Grid::parse_to_idx(old) {
if arg_y < insertion_y { rolling.to_owned() } else { rolling.replace(old, new) }
} else if Grid::range_as_indices(old).is_some() {
// ranges are not changed when moved vertically
rolling.to_string()
} else {
unimplemented!("Invalid variable wanted to be translated")
#[cfg(debug_assertions)]
unimplemented!("Invalid variable wanted to be translated");
#[cfg(not(debug_assertions))]
rolling.to_string()
}
})
}) {
@@ -405,8 +409,26 @@ impl Grid {
if let Some((arg_x, _)) = Grid::parse_to_idx(old) {
// add 1 because of the insertion
if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) }
} else if let Some((start, end)) = Grid::range_as_indices(old) {
let mut range_start = Grid::num_to_char(start);
let mut range_end = Grid::num_to_char(end);
if start >= insertion_x {
range_start = Grid::num_to_char(start+1);
}
if end >= insertion_x {
range_end = Grid::num_to_char(end+1);
}
let new = format!("{range_start}:{range_end}");
rolling.replace(old, &new)
} else {
unimplemented!("Invalid variable wanted to be translated")
#[cfg(debug_assertions)]
unimplemented!("Invalid variable wanted to be translated");
#[cfg(not(debug_assertions))]
rolling.to_string()
}
})
}) {
@@ -431,7 +453,7 @@ impl Grid {
return Err(format!("\"{eq}\" is not an equation"));
}
let ctx = ctx::CallbackContext::new(&self);
let ctx = ctx::CallbackContext::new(self);
let prep_for_return = |v: Value| {
if v.is_number() {
@@ -467,14 +489,29 @@ impl Grid {
}
}
/// Gets the indices of the range labels.
/// A:B -> (0,1)
pub fn range_as_indices(range: &str) -> Option<(usize, usize)> {
let v = range.split(':').collect::<Vec<&str>>();
if v.len() == 2 {
let start_col = v[0];
let end_col = v[1];
let start_idx = Grid::char_to_idx(start_col);
let end_idx = Grid::char_to_idx(end_col);
return Some((start_idx, end_idx));
}
None
}
pub fn char_to_idx(i: &str) -> usize {
let x_idx = i
i
.chars()
.filter(|f| f.is_alphabetic())
.enumerate()
.map(|(idx, c)| ((c.to_ascii_lowercase() as usize).saturating_sub(97)) + (26 * idx))
.fold(0, |a, b| a + b);
x_idx
.sum()
}
/// Parse values in the format of A0, C10 ZZ99, etc, and
@@ -503,12 +540,11 @@ impl Grid {
/// Helper for tests
#[cfg(test)]
/// Don't ever remove this from being just a test-helper.
/// This function doesn't correctly use the undo/redo api, which would require doing
/// transactions on the grid instead of direct access.
pub fn set_cell<T: Into<CellType>>(&mut self, cell_id: &str, val: T) {
pub fn set_cell<T: Into<CellType> + Clone>(&mut self, cell_id: &str, val: T) {
if let Some(loc) = Self::parse_to_idx(cell_id) {
self.get_grid_mut().set_cell_raw(loc, Some(val))
self.transact_on_grid(|grid| {
grid.set_cell_raw(loc, Some(val.clone()));
});
}
self.dirty = true;
}
@@ -528,7 +564,7 @@ impl Grid {
if x >= LEN || y >= LEN {
return &None;
}
&self.get_grid().get_cell_raw(x, y)
self.get_grid().get_cell_raw(x, y)
}
pub fn num_to_char(idx: usize) -> String {
@@ -545,7 +581,7 @@ impl Grid {
}
word[1] = ((idx % 26) + 65) as u8 as char;
word.iter().collect()
word.iter().filter(|a| !a.is_ascii_whitespace()).collect()
}
}
@@ -671,8 +707,8 @@ fn alphanumeric_indexing() {
assert_eq!(Grid::parse_to_idx("="), None);
assert_eq!(Grid::parse_to_idx("A:A"), None);
assert_eq!(Grid::num_to_char(0).trim(), "A");
assert_eq!(Grid::num_to_char(25).trim(), "Z");
assert_eq!(Grid::num_to_char(0), "A");
assert_eq!(Grid::num_to_char(25), "Z");
assert_eq!(Grid::num_to_char(26), "AA");
assert_eq!(Grid::num_to_char(51), "AZ");
assert_eq!(Grid::num_to_char(701), "ZZ");
@@ -1078,6 +1114,66 @@ fn insert_col_before_3() {
assert_eq!(cell.to_string(), "=B0*B1");
}
#[test]
fn insert_col_before_static_range() {
let mut grid = Grid::new();
grid.set_cell("A0", 2.);
grid.set_cell("A1", 2.);
grid.set_cell("B0", "=sum(A:A)".to_string());
grid.mv_cursor_to(1, 0);
grid.insert_column_before(grid.cursor());
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
assert_eq!(cell.to_string(), "=sum(A:A)");
}
#[test]
fn insert_col_before_move_range() {
let mut grid = Grid::new();
grid.set_cell("B0", 2.);
grid.set_cell("B1", 2.);
grid.set_cell("A0", "=sum(B:B)".to_string());
grid.mv_cursor_to(0, 0);
grid.insert_column_after(grid.cursor());
let cell = grid.get_cell("A0").as_ref().expect("Just set it");
assert_eq!(cell.to_string(), "=sum(C:C)");
}
#[test]
fn insert_row_before_static_range() {
let mut grid = Grid::new();
grid.set_cell("A0", 2.);
grid.set_cell("A1", 2.);
grid.set_cell("B0", "=sum(A:A)".to_string());
grid.mv_cursor_to(0, 0);
grid.insert_row_above(grid.cursor());
let cell = grid.get_cell("B1").as_ref().expect("Just set it");
assert_eq!(cell.to_string(), "=sum(A:A)");
}
#[test]
fn insert_row_before_move_range() {
let mut grid = Grid::new();
grid.set_cell("A0", 2.);
grid.set_cell("A1", 2.);
grid.set_cell("B0", "=sum(A:A)".to_string());
grid.mv_cursor_to(0, 0);
grid.insert_row_below(grid.cursor());
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
assert_eq!(cell.to_string(), "=sum(A:A)");
}
#[test]
fn insert_row_above_1() {
let mut grid = Grid::new();

View File

@@ -11,15 +11,15 @@ pub enum CellType {
Equation(String),
}
impl Into<CellType> for f64 {
fn into(self) -> CellType {
CellType::duck_type(self.to_string())
impl From<f64> for CellType {
fn from(value: f64) -> Self {
CellType::duck_type(value.to_string())
}
}
impl Into<CellType> for String {
fn into(self) -> CellType {
CellType::duck_type(self)
impl From<String> for CellType {
fn from(value: String) -> Self {
CellType::duck_type(value)
}
}
@@ -37,14 +37,10 @@ impl CellType {
}
// escape string of it has a comma
if display.contains(CSV_DELIMITER) {
format!("\"{display}\"")
} else {
display
}
if display.contains(CSV_DELIMITER) { format!("\"{display}\"") } else { display }
}
fn duck_type<'a>(value: impl Into<String>) -> Self {
fn duck_type(value: impl Into<String>) -> Self {
let value = value.into();
if let Ok(parse) = value.parse::<f64>() {
@@ -56,7 +52,12 @@ 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 {
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(),
@@ -73,10 +74,8 @@ impl CellType {
let mut lock_y = false;
if old_var.contains('$') {
let locations = old_var
.char_indices()
.filter(|(_, c)| *c == '$').map(|(i, _)| i)
.collect::<Vec<usize>>();
let locations =
old_var.char_indices().filter(|(_, c)| *c == '$').map(|(i, _)| i).collect::<Vec<usize>>();
match locations.len() {
1 => {
if locations[0] == 0 {
@@ -89,7 +88,7 @@ impl CellType {
}
2 => {
// Ignore this variable all together, effectively lockng X & Y
continue;
continue;
}
_ => {
// There are 0 or >2 "$" in this string.
@@ -100,7 +99,7 @@ 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;
@@ -110,20 +109,11 @@ impl CellType {
let x2 = x2 as i32;
let y2 = y2 as i32;
let dest_x = if lock_x {
src_x as usize
} else {
(src_x as i32 + (x2 - x1)) as usize
};
let dest_x = if lock_x { src_x } else { (src_x as i32 + (x2 - x1)) as usize };
let dest_y = if lock_y {
src_y as usize
} else {
(src_y as i32 + (y2 - y1)) as usize
};
let dest_y = if lock_y { src_y } else { (src_y as i32 + (y2 - y1)) as usize };
let alpha = Grid::num_to_char(dest_x);
let alpha = alpha.trim();
// Persist the "$" locking
let new_var = if lock_x {
@@ -141,37 +131,29 @@ impl CellType {
// why you coping invalid stuff, nerd?
//
// could be copying a range
if old_var.contains(':') {
let parts = old_var.split(':').collect::<Vec<&str>>();
// This means the var was formatted as X:X
if parts.len() == 2 {
// how far is the movement?
let dx = to.0 as i32 - from.0 as i32;
if let Some(parts) = Grid::range_as_indices(&old_var) {
// how far is the movement?
let dx = to.0 as i32 - from.0 as i32;
let range_start = parts[0];
let range_end = parts[1];
let xs = parts.0 as i32;
let xe = parts.1 as i32;
// get the letters as numbers
let xs = Grid::char_to_idx(range_start) as i32;
let xe = Grid::char_to_idx(range_end) as i32;
// apply movement
let mut new_range_start = xs+dx;
let mut new_range_end = xe+dx;
// apply movement
let mut new_range_start = xs + dx;
let mut new_range_end = xe + dx;
// bottom out at 0
if new_range_start < 0 {
new_range_start = 0;
}
if new_range_end < 0 {
new_range_end = 0;
}
// convert the index back into a letter and then submit it
let start = Grid::num_to_char(new_range_start as usize);
let end = Grid::num_to_char(new_range_end as usize);
equation = replace_fn(&equation, &old_var, &format!("{}:{}", start.trim(), end.trim()));
// bottom out at 0
if new_range_start < 0 {
new_range_start = 0;
}
if new_range_end < 0 {
new_range_end = 0;
}
// convert the index back into a letter and then submit it
let start = Grid::num_to_char(new_range_start as usize);
let end = Grid::num_to_char(new_range_end as usize);
equation = replace_fn(&equation, &old_var, &format!("{start}:{end}"));
}
}
}
@@ -181,7 +163,7 @@ impl CellType {
}
pub fn translate_cell(&self, from: (usize, usize), to: (usize, usize)) -> CellType {
self.custom_translate_cell(from, to, |a,b,c| a.replace(b, c))
self.custom_translate_cell(from, to, |a, b, c| a.replace(b, c))
}
}
@@ -206,4 +188,3 @@ impl PartialEq for CellType {
}
}
}

View File

@@ -16,26 +16,12 @@ pub struct CallbackContext<'a> {
impl<'a> CallbackContext<'a> {
fn expand_range(&self, range: &str) -> Option<Vec<&CellType>> {
let v = range.split(':').collect::<Vec<&str>>();
if v.len() == 2 {
let start_col = v[0];
let end_col = v[1];
let as_index = |s: &str| {
s.char_indices()
// .filter(|f| f.1 as u8 >= 97) // prevent sub with overflow errors
.map(|(idx, c)| ((c.to_ascii_lowercase() as usize).saturating_sub(97)) + (26 * idx))
.fold(0, |a, b| a + b)
};
let start_idx = as_index(start_col);
let end_idx = as_index(end_col);
if let Some((start, end)) = Grid::range_as_indices(range) {
let mut buf = Vec::new();
for x in start_idx..=end_idx {
for x in start..=end {
for y in 0..=self.variables.get_grid().max_y_at_x(x) {
if let Some(s) = self.variables.get_grid().get_cell_raw(x, y) {
if let Some(s) = self.variables.get_cell_raw(x, y) {
buf.push(s);
}
}

View File

@@ -87,7 +87,7 @@ impl Mode {
path.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a")
));
if let None = app.file {
if app.file.is_none() {
app.file = Some(path)
}
}
@@ -197,7 +197,7 @@ impl Mode {
}
"export" => {
if let Some(arg1) = args.get(1) {
save_range(&arg1);
save_range(arg1);
} else {
app.msg = StatusMessage::error("export <path.csv>")
}
@@ -273,20 +273,20 @@ impl Mode {
// Go to end of row
'$' => {
let (_, y) = app.grid.cursor();
app.grid.mv_cursor_to(super::logic::calc::LEN, y);
app.grid.mv_cursor_to(super::logic::calc::LEN-1, y);
return;
}
// Go to bottom of column
'G' => {
let (x, _) = app.grid.cursor();
app.grid.mv_cursor_to(x, super::logic::calc::LEN);
app.grid.mv_cursor_to(x, super::logic::calc::LEN-1);
return;
}
// edit cell
'i' | 'a' => {
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_default();
app.mode = Mode::Insert(Chord::from(val));
}
@@ -477,14 +477,12 @@ impl Mode {
let len = match &self {
Mode::Insert(edit) | Mode::VisualCmd(_, edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(),
Mode::Normal => {
let len = cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default();
len
cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default()
}
Mode::Visual(_) => 0,
};
// min 20 chars, expand if needed
let len = max(len as u16 + 1, 20);
len
max(len as u16 + 1, 20)
}
pub fn render(&self, f: &mut ratatui::Frame, area: prelude::Rect, cell: &Option<CellType>) {
@@ -498,8 +496,7 @@ impl Mode {
Mode::Chord(chord) => f.render_widget(chord, area),
Mode::Normal => f.render_widget(
Paragraph::new({
let cell = cell.as_ref().map(|f| f.to_string()).unwrap_or_default();
cell
cell.as_ref().map(|f| f.to_string()).unwrap_or_default()
}),
area,
),
@@ -522,8 +519,7 @@ impl From<String> for Chord {
impl Chord {
pub fn new(inital: char) -> Self {
let mut buf = Vec::new();
buf.push(inital);
let buf = vec![inital];
Self { buf }
}

View File

@@ -1,4 +1,4 @@
use std::{collections::HashMap, sync::RwLock};
use std::{cmp::min, collections::HashMap, sync::RwLock};
use ratatui::prelude;
@@ -48,11 +48,11 @@ impl ScreenSpace {
// ======= 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;
let x_cells = (screen_size.0 / self.get_cell_width(vars)) -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;
let y_cells = (screen_size.1 / self.get_cell_height(vars)) -2;
(x_cells,y_cells)
} else {
(0,0)
@@ -123,22 +123,12 @@ impl ScreenSpace {
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 width = (area.width as usize / self.get_cell_width(vars)) + 1;
let height = area.height as usize / self.get_cell_height(vars);
let x_max =
if width > LEN {
LEN - 1
} else {
width
};
let y_max =
if height > LEN {
LEN - 1
} else {
height
};
let x_max = min(LEN-1, width);
let y_max = min(LEN-1, height);
(x_max as u16, y_max as u16)
}

View File

@@ -1,3 +1,10 @@
#![allow(clippy::needless_return)]
#![allow(clippy::len_zero)]
#![allow(clippy::collapsible_if)]
#![allow(clippy::collapsible_else_if)]
#![allow(clippy::collapsible_match)]
#![allow(clippy::single_match)]
mod app;
use std::env::args;