mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 13:29:48 -04:00
commit
49786fd6d9
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -482,9 +482,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
|
||||
checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
|
||||
dependencies = [
|
||||
"csv-core",
|
||||
"itoa",
|
||||
@ -501,16 +501,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csvx"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92081efd8b1d03f5a1bf242876cfdd8fa2bf9fe521ddb2d31f8747dfa2dd2cb7"
|
||||
dependencies = [
|
||||
"csv",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
@ -1507,7 +1497,7 @@ dependencies = [
|
||||
"clap",
|
||||
"colorsys",
|
||||
"crossterm",
|
||||
"csvx",
|
||||
"csv",
|
||||
"futures",
|
||||
"ironcalc",
|
||||
"ratatui",
|
||||
|
@ -9,7 +9,6 @@ edition = "2021"
|
||||
anyhow = { version = "1.0.91", features = ["backtrace"] }
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
crossterm = { version = "0.28.1", features = ["event-stream", "serde"] }
|
||||
csvx = "0.1.17"
|
||||
# this revision introduces a way to get the Model back out of the UserModel
|
||||
ironcalc = { git = "https://github.com/ironcalc/IronCalc" }
|
||||
futures = "0.3.31"
|
||||
@ -21,3 +20,4 @@ slice-utils = { git = "https://dev.zaphar.net/zaphar/slice-cursor-rs.git" }
|
||||
serde_json = "1.0.133"
|
||||
colorsys = "0.6.7"
|
||||
tui-markdown = { version = "0.3.1", features = [] }
|
||||
csv = "1.3.1"
|
||||
|
@ -12,6 +12,7 @@ The currently supported commands are:
|
||||
* `new-sheet [name]` Creates a new sheet. If the name is provided then uses that. If omitted then uses a default sheet name.
|
||||
* `select-sheet <name>` Select a sheet by name.
|
||||
* `edit <path>` Edit a new spreadsheet at the current path. `e` is a shorthand alias for this command.
|
||||
* `export-csv <path>` Export the current sheet to a csv file at `<path>`.
|
||||
* `quit` Quits the application. `q` is a shorthand alias for this command.
|
||||
|
||||
<aside>Note that in the case of `quit` and `edit` that we do not currently
|
||||
|
@ -23,7 +23,6 @@ pub(crate) const COL_PIXELS: f64 = 5.0;
|
||||
pub(crate) const LAST_COLUMN: i32 = 16_384;
|
||||
pub(crate) const LAST_ROW: i32 = 1_048_576;
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AddressRange<'book> {
|
||||
pub start: &'book Address,
|
||||
@ -37,7 +36,11 @@ impl<'book> AddressRange<'book> {
|
||||
for ri in row_range.iter() {
|
||||
let mut row = Vec::with_capacity(col_range.len());
|
||||
for ci in col_range.iter() {
|
||||
row.push(Address { sheet: self.start.sheet, row: *ri, col: *ci });
|
||||
row.push(Address {
|
||||
sheet: self.start.sheet,
|
||||
row: *ri,
|
||||
col: *ci,
|
||||
});
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
@ -49,7 +52,11 @@ impl<'book> AddressRange<'book> {
|
||||
let mut rows = Vec::with_capacity(row_range.len() * col_range.len());
|
||||
for ri in row_range.iter() {
|
||||
for ci in col_range.iter() {
|
||||
rows.push(Address { sheet: self.start.sheet, row: *ri, col: *ci });
|
||||
rows.push(Address {
|
||||
sheet: self.start.sheet,
|
||||
row: *ri,
|
||||
col: *ci,
|
||||
});
|
||||
}
|
||||
}
|
||||
rows
|
||||
@ -112,6 +119,58 @@ impl Book {
|
||||
)?))
|
||||
}
|
||||
|
||||
pub fn csv_for_sheet<W>(&self, sheet: u32, sink: W) -> Result<()>
|
||||
where
|
||||
W: std::io::Write,
|
||||
{
|
||||
let rows = self.get_export_rows_for_sheet(sheet)?;
|
||||
let mut writer = csv::Writer::from_writer(sink);
|
||||
for row in rows {
|
||||
writer.write_record(row)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_export_rows(&self) -> Result<Vec<Vec<String>>> {
|
||||
let sheet = self.location.sheet;
|
||||
Ok(self.get_export_rows_for_sheet(sheet)?)
|
||||
}
|
||||
|
||||
pub fn get_export_rows_for_sheet(&self, sheet: u32) -> Result<Vec<Vec<String>>, anyhow::Error> {
|
||||
let worksheet = self
|
||||
.model
|
||||
.get_model()
|
||||
.workbook
|
||||
.worksheet(sheet)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
let mut max_row = 0;
|
||||
let mut max_col = 0;
|
||||
for (r, cols) in worksheet.sheet_data.iter() {
|
||||
if max_row <= *r {
|
||||
max_row = *r;
|
||||
}
|
||||
for (c, _) in cols.iter() {
|
||||
if max_col <= *c {
|
||||
max_col = *c;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut rows = Vec::new();
|
||||
for ri in 0..=max_row {
|
||||
let mut row = Vec::new();
|
||||
for ci in 0..=max_col {
|
||||
let cell_content = self.get_cell_addr_rendered(&Address {
|
||||
sheet,
|
||||
row: ri as usize,
|
||||
col: ci as usize,
|
||||
})?;
|
||||
row.push(cell_content);
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Evaluate the spreadsheet calculating formulas and style changes.
|
||||
/// This can be an expensive operation.
|
||||
pub fn evaluate(&mut self) {
|
||||
@ -123,6 +182,15 @@ impl Book {
|
||||
Ok(Self::from_model(load_from_xlsx(path, locale, tz)?))
|
||||
}
|
||||
|
||||
/// Save a sheet in the book to a csv file
|
||||
pub fn save_sheet_to_csv(&self, sheet: u32, path: &str) -> Result<()> {
|
||||
let file_path = std::path::Path::new(path);
|
||||
let file = std::fs::File::create(file_path)?;
|
||||
let writer = std::io::BufWriter::new(file);
|
||||
self.csv_for_sheet(sheet, writer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save book to an xlsx file.
|
||||
pub fn save_to_xlsx(&mut self, path: &str) -> Result<()> {
|
||||
// TODO(zaphar): Currently overwrites. Should we prompt in this case?
|
||||
@ -604,7 +672,13 @@ impl Book {
|
||||
.get_model()
|
||||
.workbook
|
||||
.worksheet(self.location.sheet)
|
||||
.map_err(|s| anyhow!("Invalid Worksheet id: {}: error: {}", self.location.sheet, s))?)
|
||||
.map_err(|s| {
|
||||
anyhow!(
|
||||
"Invalid Worksheet id: {}: error: {}",
|
||||
self.location.sheet,
|
||||
s
|
||||
)
|
||||
})?)
|
||||
}
|
||||
|
||||
pub(crate) fn get_sheet_name_by_idx(&self, idx: usize) -> Result<&str> {
|
||||
@ -636,7 +710,15 @@ impl Default for Book {
|
||||
fn default() -> Self {
|
||||
let mut book =
|
||||
Book::new(UserModel::new_empty("default_name", "en", "America/New_York").unwrap());
|
||||
book.update_cell(&Address { sheet: 0, row: 1, col: 1 }, "").unwrap();
|
||||
book.update_cell(
|
||||
&Address {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
col: 1,
|
||||
},
|
||||
"",
|
||||
)
|
||||
.unwrap();
|
||||
book
|
||||
}
|
||||
}
|
||||
|
@ -108,3 +108,21 @@ fn test_book_col_size() {
|
||||
book.set_col_size(1, 20).expect("Failed to set column size");
|
||||
assert_eq!(20, book.get_col_size(1).expect("Failed to get column size"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_get_exportable_rows() {
|
||||
let mut book = Book::default();
|
||||
book.update_cell(&Address { sheet: 0, row: 1, col: 3 }, "1-3")
|
||||
.expect("failed to edit cell");
|
||||
book.update_cell(&Address { sheet: 0, row: 3, col: 6 }, "3-6")
|
||||
.expect("failed to edit cell");
|
||||
|
||||
let rows = book.get_export_rows().expect("Failed to get export rows");
|
||||
assert_eq!(4, rows.len());
|
||||
assert_eq!(rows, vec![
|
||||
vec!["", "" , "", "", "", "", ""],
|
||||
vec!["", "" , "", "1-3", "", "", ""],
|
||||
vec!["", "" , "", "", "", "", ""],
|
||||
vec!["", "" , "", "", "", "", "3-6"],
|
||||
]);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ pub enum Cmd<'a> {
|
||||
SelectSheet(&'a str),
|
||||
Edit(&'a str),
|
||||
Help(Option<&'a str>),
|
||||
ExportCsv(&'a str),
|
||||
Quit,
|
||||
}
|
||||
|
||||
@ -35,10 +36,14 @@ pub fn parse<'cmd, 'i: 'cmd>(input: &'i str) -> Result<Option<Cmd<'cmd>>, &'stat
|
||||
if let Some(cmd) = try_consume_insert_row(cursor.clone())? {
|
||||
return Ok(Some(cmd));
|
||||
}
|
||||
//// try consume insert-col command.
|
||||
// try consume insert-col command.
|
||||
if let Some(cmd) = try_consume_insert_column(cursor.clone())? {
|
||||
return Ok(Some(cmd));
|
||||
}
|
||||
// Try consume export
|
||||
if let Some(cmd) = try_consume_export_csv(cursor.clone())? {
|
||||
return Ok(Some(cmd));
|
||||
}
|
||||
// try consume edit command.
|
||||
if let Some(cmd) = try_consume_edit(cursor.clone())? {
|
||||
return Ok(Some(cmd));
|
||||
@ -99,7 +104,7 @@ fn try_consume_write<'cmd, 'i: 'cmd>(
|
||||
return Ok(None);
|
||||
}
|
||||
if input.remaining() > 0 && !is_ws(&mut input) {
|
||||
return Err("Invalid command: Did you mean to type `write <arg>`?");
|
||||
return Err("Invalid command: Did you mean to type `write <path>`?");
|
||||
}
|
||||
let arg = input.span(0..).trim();
|
||||
return Ok(Some(Cmd::Write(if arg.is_empty() {
|
||||
@ -109,6 +114,23 @@ fn try_consume_write<'cmd, 'i: 'cmd>(
|
||||
})));
|
||||
}
|
||||
|
||||
fn try_consume_export_csv<'cmd, 'i: 'cmd>(
|
||||
mut input: StrCursor<'i>,
|
||||
) -> Result<Option<Cmd<'cmd>>, &'static str> {
|
||||
const LONG: &'static str = "export-csv";
|
||||
|
||||
if compare(input.clone(), LONG) {
|
||||
input.seek(LONG.len());
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
if input.remaining() == 0 || !is_ws(&mut input) {
|
||||
return Err("Invalid command: Did you mean to type `export <path>`?");
|
||||
}
|
||||
let arg = input.span(0..).trim();
|
||||
return Ok(Some(Cmd::ExportCsv(arg)));
|
||||
}
|
||||
|
||||
fn try_consume_new_sheet<'cmd, 'i: 'cmd>(
|
||||
mut input: StrCursor<'i>,
|
||||
) -> Result<Option<Cmd<'cmd>>, &'static str> {
|
||||
|
@ -425,6 +425,10 @@ impl<'ws> Workspace<'ws> {
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
Ok(Some(Cmd::ExportCsv(path))) => {
|
||||
self.book.save_sheet_to_csv(self.book.location.sheet, path)?;
|
||||
Ok(None)
|
||||
}
|
||||
Ok(Some(Cmd::InsertColumns(count))) => {
|
||||
self.book.insert_columns(self.book.location.col, count)?;
|
||||
self.book.evaluate();
|
||||
|
@ -217,6 +217,17 @@ fn test_cmd_new_sheet_with_name() {
|
||||
assert_eq!(cmd, Cmd::NewSheet(Some("test")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cmd_export() {
|
||||
let input = "export-csv test.csv";
|
||||
let result = parse(input);
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
assert!(output.is_some());
|
||||
let cmd = output.unwrap();
|
||||
assert_eq!(cmd, Cmd::Export("test.csv"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cmd_new_sheet_no_name() {
|
||||
let input = "new-sheet";
|
||||
|
Loading…
x
Reference in New Issue
Block a user