mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 05:19:48 -04:00
feat: wire in ironcalc to the ui
This commit is contained in:
parent
a27d2871c2
commit
c55b1cdac3
132
src/book/mod.rs
132
src/book/mod.rs
@ -1,35 +1,51 @@
|
|||||||
use std::path::PathBuf;
|
use std::cmp::max;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use ironcalc::{
|
use ironcalc::{
|
||||||
base::{
|
base::{
|
||||||
locale, types::{SheetData, Worksheet}, Model
|
types::{SheetData, Worksheet},
|
||||||
|
worksheet::WorksheetDimension,
|
||||||
|
Model,
|
||||||
},
|
},
|
||||||
export::save_to_xlsx,
|
export::save_to_xlsx,
|
||||||
import::load_from_xlsx,
|
import::load_from_xlsx,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::ui::Address;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
/// A spreadsheet book with some internal state tracking.
|
/// A spreadsheet book with some internal state tracking.
|
||||||
pub struct Book {
|
pub struct Book {
|
||||||
model: Model,
|
pub(crate) model: Model,
|
||||||
current_sheet: u32,
|
pub current_sheet: u32,
|
||||||
current_location: (i32, i32),
|
pub location: crate::ui::Address,
|
||||||
|
// TODO(zaphar): Because the ironcalc model is sparse we need to track our render size
|
||||||
|
// separately
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Book {
|
impl Book {
|
||||||
|
|
||||||
/// Construct a new book from a Model
|
/// Construct a new book from a Model
|
||||||
pub fn new(model: Model) -> Self {
|
pub fn new(model: Model) -> Self {
|
||||||
Self {
|
Self {
|
||||||
model,
|
model,
|
||||||
current_sheet: 0,
|
current_sheet: 0,
|
||||||
current_location: (0, 0),
|
location: Address::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_from_xlsx(path: &str) -> Result<Self> {
|
||||||
|
Ok(Self::new(load_from_xlsx(path, "en", "America/New_York")?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(&mut self) {
|
||||||
|
self.model.evaluate();
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(zaphar): Should I support ICalc?
|
// TODO(zaphar): Should I support ICalc?
|
||||||
/// Construct a new book from a path.
|
/// Construct a new book from a path.
|
||||||
pub fn new_from_xlsx(path: &str, locale: &str, tz: &str) -> Result<Self> {
|
pub fn new_from_xlsx_with_locale(path: &str, locale: &str, tz: &str) -> Result<Self> {
|
||||||
Ok(Self::new(load_from_xlsx(path, locale, tz)?))
|
Ok(Self::new(load_from_xlsx(path, locale, tz)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +55,15 @@ impl Book {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_all_sheets_identifiers(&self) -> Vec<(String, u32)> {
|
||||||
|
self.model
|
||||||
|
.workbook
|
||||||
|
.worksheets
|
||||||
|
.iter()
|
||||||
|
.map(|sheet| (sheet.get_name(), sheet.get_sheet_id()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the currently set sheets name.
|
/// Get the currently set sheets name.
|
||||||
pub fn get_sheet_name(&self) -> Result<&str> {
|
pub fn get_sheet_name(&self) -> Result<&str> {
|
||||||
Ok(&self.get_sheet()?.name)
|
Ok(&self.get_sheet()?.name)
|
||||||
@ -49,49 +74,90 @@ impl Book {
|
|||||||
Ok(&self.get_sheet()?.sheet_data)
|
Ok(&self.get_sheet()?.sheet_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn move_to(&mut self, loc: Address) -> Result<()> {
|
||||||
|
// FIXME(zaphar): Check that this is safe first.
|
||||||
|
self.location.row = loc.row;
|
||||||
|
self.location.col = loc.col;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a cells formatted content.
|
/// Get a cells formatted content.
|
||||||
pub fn get_cell_rendered(&self) -> Result<String> {
|
pub fn get_current_cell_rendered(&self) -> Result<String> {
|
||||||
|
Ok(self.get_cell_addr_rendered(self.location.row, self.location.col)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(zaphar): Use Address here too
|
||||||
|
pub fn get_cell_addr_rendered(&self, row: usize, col: usize) -> Result<String> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.model
|
.model
|
||||||
.get_formatted_cell_value(
|
.get_formatted_cell_value(self.current_sheet, row as i32, col as i32)
|
||||||
self.current_sheet,
|
|
||||||
self.current_location.0,
|
|
||||||
self.current_location.1,
|
|
||||||
)
|
|
||||||
.map_err(|s| anyhow!("Unable to format cell {}", s))?)
|
.map_err(|s| anyhow!("Unable to format cell {}", s))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a cells actual content as a string.
|
/// Get a cells actual content as a string.
|
||||||
pub fn get_cell_contents(&self) -> Result<String> {
|
pub fn get_current_cell_contents(&self) -> Result<String> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.model
|
.model
|
||||||
.get_cell_content(
|
.get_cell_content(
|
||||||
self.current_sheet,
|
self.current_sheet,
|
||||||
self.current_location.0,
|
self.location.row as i32,
|
||||||
self.current_location.1,
|
self.location.col as i32,
|
||||||
)
|
)
|
||||||
.map_err(|s| anyhow!("Unable to format cell {}", s))?)
|
.map_err(|s| anyhow!("Unable to format cell {}", s))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn edit_cell(&mut self, value: String) -> Result<()> {
|
/// Update the current cell in a book.
|
||||||
|
/// This update won't be reflected until you call `Book::evaluate`.
|
||||||
|
pub fn edit_current_cell<S: Into<String>>(&mut self, value: S) -> Result<()> {
|
||||||
|
self.update_entry(&self.location.clone(), value)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an entry in the current sheet for a book.
|
||||||
|
/// This update won't be reflected until you call `Book::evaluate`.
|
||||||
|
pub fn update_entry<S: Into<String>>(&mut self, location: &Address, value: S) -> Result<()> {
|
||||||
self.model
|
self.model
|
||||||
.set_user_input(
|
.set_user_input(
|
||||||
self.current_sheet,
|
self.current_sheet,
|
||||||
self.current_location.0,
|
location.row as i32,
|
||||||
self.current_location.1,
|
location.col as i32,
|
||||||
value,
|
value.into(),
|
||||||
)
|
)
|
||||||
.map_err(|e| anyhow!("Invalid cell contents: {}", e))?;
|
.map_err(|e| anyhow!("Invalid cell contents: {}", e))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_rows(&mut self, row_idx: usize, count: usize) -> Result<()> {
|
||||||
|
Ok(self
|
||||||
|
.model
|
||||||
|
.insert_rows(self.current_sheet, row_idx as i32, count as i32)
|
||||||
|
.map_err(|e| anyhow!("Unable to insert row(s): {}", e))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_columns(&mut self, col_idx: usize, count: usize) -> Result<()> {
|
||||||
|
Ok(self
|
||||||
|
.model
|
||||||
|
.insert_columns(self.current_sheet, col_idx as i32, count as i32)
|
||||||
|
.map_err(|e| anyhow!("Unable to insert column(s): {}", e))?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the current sheets dimensions. This is a somewhat expensive calculation.
|
/// Get the current sheets dimensions. This is a somewhat expensive calculation.
|
||||||
pub fn get_dimensions(&self) -> Result<(usize, usize)> {
|
pub fn get_dimensions(&self) -> Result<WorksheetDimension> {
|
||||||
let dimensions = self.get_sheet()?.dimension();
|
Ok(self.get_sheet()?.dimension())
|
||||||
Ok((
|
}
|
||||||
dimensions.max_row.try_into()?,
|
|
||||||
dimensions.max_column.try_into()?,
|
// Get the size of the current sheet as a `(row_count, column_count)`
|
||||||
))
|
pub fn get_size(&self) -> Result<(usize, usize)> {
|
||||||
|
let sheet = &self.get_sheet()?.sheet_data;
|
||||||
|
let mut row_count = 0 as i32;
|
||||||
|
let mut col_count = 0 as i32;
|
||||||
|
for (ri, cols) in sheet.iter() {
|
||||||
|
row_count = max(*ri, row_count);
|
||||||
|
for (ci, _) in cols.iter() {
|
||||||
|
col_count = max(*ci, col_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((row_count as usize, col_count as usize))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select a sheet by name.
|
/// Select a sheet by name.
|
||||||
@ -129,19 +195,17 @@ impl Book {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_sheet(&self) -> Result<&Worksheet> {
|
pub(crate) fn get_sheet(&self) -> Result<&Worksheet> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.model
|
.model
|
||||||
.workbook
|
.workbook
|
||||||
.worksheet(self.current_sheet)
|
.worksheet(self.current_sheet)
|
||||||
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
|
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_sheet_mut(&mut self) -> Result<&mut Worksheet> {
|
impl Default for Book {
|
||||||
Ok(self
|
fn default() -> Self {
|
||||||
.model
|
Book::new(Model::new_empty("default_name", "en", "America/New_York").unwrap())
|
||||||
.workbook
|
|
||||||
.worksheet_mut(self.current_sheet)
|
|
||||||
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
73
src/book/test.rs
Normal file
73
src/book/test.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
use ironcalc::base::worksheet::WorksheetDimension;
|
||||||
|
|
||||||
|
use crate::ui::Address;
|
||||||
|
|
||||||
|
use super::Book;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_book_default() {
|
||||||
|
let mut book = Book::default();
|
||||||
|
let WorksheetDimension {
|
||||||
|
min_row,
|
||||||
|
max_row,
|
||||||
|
min_column,
|
||||||
|
max_column,
|
||||||
|
} = book.get_dimensions().expect("couldn't get dimensions");
|
||||||
|
assert_eq!((1, 1, 1, 1), (min_row, max_row, min_column, max_column));
|
||||||
|
assert_eq!((0, 0), book.get_size().expect("Failed to get size"));
|
||||||
|
let cell = book
|
||||||
|
.get_current_cell_contents()
|
||||||
|
.expect("couldn't get contents");
|
||||||
|
assert_eq!("", cell);
|
||||||
|
book.edit_current_cell("1").expect("failed to edit cell");
|
||||||
|
book.evaluate();
|
||||||
|
let cell = book
|
||||||
|
.get_current_cell_contents()
|
||||||
|
.expect("couldn't get contents");
|
||||||
|
assert_eq!("1", cell);
|
||||||
|
let cell = book
|
||||||
|
.get_current_cell_rendered()
|
||||||
|
.expect("couldn't get contents");
|
||||||
|
assert_eq!("1", cell);
|
||||||
|
let sheets = book.get_all_sheets_identifiers();
|
||||||
|
dbg!(&sheets);
|
||||||
|
assert_eq!(1, sheets.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert_rows() {
|
||||||
|
let mut book = Book::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_book_insert_cell_new_row() {
|
||||||
|
let mut book = Book::default();
|
||||||
|
book.update_entry(&Address { row: 2, col: 1 }, "1")
|
||||||
|
.expect("failed to edit cell");
|
||||||
|
book.evaluate();
|
||||||
|
dbg!(book.get_sheet().expect("Failed to get sheet"));
|
||||||
|
let WorksheetDimension {
|
||||||
|
min_row,
|
||||||
|
max_row,
|
||||||
|
min_column,
|
||||||
|
max_column,
|
||||||
|
} = book.get_dimensions().expect("couldn't get dimensions");
|
||||||
|
assert_eq!((2, 2, 1, 1), (min_row, max_row, min_column, max_column));
|
||||||
|
assert_eq!((2, 1), book.get_size().expect("Failed to get size"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_book_insert_cell_new_column() {
|
||||||
|
let mut book = Book::default();
|
||||||
|
book.insert_columns(1, 1).expect("couldn't insert rows");
|
||||||
|
book.update_entry(&Address { row: 1, col: 2 }, "1")
|
||||||
|
.expect("failed to edit cell");
|
||||||
|
let WorksheetDimension {
|
||||||
|
min_row,
|
||||||
|
max_row,
|
||||||
|
min_column,
|
||||||
|
max_column,
|
||||||
|
} = book.get_dimensions().expect("couldn't get dimensions");
|
||||||
|
assert_eq!((1, 1, 2, 2), (min_row, max_row, min_column, max_column));
|
||||||
|
assert_eq!((1, 2), book.get_size().expect("Failed to get size"));
|
||||||
|
}
|
12
src/main.rs
12
src/main.rs
@ -4,7 +4,6 @@ use clap::Parser;
|
|||||||
use ratatui;
|
use ratatui;
|
||||||
use ui::Workspace;
|
use ui::Workspace;
|
||||||
|
|
||||||
mod sheet;
|
|
||||||
mod ui;
|
mod ui;
|
||||||
mod book;
|
mod book;
|
||||||
|
|
||||||
@ -13,10 +12,15 @@ mod book;
|
|||||||
pub struct Args {
|
pub struct Args {
|
||||||
#[arg()]
|
#[arg()]
|
||||||
workbook: PathBuf,
|
workbook: PathBuf,
|
||||||
|
#[arg(default_value_t=String::from("en"), short, long)]
|
||||||
|
locale_name: String,
|
||||||
|
#[arg(default_value_t=String::from("America/New_York"), short, long)]
|
||||||
|
timezone_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(terminal: &mut ratatui::DefaultTerminal, name: PathBuf) -> anyhow::Result<ExitCode> {
|
fn run(terminal: &mut ratatui::DefaultTerminal, args: Args) -> anyhow::Result<ExitCode> {
|
||||||
let mut ws = Workspace::load(&name)?;
|
let mut ws = Workspace::load(&args.workbook, &args.locale_name, &args.timezone_name)?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|frame| ui::draw(frame, &mut ws))?;
|
terminal.draw(|frame| ui::draw(frame, &mut ws))?;
|
||||||
if let Some(code) = ws.handle_input()? {
|
if let Some(code) = ws.handle_input()? {
|
||||||
@ -30,7 +34,7 @@ fn main() -> anyhow::Result<ExitCode> {
|
|||||||
|
|
||||||
let mut terminal = ratatui::init();
|
let mut terminal = ratatui::init();
|
||||||
terminal.clear()?;
|
terminal.clear()?;
|
||||||
let app_result = run(&mut terminal, args.workbook);
|
let app_result = run(&mut terminal, args);
|
||||||
ratatui::restore();
|
ratatui::restore();
|
||||||
app_result
|
app_result
|
||||||
}
|
}
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
//! DataModel for a SpreadSheet
|
|
||||||
//!
|
|
||||||
//! # Overview
|
|
||||||
//!
|
|
||||||
//! Sheets can contain a [Tbl]. Tbl's contain a collection of [Address] to [Computable]
|
|
||||||
//! associations. From this we can compute the dimensions of a Tbl as well as render
|
|
||||||
//! them into a [Table] Widget.
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use csvx;
|
|
||||||
|
|
||||||
use std::borrow::Borrow;
|
|
||||||
|
|
||||||
/// The Address in a [Tbl].
|
|
||||||
#[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
|
|
||||||
pub struct Address {
|
|
||||||
pub row: usize,
|
|
||||||
pub col: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Address {
|
|
||||||
pub fn new(row: usize, col: usize) -> Self {
|
|
||||||
Self { row, col }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A single table of addressable computable values.
|
|
||||||
pub struct Tbl {
|
|
||||||
pub csv: csvx::Table,
|
|
||||||
pub location: Address,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tbl {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::from_str("").unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dimensions(&self) -> (usize, usize) {
|
|
||||||
let table = self.csv.get_raw_table();
|
|
||||||
let row_count = table.len();
|
|
||||||
if row_count > 0 {
|
|
||||||
let col_count = table.first().unwrap().len();
|
|
||||||
return (row_count, col_count);
|
|
||||||
}
|
|
||||||
return (0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_str<S: Borrow<str>>(input: S) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
csv: csvx::Table::new(input)
|
|
||||||
.map_err(|e| anyhow!("Error parsing table from csv text: {}", e))?,
|
|
||||||
location: Address::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_raw_value(&self, Address {row, col}: &Address) -> String {
|
|
||||||
self.csv.get_raw_table()[*row][*col].clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn move_to(&mut self, addr: Address) -> Result<()> {
|
|
||||||
let (row, col) = self.dimensions();
|
|
||||||
if addr.row >= row || addr.col >= col {
|
|
||||||
return Err(anyhow!("Invalid address to move to: {:?}", addr));
|
|
||||||
}
|
|
||||||
self.location = addr;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_entry(&mut self, address: &Address, value: String) -> Result<()> {
|
|
||||||
let (row, col) = self.dimensions();
|
|
||||||
if address.row >= row {
|
|
||||||
// then we need to add rows.
|
|
||||||
for r in row..=address.row {
|
|
||||||
self.csv.insert_y(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if address.col >= col {
|
|
||||||
for c in col..=address.col {
|
|
||||||
self.csv.insert_x(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(self
|
|
||||||
.csv
|
|
||||||
.update(address.col, address.row, value.trim())?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
@ -1,13 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_dimensions_calculation() {
|
|
||||||
let mut tbl = Tbl::new();
|
|
||||||
tbl.update_entry(&Address::new(0, 0), String::new()).unwrap();
|
|
||||||
assert_eq!((1, 1), tbl.dimensions());
|
|
||||||
tbl.update_entry(&Address::new(0, 10), String::new()).unwrap();
|
|
||||||
assert_eq!((1, 11), tbl.dimensions());
|
|
||||||
tbl.update_entry(&Address::new(20, 5), String::new()).unwrap();
|
|
||||||
assert_eq!((21, 11), tbl.dimensions());
|
|
||||||
}
|
|
||||||
|
|
243
src/ui/mod.rs
243
src/ui/mod.rs
@ -1,16 +1,12 @@
|
|||||||
//! Ui rendering logic
|
//! Ui rendering logic
|
||||||
|
|
||||||
use std::{
|
use std::{path::PathBuf, process::ExitCode};
|
||||||
fs::File,
|
|
||||||
io::Read,
|
|
||||||
path::PathBuf,
|
|
||||||
process::ExitCode,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::sheet::{Address, Tbl};
|
use crate::book::Book;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::Result;
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
|
use ironcalc::base::worksheet::WorksheetDimension;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
self,
|
self,
|
||||||
layout::{Constraint, Flex, Layout},
|
layout::{Constraint, Flex, Layout},
|
||||||
@ -35,21 +31,41 @@ pub struct AppState {
|
|||||||
pub table_state: TableState,
|
pub table_state: TableState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(jwall): This should probably move to a different module.
|
||||||
|
/// The Address in a Table.
|
||||||
|
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
|
||||||
|
pub struct Address {
|
||||||
|
pub row: usize,
|
||||||
|
pub col: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Address {
|
||||||
|
pub fn new(row: usize, col: usize) -> Self {
|
||||||
|
Self { row, col }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Address {
|
||||||
|
fn default() -> Self {
|
||||||
|
Address::new(1, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Interaction Modalities
|
// Interaction Modalities
|
||||||
// * Navigate
|
// * Navigate
|
||||||
// * Edit
|
// * Edit
|
||||||
pub struct Workspace<'ws> {
|
pub struct Workspace<'ws> {
|
||||||
name: PathBuf,
|
name: PathBuf,
|
||||||
tbl: Tbl,
|
book: Book,
|
||||||
state: AppState,
|
state: AppState,
|
||||||
text_area: TextArea<'ws>,
|
text_area: TextArea<'ws>,
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'ws> Workspace<'ws> {
|
impl<'ws> Workspace<'ws> {
|
||||||
pub fn new(tbl: Tbl, name: PathBuf) -> Self {
|
pub fn new(book: Book, name: PathBuf) -> Self {
|
||||||
let mut ws = Self {
|
let mut ws = Self {
|
||||||
tbl,
|
book,
|
||||||
name,
|
name,
|
||||||
state: AppState::default(),
|
state: AppState::default(),
|
||||||
text_area: reset_text_area("".to_owned()),
|
text_area: reset_text_area("".to_owned()),
|
||||||
@ -59,61 +75,52 @@ impl<'ws> Workspace<'ws> {
|
|||||||
ws
|
ws
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(path: &PathBuf) -> Result<Self> {
|
pub fn load(path: &PathBuf, locale: &str, tz: &str) -> Result<Self> {
|
||||||
let input = if path.exists() {
|
let book = if path.exists() {
|
||||||
if path.is_file() {
|
Book::new_from_xlsx_with_locale(&path.to_string_lossy().to_string(), locale, tz)?
|
||||||
let mut f = File::open(path)?;
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
let _ = f.read_to_end(&mut buf)?;
|
|
||||||
String::from_utf8(buf).context(format!("Error reading file: {:?}", path))?
|
|
||||||
} else {
|
|
||||||
return Err(anyhow!("Not a valid path: {}", path.to_string_lossy().to_string()));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
String::from(",,,\n,,,\n")
|
Book::default()
|
||||||
};
|
};
|
||||||
let mut tbl = Tbl::from_str(input)?;
|
//book.move_to(Address { row: 0, col: 0 })?;
|
||||||
tbl.move_to(Address { row: 0, col: 0 })?;
|
Ok(Workspace::new(book, path.clone()))
|
||||||
Ok(Workspace::new(
|
|
||||||
tbl,
|
|
||||||
path.clone(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_down(&mut self) -> Result<()> {
|
pub fn move_down(&mut self) -> Result<()> {
|
||||||
let mut loc = self.tbl.location.clone();
|
let mut loc = self.book.location.clone();
|
||||||
let (row, _) = self.tbl.dimensions();
|
let WorksheetDimension { min_row: _, max_row, min_column: _, max_column: _ } = self.book.get_dimensions()?;
|
||||||
if loc.row < row - 1 {
|
if loc.row <= max_row as usize {
|
||||||
loc.row += 1;
|
loc.row += 1;
|
||||||
self.tbl.move_to(loc)?;
|
self.book.move_to(loc)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_up(&mut self) -> Result<()> {
|
pub fn move_up(&mut self) -> Result<()> {
|
||||||
let mut loc = self.tbl.location.clone();
|
let mut loc = self.book.location.clone();
|
||||||
if loc.row > 0 {
|
let WorksheetDimension { min_row, max_row: _, min_column: _, max_column: _ } = self.book.get_dimensions()?;
|
||||||
|
if loc.row > min_row as usize {
|
||||||
loc.row -= 1;
|
loc.row -= 1;
|
||||||
self.tbl.move_to(loc)?;
|
self.book.move_to(loc)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_left(&mut self) -> Result<()> {
|
pub fn move_left(&mut self) -> Result<()> {
|
||||||
let mut loc = self.tbl.location.clone();
|
let mut loc = self.book.location.clone();
|
||||||
if loc.col > 0 {
|
let WorksheetDimension { min_row: _, max_row: _, min_column, max_column: _ } = self.book.get_dimensions()?;
|
||||||
|
if loc.col > min_column as usize {
|
||||||
loc.col -= 1;
|
loc.col -= 1;
|
||||||
self.tbl.move_to(loc)?;
|
self.book.move_to(loc)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_right(&mut self) -> Result<()> {
|
pub fn move_right(&mut self) -> Result<()> {
|
||||||
let mut loc = self.tbl.location.clone();
|
let mut loc = self.book.location.clone();
|
||||||
let (_, col) = self.tbl.dimensions();
|
let WorksheetDimension { min_row: _, max_row: _, min_column: _, max_column} = self.book.get_dimensions()?;
|
||||||
if loc.col < col - 1 {
|
if loc.col < max_column as usize {
|
||||||
loc.col += 1;
|
loc.col += 1;
|
||||||
self.tbl.move_to(loc)?;
|
self.book.move_to(loc)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -146,7 +153,8 @@ impl<'ws> Workspace<'ws> {
|
|||||||
"* ESC: Exit edit mode".into(),
|
"* ESC: Exit edit mode".into(),
|
||||||
"Otherwise edit as normal".into(),
|
"Otherwise edit as normal".into(),
|
||||||
]),
|
]),
|
||||||
}).block(info_block)
|
})
|
||||||
|
.block(info_block)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_edit_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
fn handle_edit_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
||||||
@ -157,8 +165,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.text_area.set_cursor_style(Style::default());
|
self.text_area.set_cursor_style(Style::default());
|
||||||
let contents = self.text_area.lines().join("\n");
|
let contents = self.text_area.lines().join("\n");
|
||||||
if self.dirty {
|
if self.dirty {
|
||||||
let loc = self.tbl.location.clone();
|
self.book.edit_current_cell(contents)?;
|
||||||
self.tbl.update_entry(&loc, contents)?;
|
|
||||||
}
|
}
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@ -189,19 +196,19 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.save_file()?;
|
self.save_file()?;
|
||||||
}
|
}
|
||||||
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
|
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
let (row, _) = self.tbl.dimensions();
|
let WorksheetDimension { min_row: _, max_row, min_column: _, max_column: _ } = self.book.get_dimensions()?;
|
||||||
self.tbl.csv.insert_y(row);
|
self.book.insert_rows(max_row as usize, 1)?;
|
||||||
let (row, _) = self.tbl.dimensions();
|
let WorksheetDimension { min_row: _, max_row, min_column: _, max_column: _ } = self.book.get_dimensions()?;
|
||||||
let mut loc = self.tbl.location.clone();
|
let mut loc = self.book.location.clone();
|
||||||
if loc.row < row - 1 {
|
if loc.row < max_row as usize {
|
||||||
loc.row = row - 1;
|
loc.row = (max_row - 1) as usize;
|
||||||
self.tbl.move_to(loc)?;
|
self.book.move_to(loc)?;
|
||||||
}
|
}
|
||||||
self.handle_movement_change();
|
self.handle_movement_change();
|
||||||
}
|
}
|
||||||
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
|
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
let (_, col) = self.tbl.dimensions();
|
let WorksheetDimension { min_row: _, max_row: _, min_column: _, max_column} = self.book.get_dimensions()?;
|
||||||
self.tbl.csv.insert_x(col);
|
self.book.insert_columns(max_column as usize, 1)?;
|
||||||
}
|
}
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
return Ok(Some(ExitCode::SUCCESS));
|
return Ok(Some(ExitCode::SUCCESS));
|
||||||
@ -236,13 +243,15 @@ impl<'ws> Workspace<'ws> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_movement_change(&mut self) {
|
fn handle_movement_change(&mut self) {
|
||||||
let contents = self.tbl.get_raw_value(&self.tbl.location);
|
let contents = self
|
||||||
|
.book
|
||||||
|
.get_current_cell_contents()
|
||||||
|
.expect("Unexpected failure getting current cell contents");
|
||||||
self.text_area = reset_text_area(contents);
|
self.text_area = reset_text_area(contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_file(&self) -> Result<()> {
|
fn save_file(&self) -> Result<()> {
|
||||||
let contents = self.tbl.csv.export_raw_table().map_err(|e| anyhow::anyhow!("Error serializing to csv: {:?}", e))?;
|
self.book.save_to_xlsx(&self.name.to_string_lossy().to_string())?;
|
||||||
std::fs::write(&self.name, contents)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -260,9 +269,12 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
|
|||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
let outer_block = Block::bordered()
|
let outer_block = Block::bordered()
|
||||||
.title(Line::from(self.name
|
.title(Line::from(
|
||||||
.file_name().map(|p| p.to_string_lossy().to_string())
|
self.name
|
||||||
.unwrap_or_else(|| String::from("Unknown"))))
|
.file_name()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| String::from("Unknown")),
|
||||||
|
))
|
||||||
.title_bottom(match &self.state.modality {
|
.title_bottom(match &self.state.modality {
|
||||||
Modality::Navigate => "navigate",
|
Modality::Navigate => "navigate",
|
||||||
Modality::CellEdit => "edit",
|
Modality::CellEdit => "edit",
|
||||||
@ -270,22 +282,26 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
|
|||||||
.title_bottom(
|
.title_bottom(
|
||||||
Line::from(format!(
|
Line::from(format!(
|
||||||
"{},{}",
|
"{},{}",
|
||||||
self.tbl.location.row, self.tbl.location.col
|
self.book.location.row, self.book.location.col
|
||||||
))
|
))
|
||||||
.right_aligned(),
|
.right_aligned(),
|
||||||
);
|
);
|
||||||
let [edit_rect, table_rect, info_rect] =
|
let [edit_rect, table_rect, info_rect] = Layout::vertical(&[
|
||||||
Layout::vertical(&[Constraint::Fill(1), Constraint::Fill(30), Constraint::Fill(9)])
|
Constraint::Fill(1),
|
||||||
.vertical_margin(2)
|
Constraint::Fill(30),
|
||||||
.horizontal_margin(2)
|
Constraint::Fill(9),
|
||||||
.flex(Flex::Legacy)
|
])
|
||||||
.areas(area.clone());
|
.vertical_margin(2)
|
||||||
|
.horizontal_margin(2)
|
||||||
|
.flex(Flex::Legacy)
|
||||||
|
.areas(area.clone());
|
||||||
outer_block.render(area, buf);
|
outer_block.render(area, buf);
|
||||||
self.text_area.render(edit_rect, buf);
|
self.text_area.render(edit_rect, buf);
|
||||||
let table_block = Block::bordered();
|
let table_block = Block::bordered();
|
||||||
let table = Table::from(&self.tbl).block(table_block);
|
let table_inner: Table = TryFrom::try_from(&self.book).expect("");
|
||||||
|
let table = table_inner.block(table_block);
|
||||||
// https://docs.rs/ratatui/latest/ratatui/widgets/struct.TableState.html
|
// https://docs.rs/ratatui/latest/ratatui/widgets/struct.TableState.html
|
||||||
let Address { row, col } = self.tbl.location;
|
let Address { row, col } = self.book.location;
|
||||||
// TODO(zaphar): Apparently scrolling by columns doesn't work?
|
// TODO(zaphar): Apparently scrolling by columns doesn't work?
|
||||||
self.state.table_state.select_cell(Some((row, col)));
|
self.state.table_state.select_cell(Some((row, col)));
|
||||||
self.state.table_state.select_column(Some(col));
|
self.state.table_state.select_column(Some(col));
|
||||||
@ -298,62 +314,65 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COLNAMES: [&'static str; 26] = [
|
const COLNAMES: [&'static str; 26] = [
|
||||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R",
|
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S",
|
||||||
"S", "T", "U", "V", "W", "X", "Y", "Z",
|
"T", "U", "V", "W", "X", "Y", "Z",
|
||||||
];
|
];
|
||||||
|
|
||||||
impl<'t> From<&Tbl> for Table<'t> {
|
// TODO(jwall): Maybe this should be TryFrom?
|
||||||
fn from(value: &Tbl) -> Self {
|
impl<'t, 'book: 't> TryFrom<&'book Book> for Table<'t> {
|
||||||
let (_, cols) = value.dimensions();
|
fn try_from(value: &'book Book) -> std::result::Result<Self, Self::Error> {
|
||||||
let rows: Vec<Row> = value
|
// TODO(zaphar): This is apparently expensive. Maybe we can cache it somehow?
|
||||||
.csv
|
// We should do the correct thing here if this fails
|
||||||
.get_calculated_table()
|
let WorksheetDimension { min_row, max_row, min_column, max_column } = value.get_dimensions()?;
|
||||||
.iter()
|
let (row_count, col_count) = ((max_row - min_row) as usize, (max_column - min_column) as usize);
|
||||||
.enumerate()
|
let rows: Vec<Row> = (1..=row_count)
|
||||||
.map(|(ri, r)| {
|
.into_iter()
|
||||||
let cells =
|
.map(|ri| {
|
||||||
vec![Cell::new(format!("{}", ri))]
|
let cells: Vec<Cell> = (1..=col_count)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(r.iter().enumerate().map(|(ci, v)| {
|
.map(|ci| {
|
||||||
let content = format!("{}", v);
|
// TODO(zaphar): Is this safe?
|
||||||
let cell = Cell::new(Text::raw(content));
|
let content = value.get_cell_addr_rendered(ri, ci).unwrap();
|
||||||
match (value.location.row == ri, value.location.col == ci) {
|
let cell = Cell::new(Text::raw(content));
|
||||||
(true, true) => cell.fg(Color::White).underlined(),
|
match (value.location.row == ri, value.location.col == ci) {
|
||||||
_ => cell
|
(true, true) => cell.fg(Color::White).underlined(),
|
||||||
.bg(if ri % 2 == 0 {
|
_ => cell
|
||||||
Color::Rgb(57, 61, 71)
|
.bg(if ri % 2 == 0 {
|
||||||
} else {
|
Color::Rgb(57, 61, 71)
|
||||||
Color::Rgb(165, 169, 160)
|
} else {
|
||||||
})
|
Color::Rgb(165, 169, 160)
|
||||||
.fg(if ri % 2 == 0 {
|
})
|
||||||
Color::White
|
.fg(if ri % 2 == 0 {
|
||||||
} else {
|
Color::White
|
||||||
Color::Rgb(31, 32, 34)
|
} else {
|
||||||
}),
|
Color::Rgb(31, 32, 34)
|
||||||
}
|
}),
|
||||||
.bold()
|
}
|
||||||
}));
|
.bold()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
Row::new(cells)
|
Row::new(cells)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// TODO(zaphar): Handle the double letter column names
|
let mut constraints: Vec<Constraint> = Vec::new();
|
||||||
let mut header = Vec::with_capacity(cols+1);
|
constraints.push(Constraint::Max(5));
|
||||||
|
for _ in 0..col_count {
|
||||||
|
constraints.push(Constraint::Min(5));
|
||||||
|
}
|
||||||
|
let mut header = Vec::with_capacity(col_count as usize);
|
||||||
header.push(Cell::new(""));
|
header.push(Cell::new(""));
|
||||||
header.extend((0..cols).map(|i| {
|
header.extend((0..(col_count as usize)).map(|i| {
|
||||||
let count = (i / 26) + 1;
|
let count = (i / 26) + 1;
|
||||||
Cell::new(COLNAMES[i % 26].repeat(count))
|
Cell::new(COLNAMES[i % 26].repeat(count))
|
||||||
}));
|
}));
|
||||||
let mut constraints: Vec<Constraint> = Vec::new();
|
Ok(Table::new(rows, constraints)
|
||||||
constraints.push(Constraint::Max(5));
|
|
||||||
for _ in 0..cols {
|
|
||||||
constraints.push(Constraint::Min(5));
|
|
||||||
}
|
|
||||||
Table::new(rows, constraints)
|
|
||||||
.block(Block::bordered())
|
.block(Block::bordered())
|
||||||
.header(Row::new(header).underlined())
|
.header(Row::new(header).underlined())
|
||||||
.column_spacing(1)
|
.column_spacing(1)
|
||||||
.flex(Flex::SpaceAround)
|
.flex(Flex::SpaceAround))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Error = anyhow::Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(frame: &mut Frame, ws: &mut Workspace) {
|
pub fn draw(frame: &mut Frame, ws: &mut Workspace) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user