Compare commits

..

4 Commits

Author SHA1 Message Date
b8fd938120 close #38 2025-11-14 10:34:43 -07:00
3be92aea3c close #40 2025-11-14 09:46:57 -07:00
902af1311d reorganize file extension logic 2025-11-14 09:40:53 -07:00
9c44db0d92 clean up rendering function 2025-11-14 09:33:11 -07:00
6 changed files with 243 additions and 69 deletions

View File

@@ -1,13 +1,25 @@
use std::{
cmp::{max, min}, collections::HashMap, io, path::PathBuf
cmp::{max, min},
collections::HashMap,
io,
path::PathBuf,
};
use ratatui::{
DefaultTerminal, Frame, crossterm::event, layout::{self, Constraint, Layout, Rect}, prelude, style::{Color, Modifier, Style}, widgets::{Paragraph, Widget}
DefaultTerminal, Frame,
crossterm::event,
layout::{self, Constraint, Layout, Rect},
prelude,
style::{Color, Modifier, Style},
widgets::{Paragraph, Widget},
};
use crate::app::{
clipboard::Clipboard, error_msg::StatusMessage, logic::{calc::Grid, cell::CellType}, mode::Mode, screen::ScreenSpace
clipboard::Clipboard,
error_msg::StatusMessage,
logic::{calc::Grid, cell::CellType},
mode::Mode,
screen::ScreenSpace,
};
pub struct App {
@@ -144,6 +156,13 @@ impl Widget for &App {
break;
}
}
if let Some(bound) = suggest_upper_bound {
let bound = bound as usize;
if bound < display.len() {
display.truncate(bound - 2);
display.push('…');
}
}
}
None => should_render = false,
}
@@ -210,27 +229,7 @@ impl App {
Ok(())
}
fn draw(&self, frame: &mut Frame) {
let layout = Layout::default()
.direction(layout::Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.split(frame.area());
let cmd_line = layout[0];
let body = layout[1];
let len = match &self.mode {
Mode::Insert(edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(),
Mode::Normal => {
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();
cell
}
Mode::Visual(_) => 0,
};
// min 20 chars, expand if needed
let len = max(len as u16 + 1, 20);
fn file_name_display(&self) -> String {
let file_name_status = {
let mut file_name = "[No Name]";
let mut icon = "";
@@ -246,39 +245,46 @@ impl App {
}
format!("{file_name}{icon}")
};
file_name_status
}
fn draw(&self, frame: &mut Frame) {
let (x, y) = self.grid.cursor();
let current_cell = self.grid.get_cell_raw(x, y);
let len = self.mode.chars_to_display(current_cell);
let file_name_status = self.file_name_display();
// layout
// ======================================================
let layout = Layout::default()
.direction(layout::Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.split(frame.area());
let cmd_line = layout[0];
let body = layout[1];
let cmd_line_split = Layout::default()
.direction(layout::Direction::Horizontal)
.constraints([Constraint::Length(len), Constraint::Length(file_name_status.len() as u16 + 1), Constraint::Percentage(50), Constraint::Percentage(50)])
.constraints([
Constraint::Length(len),
Constraint::Length(file_name_status.len() as u16 + 1),
Constraint::Percentage(50),
Constraint::Percentage(50),
])
.split(cmd_line);
let cmd_line_left = cmd_line_split[0];
let cmd_line_status = cmd_line_split[1];
let cmd_line_right = cmd_line_split[2];
let cmd_line_debug = cmd_line_split[3];
// ======================================================
match &self.mode {
Mode::Insert(editor) => {
frame.render_widget(editor, cmd_line_left);
}
Mode::Command(editor) => {
frame.render_widget(editor, cmd_line_left);
}
Mode::Chord(chord) => frame.render_widget(chord, cmd_line_left),
Mode::Normal => frame.render_widget(
Paragraph::new({
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();
cell
}),
cmd_line_left,
),
Mode::Visual(_) => {}
}
self.mode.render(frame, cmd_line_left, current_cell);
frame.render_widget(self, body);
frame.render_widget(&self.msg, cmd_line_right);
frame.render_widget(Paragraph::new(file_name_status), cmd_line_status);
#[cfg(debug_assertions)]
frame.render_widget(
Paragraph::new(format!(

View File

@@ -296,3 +296,57 @@ fn copy_paste_vars_translate() {
let a = app.grid.get_cell("A0").as_ref().expect("Should've been set by paste");
assert_eq!(a.to_string(), "=A1");
}
#[test]
fn copy_paste_double_locked_var() {
let mut app = App::new();
app.grid.set_cell("A0", 0.);
app.grid.set_cell("A1", "=$A$0".to_string());
// Copy A0
app.grid.mv_cursor_to(0, 1);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
app.grid.mv_cursor_to(1, 0);
Mode::process_key(&mut app, 'p');
let c = app.grid.get_cell("B0").as_ref().expect("Just set it");
assert_eq!(c.to_string(), "=$A$0");
}
#[test]
fn copy_paste_x_locked_var() {
let mut app = App::new();
app.grid.set_cell("A0", 0.);
app.grid.set_cell("A1", "=$A0".to_string());
// Copy A0
app.grid.mv_cursor_to(0, 1);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
app.grid.mv_cursor_to(1, 2);
Mode::process_key(&mut app, 'p');
let c = app.grid.get_cell("B2").as_ref().expect("Just set it");
assert_eq!(c.to_string(), "=$A1");
}
#[test]
fn copy_paste_y_locked_var() {
let mut app = App::new();
app.grid.set_cell("A0", 0.);
app.grid.set_cell("A1", "=A$0".to_string());
// Copy A0
app.grid.mv_cursor_to(0, 1);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
app.grid.mv_cursor_to(1, 2);
Mode::process_key(&mut app, 'p');
let c = app.grid.get_cell("B2").as_ref().expect("Just set it");
assert_eq!(c.to_string(), "=B$0");
}

View File

@@ -12,13 +12,14 @@ use crate::app::{
cell::{CSV_DELIMITER, CellType},
ctx,
},
mode::Mode,
};
#[cfg(test)]
use crate::app::app::App;
pub const LEN: usize = 1000;
pub const CSV_EXT: &str = "csv";
pub const CUSTOM_EXT: &str = "nscim";
pub struct Grid {
// a b c ...
@@ -81,16 +82,13 @@ 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();
const CSV: &str = "csv";
const CUSTOM_EXT: &str = "nscim";
let path = path.into();
let resolve_values;
match path.extension() {
Some(ext) => match ext.to_str() {
Some(CSV) => {
Some(CSV_EXT) => {
resolve_values = true;
}
Some(CUSTOM_EXT) => {
@@ -98,12 +96,12 @@ impl Grid {
}
_ => {
resolve_values = false;
path.add_extension(CUSTOM_EXT);
// path.add_extension(CUSTOM_EXT);
}
},
None => {
resolve_values = false;
path.add_extension(CUSTOM_EXT);
// path.add_extension(CUSTOM_EXT);
}
}
@@ -381,6 +379,8 @@ impl Grid {
/// Parse values in the format of A0, C10 ZZ99, etc, and
/// turn them into an X,Y index.
pub fn parse_to_idx(i: &str) -> Option<(usize, usize)> {
let i = i.replace('$', "");
let chars = i.chars().take_while(|c| c.is_alphabetic()).collect::<Vec<char>>();
let nums = i.chars().skip(chars.len()).take_while(|c| c.is_numeric()).collect::<String>();
@@ -476,6 +476,9 @@ fn cell_strings() {
#[test]
fn alphanumeric_indexing() {
assert_eq!(Grid::parse_to_idx("A0"), Some((0, 0)));
assert_eq!(Grid::parse_to_idx("$A0"), Some((0, 0)));
assert_eq!(Grid::parse_to_idx("A$0"), Some((0, 0)));
assert_eq!(Grid::parse_to_idx("$A$0"), Some((0, 0)));
assert_eq!(Grid::parse_to_idx("AA0"), Some((26, 0)));
assert_eq!(Grid::parse_to_idx("A1"), Some((0, 1)));
assert_eq!(Grid::parse_to_idx("A10"), Some((0, 10)));

View File

@@ -66,6 +66,37 @@ impl CellType {
let mut rolling = eq.clone();
// translate standard vars A0 -> A1
for old_var in ctx.dump_vars() {
let mut lock_x = false;
let mut lock_y = false;
if old_var.contains('$') {
let locations = old_var.char_indices().filter(|(_, c)| *c == '$').map(|(i, _)| i).collect::<Vec<usize>>();
match locations.len() {
1 => {
if locations[0] == 0 {
// locking the X axis (A,B,C...)
lock_x = true;
} 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 => {
// YOLO, lock both X & Y
continue; // just pretend you never even saw this var
}
_ => {
// 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.
}
}
}
if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) {
let (x1, y1) = from;
let x1 = x1 as i32;
@@ -74,12 +105,29 @@ impl CellType {
let x2 = x2 as i32;
let y2 = y2 as i32;
let dest_x = (src_x as i32 + (x2 - x1)) as usize;
let dest_y = (src_y as i32 + (y2 - y1)) as usize;
let dest_x = if lock_x {
src_x as usize
} 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 alpha = Grid::num_to_char(dest_x);
let alpha = alpha.trim();
let new_var = format!("{alpha}{dest_y}");
let new_var = if lock_x {
format!("${alpha}{dest_y}")
} else if lock_y {
format!("{alpha}${dest_y}")
} else {
format!("{alpha}{dest_y}")
};
// swap out vars
rolling = replace_fn(&rolling, &old_var, &new_var);

View File

@@ -295,7 +295,7 @@ impl Context for ExtractionContext {
registry.push(identifier.to_owned())
} else { panic!("The RwLock should always be write-able") }
// Ok(Value::Int(1))
todo!();
unimplemented!("Extracting function identifier not implemented yet")
}
fn are_builtin_functions_disabled(&self) -> bool {

View File

@@ -1,4 +1,8 @@
use std::{cmp::min, fmt::Display, path::PathBuf};
use std::{
cmp::{max, min},
fmt::Display,
path::PathBuf,
};
use ratatui::{
prelude,
@@ -9,7 +13,10 @@ use ratatui::{
use crate::app::{
app::App,
error_msg::StatusMessage,
logic::calc::LEN,
logic::{
calc::{CSV_EXT, CUSTOM_EXT, LEN},
cell::CellType,
},
};
pub enum Mode {
@@ -59,13 +66,32 @@ impl Mode {
"w" => {
// first try the passed argument as file
if let Some(arg) = args.get(1) {
if let Err(e) = app.grid.save_to(arg) {
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);
}
};
if let Err(e) = app.grid.save_to(&path) {
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")));
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(path)
@@ -76,7 +102,10 @@ impl Mode {
if let Err(e) = app.grid.save_to(file) {
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")));
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 {
@@ -168,13 +197,13 @@ impl Mode {
'A' => {
let c = app.grid.cursor();
app.grid.insert_column_after(c);
app.grid.mv_cursor_to(c.0+1, c.1);
app.grid.mv_cursor_to(c.0 + 1, c.1);
}
// insert row below
'o' => {
let c = app.grid.cursor();
app.grid.insert_row_below(c);
app.grid.mv_cursor_to(c.0, c.1+1);
app.grid.mv_cursor_to(c.0, c.1 + 1);
}
// insert row above
'O' => {
@@ -200,7 +229,7 @@ impl Mode {
match key {
'd' | 'x' => {
app.clipboard.clipboard_cut((x1,y1), (x2,y2), &mut app.grid);
app.clipboard.clipboard_cut((x1, y1), (x2, y2), &mut app.grid);
app.mode = Mode::Normal
}
'y' => {
@@ -284,7 +313,7 @@ impl Mode {
app.clipboard.paste(&mut app.grid, false);
app.grid.apply_momentum(app.clipboard.momentum());
app.mode = Mode::Normal;
let plural = if app.clipboard.qty() > 1 {"cells"} else {"cell"};
let plural = if app.clipboard.qty() > 1 { "cells" } else { "cell" };
app.msg = StatusMessage::info(format!("Pasted {plural}, no formatting"));
return;
}
@@ -294,8 +323,42 @@ impl Mode {
}
}
// IDK why it works but it does. Keystrokes are process somewhere else?
Mode::Insert(_chord) => {},
Mode::Command(_chord) => {},
Mode::Insert(_chord) => {}
Mode::Command(_chord) => {}
}
}
pub fn chars_to_display(&self, cell: &Option<CellType>) -> u16 {
let len = match &self {
Mode::Insert(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
}
Mode::Visual(_) => 0,
};
// min 20 chars, expand if needed
let len = max(len as u16 + 1, 20);
len
}
pub fn render(&self, f: &mut ratatui::Frame, area: prelude::Rect, cell: &Option<CellType>) {
match &self {
Mode::Insert(editor) => {
f.render_widget(editor, area);
}
Mode::Command(editor) => {
f.render_widget(editor, area);
}
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
}),
area,
),
Mode::Visual(_) => {}
}
}
}