wip: Use the UserModel

Closes #25
This commit is contained in:
Jeremy Wall 2025-02-15 18:27:59 -05:00
commit f392cb743f
9 changed files with 545 additions and 412 deletions

117
Cargo.lock generated
View File

@ -28,15 +28,6 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
dependencies = [
"memchr",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@ -182,12 +173,6 @@ dependencies = [
"syn",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
@ -293,9 +278,9 @@ dependencies = [
[[package]]
name = "chrono-tz"
version = "0.9.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f"
dependencies = [
"chrono",
"chrono-tz-build",
@ -304,12 +289,11 @@ dependencies = [
[[package]]
name = "chrono-tz-build"
version = "0.3.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7"
dependencies = [
"parse-zoneinfo",
"phf",
"phf_codegen",
]
@ -431,7 +415,7 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.6.0",
"bitflags",
"crossterm_winapi",
"futures-core",
"mio",
@ -483,19 +467,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "csv-sniffer"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b8e952164bb270a505d6cb6136624174c34cfb9abd16e0011f5e53058317f39"
dependencies = [
"bitflags 1.3.2",
"csv",
"csv-core",
"memchr",
"regex 0.2.11",
]
[[package]]
name = "csvx"
version = "0.1.17"
@ -869,8 +840,8 @@ dependencies = [
[[package]]
name = "ironcalc"
version = "0.2.0"
source = "git+https://github.com/ironcalc/IronCalc#98dc557a017b2ad640fb46eece17afda14177e59"
version = "0.3.0"
source = "git+https://github.com/ironcalc/IronCalc#b2c5027f56a16a0c606b01a071b816b941972aef"
dependencies = [
"bitcode",
"chrono",
@ -885,18 +856,17 @@ dependencies = [
[[package]]
name = "ironcalc_base"
version = "0.2.0"
source = "git+https://github.com/ironcalc/IronCalc#98dc557a017b2ad640fb46eece17afda14177e59"
version = "0.3.0"
source = "git+https://github.com/ironcalc/IronCalc#b2c5027f56a16a0c606b01a071b816b941972aef"
dependencies = [
"bitcode",
"chrono",
"chrono-tz",
"csv",
"csv-sniffer",
"js-sys",
"once_cell",
"rand",
"regex 1.11.1",
"regex",
"ryu",
"serde",
]
@ -949,12 +919,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.161"
@ -1085,7 +1049,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
dependencies = [
"regex 1.11.1",
"regex",
]
[[package]]
@ -1251,7 +1215,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [
"bitflags 2.6.0",
"bitflags",
"cassowary",
"compact_str",
"crossterm",
@ -1281,20 +1245,7 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "regex"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384"
dependencies = [
"aho-corasick 0.6.10",
"memchr",
"regex-syntax 0.5.6",
"thread_local",
"utf8-ranges",
"bitflags",
]
[[package]]
@ -1303,10 +1254,10 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick 1.1.3",
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax 0.8.5",
"regex-syntax",
]
[[package]]
@ -1315,18 +1266,9 @@ version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick 1.1.3",
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-syntax"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7"
dependencies = [
"ucd-util",
"regex-syntax",
]
[[package]]
@ -1370,7 +1312,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"regex 1.11.1",
"regex",
"relative-path",
"rustc_version",
"syn",
@ -1398,7 +1340,7 @@ version = "0.38.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
dependencies = [
"bitflags 2.6.0",
"bitflags",
"errno",
"libc",
"linux-raw-sys",
@ -1636,15 +1578,6 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
dependencies = [
"lazy_static",
]
[[package]]
name = "time"
version = "0.3.36"
@ -1722,12 +1655,6 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-util"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003"
[[package]]
name = "unicode-ident"
version = "1.0.13"
@ -1763,12 +1690,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "utf8-ranges"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
[[package]]
name = "utf8parse"
version = "0.2.2"

View File

@ -10,13 +10,14 @@ 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"
ratatui = "0.29.0"
thiserror = "1.0.65"
tui-textarea = "0.7.0"
tui-prompts = "0.5.0"
slice-utils = { git = "https://dev.zaphar.net/zaphar/slice-cursor-rs.git", ref = "main" }
slice-utils = { git = "https://dev.zaphar.net/zaphar/slice-cursor-rs.git" }
tui-popup = "0.6.0"
serde_json = "1.0.133"
colorsys = "0.6.7"

1
result Symbolic link
View File

@ -0,0 +1 @@
/nix/store/k826wsv9zc73jamdff1yl1rky2bw9lc6-sheetui-0.1.0

View File

@ -3,9 +3,10 @@ use std::cmp::max;
use anyhow::{anyhow, Result};
use ironcalc::{
base::{
types::{Border, Col, Fill, Font, Row, SheetData, Style, Worksheet},
expressions::types::Area,
types::{SheetData, Style, Worksheet},
worksheet::WorksheetDimension,
Model,
Model, UserModel,
},
export::save_xlsx_to_writer,
import::load_from_xlsx,
@ -16,7 +17,12 @@ use crate::ui::Address;
#[cfg(test)]
mod test;
const COL_PIXELS: f64 = 5.0;
pub(crate) const COL_PIXELS: f64 = 5.0;
// NOTE(zaphar): This is stolen from ironcalc but ironcalc doesn't expose it
// publically.
pub(crate) const LAST_COLUMN: i32 = 16_384;
pub(crate) const LAST_ROW: i32 = 1_048_576;
#[derive(Debug, Clone)]
pub struct AddressRange<'book> {
@ -37,7 +43,7 @@ impl<'book> AddressRange<'book> {
}
rows
}
pub fn as_series(&self) -> Vec<Address> {
let (row_range, col_range) = self.get_ranges();
let mut rows = Vec::with_capacity(row_range.len() * col_range.len());
@ -78,14 +84,14 @@ impl<'book> AddressRange<'book> {
/// A spreadsheet book with some internal state tracking.
pub struct Book {
pub(crate) model: Model,
pub(crate) model: UserModel,
pub current_sheet: u32,
pub location: crate::ui::Address,
}
impl Book {
/// Construct a new book from a Model
pub fn new(model: Model) -> Self {
pub fn new(model: UserModel) -> Self {
Self {
model,
current_sheet: 0,
@ -93,9 +99,17 @@ impl Book {
}
}
pub fn from_model(model: Model) -> Self {
Self::new(UserModel::from_model(model))
}
/// Construct a new book from an xlsx file.
pub fn new_from_xlsx(path: &str) -> Result<Self> {
Ok(Self::new(load_from_xlsx(path, "en", "America/New_York")?))
Ok(Self::from_model(load_from_xlsx(
path,
"en",
"America/New_York",
)?))
}
/// Evaluate the spreadsheet calculating formulas and style changes.
@ -104,10 +118,9 @@ impl Book {
self.model.evaluate();
}
// TODO(zaphar): Should I support ICalc?
/// Construct a new book from a path.
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::from_model(load_from_xlsx(path, locale, tz)?))
}
/// Save book to an xlsx file.
@ -116,7 +129,7 @@ impl Book {
let file_path = std::path::Path::new(path);
let file = std::fs::File::create(file_path)?;
let writer = std::io::BufWriter::new(file);
save_xlsx_to_writer(&self.model, writer)?;
save_xlsx_to_writer(self.model.get_model(), writer)?;
Ok(())
}
@ -124,10 +137,9 @@ impl Book {
/// is the sheet name and the u32 is the sheet index.
pub fn get_all_sheets_identifiers(&self) -> Vec<(String, u32)> {
self.model
.workbook
.worksheets
.get_worksheets_properties()
.iter()
.map(|sheet| (sheet.get_name(), sheet.get_sheet_id()))
.map(|sheet| (sheet.name.to_owned(), sheet.sheet_id))
.collect()
}
@ -136,16 +148,22 @@ impl Book {
Ok(&self.get_sheet()?.name)
}
pub fn set_sheet_name(&mut self, idx: usize, sheet_name: &str) -> Result<()> {
self.get_sheet_by_idx_mut(idx)?.set_name(sheet_name);
pub fn set_sheet_name(&mut self, idx: u32, sheet_name: &str) -> Result<()> {
self.model
.rename_sheet(idx, sheet_name)
.map_err(|e| anyhow!(e))?;
Ok(())
}
pub fn new_sheet(&mut self, sheet_name: Option<&str>) -> Result<()> {
let (_, idx) = self.model.new_sheet();
self.model.new_sheet().map_err(|e| anyhow!(e))?;
let idx = self.model.get_selected_sheet();
if let Some(name) = sheet_name {
self.set_sheet_name(idx as usize, name)?;
self.set_sheet_name(idx, name)?;
}
self.model
.set_selected_sheet(self.current_sheet)
.map_err(|e| anyhow!(e))?;
Ok(())
}
@ -174,6 +192,7 @@ impl Book {
{
let contents = self
.model
.get_model()
.extend_to(
self.current_sheet,
from.row as i32,
@ -187,7 +206,7 @@ impl Book {
self.current_sheet,
cell.row as i32,
cell.col as i32,
contents,
&contents,
)
.map_err(|e| anyhow!(e))?;
}
@ -206,32 +225,42 @@ impl Book {
pub fn clear_cell_contents(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> {
Ok(self
.model
.cell_clear_contents(sheet, row as i32, col as i32)
.range_clear_contents(&Area {
sheet,
row: row as i32,
column: col as i32,
width: 1,
height: 1,
})
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?)
}
pub fn clear_cell_range(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> {
for row in start.row..=end.row {
for col in start.col..=end.col {
self.clear_cell_contents(sheet, Address { row, col })?;
}
}
let area = calculate_area(sheet, &start, &end);
self.model
.range_clear_contents(&area)
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?;
Ok(())
}
pub fn clear_cell_all(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> {
Ok(self
.model
.cell_clear_all(sheet, row as i32, col as i32)
.range_clear_all(&Area {
sheet,
row: row as i32,
column: col as i32,
width: 1,
height: 1,
})
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?)
}
pub fn clear_cell_range_all(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> {
for row in start.row..=end.row {
for col in start.col..=end.col {
self.clear_cell_all(sheet, Address { row, col })?;
}
}
let area = calculate_area(sheet, &start, &end);
self.model
.range_clear_all(&area)
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?;
Ok(())
}
@ -244,114 +273,115 @@ impl Book {
// TODO(jwall): This is modeled a little weird. We should probably record
// the error *somewhere* but for the user there is nothing to be done except
// not use a style.
match self.model.get_style_for_cell(sheet, cell.row as i32, cell.col as i32)
match self
.model
.get_cell_style(sheet, cell.row as i32, cell.col as i32)
{
Err(_) => None,
Ok(s) => Some(s),
}
}
fn get_column(&self, sheet: u32, col: usize) -> Result<Option<&Col>> {
Ok(self.model.workbook.worksheet(sheet)
.map_err(|e| anyhow!("{}", e))?.cols.get(col))
}
fn get_row(&self, sheet: u32, col: usize) -> Result<Option<&Row>> {
Ok(self.model.workbook.worksheet(sheet)
.map_err(|e| anyhow!("{}", e))?.rows.get(col))
}
pub fn get_column_style(&self, sheet: u32, col: usize) -> Result<Option<Style>> {
// TODO(jwall): This is modeled a little weird. We should probably record
// the error *somewhere* but for the user there is nothing to be done except
// not use a style.
if let Some(col) = self.get_column(sheet, col)? {
if let Some(style_idx) = col.style.map(|idx| idx as usize) {
let styles = &self.model.workbook.styles;
if styles.cell_style_xfs.len() <= style_idx {
return Ok(Some(Style {
alignment: None,
num_fmt: styles.num_fmts[style_idx].format_code.clone(),
fill: styles.fills[style_idx].clone(),
font: styles.fonts[style_idx].clone(),
border: styles.borders[style_idx].clone(),
quote_prefix: false,
}));
}
}
/// Set the cell style
/// Valid style paths are:
/// * fill.bg_color background color
/// * fill.fg_color foreground color
/// * font.b bold
/// * font.i italicize
/// * font.strike strikethrough
/// * font.color font color
/// * num_fmt number format
/// * alignment turn off alignment
/// * alignment.horizontal make alignment horzontal
/// * alignment.vertical make alignment vertical
/// * alignment.wrap_text wrap cell text
pub fn set_cell_style(&mut self, style: &[(&str, &str)], area: &Area) -> Result<()> {
for (path, val) in style {
self.model
.update_range_style(area, path, val)
.map_err(|s| anyhow!("Unable to format cell {}", s))?;
}
return Ok(None);
}
pub fn get_row_style(&self, sheet: u32, row: usize) -> Result<Option<Style>> {
// TODO(jwall): This is modeled a little weird. We should probably record
// the error *somewhere* but for the user there is nothing to be done except
// not use a style.
if let Some(row) = self.get_row(sheet, row)? {
let style_idx = row.s as usize;
let styles = &self.model.workbook.styles;
if styles.cell_style_xfs.len() <= style_idx {
return Ok(Some(Style {
alignment: None,
num_fmt: styles.num_fmts[style_idx].format_code.clone(),
fill: styles.fills[style_idx].clone(),
font: styles.fonts[style_idx].clone(),
border: styles.borders[style_idx].clone(),
quote_prefix: false,
}));
}
}
return Ok(None);
}
pub fn create_style(&mut self) -> Style {
Style {
alignment: None,
num_fmt: String::new(),
fill: Fill::default(),
font: Font::default(),
border: Border::default(),
quote_prefix: false,
}
}
pub fn set_cell_style(&mut self, style: &Style, sheet: u32, cell: &Address) -> Result<()> {
self.model.set_cell_style(sheet, cell.row as i32, cell.col as i32, style)
.map_err(|s| anyhow!("Unable to format cell {}", s))?;
Ok(())
}
pub fn set_col_style(&mut self, style: &Style, sheet: u32, col: usize) -> Result<()> {
let idx = self.create_or_get_style_idx(style);
let sheet = self.model.workbook.worksheet_mut(sheet)
.map_err(|e| anyhow!("{}", e))?;
let width = sheet.get_column_width(col as i32)
.map_err(|e| anyhow!("{}", e))?;
sheet.set_column_style(col as i32, idx)
.map_err(|e| anyhow!("{}", e))?;
sheet.set_column_width(col as i32, width)
.map_err(|e| anyhow!("{}", e))?;
fn get_col_range(&self, sheet: u32, col_idx: usize) -> Area {
Area {
sheet,
row: 1,
column: col_idx as i32,
width: 1,
height: LAST_ROW,
}
}
fn get_row_range(&self, sheet: u32, row_idx: usize) -> Area {
Area {
sheet,
row: row_idx as i32,
column: 1,
width: LAST_COLUMN,
height: 1,
}
}
/// Set the column style.
/// Valid style paths are:
/// * fill.bg_color background color
/// * fill.fg_color foreground color
/// * font.b bold
/// * font.i italicize
/// * font.strike strikethrough
/// * font.color font color
/// * num_fmt number format
/// * alignment turn off alignment
/// * alignment.horizontal make alignment horzontal
/// * alignment.vertical make alignment vertical
/// * alignment.wrap_text wrap cell text
pub fn set_col_style(
&mut self,
style: &[(&str, &str)],
sheet: u32,
col_idx: usize,
) -> Result<()> {
// TODO(jeremy): This is a little hacky and the underlying model
// supports a better mechanism but UserModel doesn't support it yet.
// https://github.com/ironcalc/IronCalc/issues/273
// https://github.com/ironcalc/IronCalc/pull/276 is the coming fix.
// NOTE(jwall): Because of the number of cells required to modify
// this is crazy slow
let area = self.get_col_range(sheet, col_idx);
self.set_cell_style(style, &area)?;
Ok(())
}
pub fn set_row_style(&mut self, style: &Style, sheet: u32, row: usize) -> Result<()> {
let idx = self.create_or_get_style_idx(style);
self.model.workbook.worksheet_mut(sheet)
.map_err(|e| anyhow!("{}", e))?
.set_row_style(row as i32, idx)
.map_err(|e| anyhow!("{}", e))?;
/// Set the row style
/// Valid style paths are:
/// * fill.bg_color background color
/// * fill.fg_color foreground color
/// * font.b bold
/// * font.i italicize
/// * font.strike strikethrough
/// * font.color font color
/// * num_fmt number format
/// * alignment turn off alignment
/// * alignment.horizontal make alignment horzontal
/// * alignment.vertical make alignment vertical
/// * alignment.wrap_text wrap cell text
pub fn set_row_style(
&mut self,
style: &[(&str, &str)],
sheet: u32,
row_idx: usize,
) -> Result<()> {
// TODO(jeremy): This is a little hacky and the underlying model
// supports a better mechanism but UserModel doesn't support it yet.
// https://github.com/ironcalc/IronCalc/issues/273
// https://github.com/ironcalc/IronCalc/pull/276 is the coming fix.
let area = self.get_row_range(sheet, row_idx);
self.set_cell_style(style, &area)?;
Ok(())
}
fn create_or_get_style_idx(&mut self, style: &Style) -> i32 {
let idx = if let Some(style_idx) = self.model.workbook.styles.get_style_index(style) {
style_idx
} else {
self.model.workbook.styles.create_new_style(style)
};
idx
}
/// Get a cells rendered content for display.
pub fn get_cell_addr_rendered(&self, Address { row, col }: &Address) -> Result<String> {
Ok(self
@ -382,20 +412,21 @@ impl Book {
/// 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<()> {
pub fn edit_current_cell<S: AsRef<str>>(&mut self, value: S) -> Result<()> {
self.update_cell(&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_cell<S: Into<String>>(&mut self, location: &Address, value: S) -> Result<()> {
pub fn update_cell<S: AsRef<str>>(&mut self, location: &Address, value: S) -> Result<()> {
self.model
.set_user_input(
self.current_sheet,
location.row as i32,
location.col as i32,
value.into(),
// TODO(jwall): This could probably be made more efficient
value.as_ref(),
)
.map_err(|e| anyhow!("Invalid cell contents: {}", e))?;
Ok(())
@ -403,9 +434,11 @@ impl Book {
/// Insert `count` rows at a `row_idx`.
pub fn insert_rows(&mut self, row_idx: usize, count: usize) -> Result<()> {
self.model
.insert_rows(self.current_sheet, row_idx as i32, count as i32)
.map_err(|e| anyhow!("Unable to insert row(s): {}", e))?;
for i in 0..count {
self.model
.insert_row(self.current_sheet, (row_idx + i) as i32)
.map_err(|e| anyhow!("Unable to insert row(s): {}", e))?;
}
if self.location.row >= row_idx {
self.move_to(&Address {
row: self.location.row + count,
@ -417,9 +450,11 @@ impl Book {
/// Insert `count` columns at a `col_idx`.
pub fn insert_columns(&mut self, col_idx: usize, count: usize) -> Result<()> {
self.model
.insert_columns(self.current_sheet, col_idx as i32, count as i32)
.map_err(|e| anyhow!("Unable to insert column(s): {}", e))?;
for i in 0..count {
self.model
.insert_column(self.current_sheet, (col_idx + i) as i32)
.map_err(|e| anyhow!("Unable to insert column(s): {}", e))?;
}
if self.location.col >= col_idx {
self.move_to(&Address {
row: self.location.row,
@ -436,16 +471,33 @@ impl Book {
/// Get column size
pub fn get_col_size(&self, idx: usize) -> Result<usize> {
self.get_column_size_for_sheet(self.current_sheet, idx)
}
pub fn get_column_size_for_sheet(
&self,
sheet: u32,
idx: usize,
) -> std::result::Result<usize, anyhow::Error> {
Ok((self
.get_sheet()?
.get_column_width(idx as i32)
.model
.get_column_width(sheet, idx as i32)
.map_err(|e| anyhow!("Error getting column width: {:?}", e))?
/ COL_PIXELS) as usize)
}
pub fn set_col_size(&mut self, idx: usize, cols: usize) -> Result<()> {
self.get_sheet_mut()?
.set_column_width(idx as i32, cols as f64 * COL_PIXELS)
pub fn set_col_size(&mut self, col: usize, width: usize) -> Result<()> {
self.set_column_size_for_sheet(self.current_sheet, col, width)
}
pub fn set_column_size_for_sheet(
&mut self,
sheet: u32,
col: usize,
width: usize,
) -> std::result::Result<(), anyhow::Error> {
self.model
.set_column_width(sheet, col as i32, width as f64 * COL_PIXELS)
.map_err(|e| anyhow!("Error setting column width: {:?}", e))?;
Ok(())
}
@ -468,6 +520,7 @@ impl Book {
pub fn select_sheet_by_name(&mut self, name: &str) -> bool {
if let Some((idx, _sheet)) = self
.model
.get_model()
.workbook
.worksheets
.iter()
@ -482,25 +535,31 @@ impl Book {
/// Get all sheet names
pub fn get_sheet_names(&self) -> Vec<String> {
self.model.workbook.get_worksheet_names()
self.model.get_model().workbook.get_worksheet_names()
}
pub fn select_next_sheet(&mut self) {
let len = self.model.workbook.worksheets.len() as u32;
let len = self.model.get_model().workbook.worksheets.len() as u32;
let mut next = self.current_sheet + 1;
if next == len {
next = 0;
}
self.model
.set_selected_sheet(next)
.expect("Unexpected error selecting sheet");
self.current_sheet = next;
}
pub fn select_prev_sheet(&mut self) {
let len = self.model.workbook.worksheets.len() as u32;
let len = self.model.get_model().workbook.worksheets.len() as u32;
let next = if self.current_sheet == 0 {
len - 1
} else {
self.current_sheet - 1
};
self.model
.set_selected_sheet(next)
.expect("Unexpected error selecting sheet");
self.current_sheet = next;
}
@ -508,12 +567,16 @@ impl Book {
pub fn select_sheet_by_id(&mut self, id: u32) -> bool {
if let Some((idx, _sheet)) = self
.model
.get_model()
.workbook
.worksheets
.iter()
.enumerate()
.find(|(_idx, sheet)| sheet.sheet_id == id)
{
self.model
.set_selected_sheet(idx as u32)
.expect("Unexpected error selecting sheet");
self.current_sheet = idx as u32;
return true;
}
@ -522,42 +585,46 @@ impl Book {
/// Get the current `Worksheet`.
pub(crate) fn get_sheet(&self) -> Result<&Worksheet> {
// TODO(jwall): Is there a cleaner way to do this with UserModel?
// Looks like it should be done with:
// https://docs.rs/ironcalc_base/latest/ironcalc_base/struct.UserModel.html#method.get_worksheets_properties
Ok(self
.model
.get_model()
.workbook
.worksheet(self.current_sheet)
.map_err(|s| anyhow!("Invalid Worksheet id: {}: error: {}", self.current_sheet, s))?)
}
pub(crate) fn get_sheet_mut(&mut self) -> Result<&mut Worksheet> {
Ok(self
.model
.workbook
.worksheet_mut(self.current_sheet)
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
}
pub(crate) fn get_sheet_name_by_idx(&self, idx: usize) -> Result<&str> {
// TODO(jwall): Is there a cleaner way to do this with UserModel?
// Looks like it should be done with:
// https://docs.rs/ironcalc_base/latest/ironcalc_base/struct.UserModel.html#method.get_worksheets_properties
Ok(&self
.model
.get_model()
.workbook
.worksheet(idx as u32)
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?
.name)
}
pub(crate) fn get_sheet_by_idx_mut(&mut self, idx: usize) -> Result<&mut Worksheet> {
Ok(self
.model
.workbook
.worksheet_mut(idx as u32)
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
}
}
fn calculate_area(sheet: u32, start: &Address, end: &Address) -> Area {
let area = Area {
sheet,
row: start.row as i32,
column: start.col as i32,
height: (end.row - start.row + 1) as i32,
width: (end.col - start.col + 1) as i32,
};
area
}
impl Default for Book {
fn default() -> Self {
let mut book =
Book::new(Model::new_empty("default_name", "en", "America/New_York").unwrap());
Book::new(UserModel::new_empty("default_name", "en", "America/New_York").unwrap());
book.update_cell(&Address { row: 1, col: 1 }, "").unwrap();
book
}

View File

@ -7,9 +7,9 @@ pub enum Cmd<'a> {
Write(Option<&'a str>),
InsertRows(usize),
InsertColumns(usize),
ColorRows(Option<usize>, &'a str),
ColorColumns(Option<usize>, &'a str),
ColorCell(&'a str),
ColorRows(Option<usize>, String),
ColorColumns(Option<usize>, String),
ColorCell(String),
RenameSheet(Option<usize>, &'a str),
NewSheet(Option<&'a str>),
SelectSheet(&'a str),
@ -165,10 +165,7 @@ fn try_consume_color_cell<'cmd, 'i: 'cmd>(
if input.remaining() > 0 && !is_ws(&mut input) {
return Err("Invalid command: Did you mean to type `color-cell <color>`?");
}
let arg = input.span(0..).trim();
if arg.len() == 0 {
return Err("Invalid command: Did you mean to type `color-cell <color>`?");
}
let arg = parse_color(input.span(0..).trim())?;
return Ok(Some(Cmd::ColorCell(arg)));
}
@ -330,10 +327,7 @@ fn try_consume_color_rows<'cmd, 'i: 'cmd>(
return Err("Invalid command: Did you mean to type `color-rows [count] <color>`?");
}
let (idx, rest) = try_consume_usize(input.clone());
let arg = rest.span(0..).trim();
if arg.is_empty() {
return Err("Invalid command: `color-rows` requires a color argument");
}
let arg = parse_color(rest.span(0..).trim())?;
return Ok(Some(Cmd::ColorRows(idx, arg)));
}
@ -350,19 +344,59 @@ fn try_consume_color_columns<'cmd, 'i: 'cmd>(
return Err("Invalid command: Did you mean to type `color-columns [count] <color>`?");
}
let (idx, rest) = try_consume_usize(input.clone());
let arg = rest.span(0..).trim();
if arg.is_empty() {
return Err("Invalid command: `color-columns` requires a color argument");
}
let arg = parse_color(rest.span(0..).trim())?;
return Ok(Some(Cmd::ColorColumns(idx, arg)));
}
fn try_consume_usize<'cmd, 'i: 'cmd>(
mut input: StrCursor<'i>,
) -> (Option<usize>, StrCursor<'i>) {
pub(crate) fn parse_color(color: &str) -> Result<String, &'static str> {
use colorsys::{Ansi256, Rgb};
if color.is_empty() {
return Err("Invalid command: `color-columns` requires a color argument");
}
let parsed = match color.to_lowercase().as_str() {
"black" => Ansi256::new(0).as_rgb().to_hex_string(),
"red" => Ansi256::new(1).as_rgb().to_hex_string(),
"green" => Ansi256::new(2).as_rgb().to_hex_string(),
"yellow" => Ansi256::new(3).as_rgb().to_hex_string(),
"blue" => Ansi256::new(4).as_rgb().to_hex_string(),
"magenta" => Ansi256::new(5).as_rgb().to_hex_string(),
"cyan" => Ansi256::new(6).as_rgb().to_hex_string(),
"gray" | "grey" => Ansi256::new(7).as_rgb().to_hex_string(),
"darkgrey" | "darkgray" => Ansi256::new(8).as_rgb().to_hex_string(),
"lightred" => Ansi256::new(9).as_rgb().to_hex_string(),
"lightgreen" => Ansi256::new(10).as_rgb().to_hex_string(),
"lightyellow" => Ansi256::new(11).as_rgb().to_hex_string(),
"lightblue" => Ansi256::new(12).as_rgb().to_hex_string(),
"lightmagenta" => Ansi256::new(13).as_rgb().to_hex_string(),
"lightcyan" => Ansi256::new(14).as_rgb().to_hex_string(),
"white" => Ansi256::new(15).as_rgb().to_hex_string(),
candidate => {
if candidate.starts_with("#") {
candidate.to_string()
} else if candidate.starts_with("rgb(") {
if let Ok(rgb) = <Rgb as std::str::FromStr>::from_str(candidate) {
// Note that the colorsys rgb model clamps the f64 values to no more
// than 255.0 so the below casts are safe.
rgb.to_hex_string()
} else {
return Err("Invalid color");
}
} else {
return Err("Invalid color");
}
}
};
Ok(parsed)
}
fn try_consume_usize<'cmd, 'i: 'cmd>(mut input: StrCursor<'i>) -> (Option<usize>, StrCursor<'i>) {
let mut out = String::new();
let original_input = input.clone();
while input.peek_next().map(|c| (*c as char).is_ascii_digit()).unwrap_or(false) {
while input
.peek_next()
.map(|c| (*c as char).is_ascii_digit())
.unwrap_or(false)
{
out.push(*input.next().unwrap() as char);
}
if out.len() > 0 {

View File

@ -1,11 +1,11 @@
//! Ui rendering logic
use std::{path::PathBuf, process::ExitCode};
use crate::book::{AddressRange, Book};
use crate::book::{self, AddressRange, Book};
use anyhow::{anyhow, Result};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use ironcalc::base::Model;
use ironcalc::base::{expressions::types::Area, Model};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Flex, Layout},
@ -131,7 +131,7 @@ impl<'ws> AppState<'ws> {
}
}
// TODO(jwall): This should probably move to a different module.
// TODO(jwall): Should we just be using `Area` for this?.
/// The Address in a Table.
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
pub struct Address {
@ -187,7 +187,7 @@ impl<'ws> Workspace<'ws> {
pub fn new_empty(locale: &str, tz: &str) -> Result<Self> {
Ok(Self::new(
Book::new(Model::new_empty("", locale, tz).map_err(|e| anyhow!("{}", e))?),
Book::from_model(Model::new_empty("", locale, tz).map_err(|e| anyhow!("{}", e))?),
PathBuf::default(),
))
}
@ -235,7 +235,7 @@ impl<'ws> Workspace<'ws> {
/// Move a row down in the current sheet.
pub fn move_down(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
if loc.row < render::viewport::LAST_ROW {
if loc.row < (book::LAST_ROW as usize) {
loc.row += 1;
self.book.move_to(&loc)?;
}
@ -244,10 +244,13 @@ impl<'ws> Workspace<'ws> {
/// Move to the top row without changing columns
pub fn move_to_top(&mut self) -> Result<()> {
self.book.move_to(&Address { row: 1, col: self.book.location.col })?;
self.book.move_to(&Address {
row: 1,
col: self.book.location.col,
})?;
Ok(())
}
/// Move a row up in the current sheet.
pub fn move_up(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
@ -271,7 +274,7 @@ impl<'ws> Workspace<'ws> {
/// Move a column to the left in the current sheet.
pub fn move_right(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
if loc.col < render::viewport::LAST_COLUMN {
if loc.col < (book::LAST_COLUMN as usize) {
loc.col += 1;
self.book.move_to(&loc)?;
}
@ -320,7 +323,8 @@ impl<'ws> Workspace<'ws> {
"Edit Mode:".to_string(),
"* ENTER/RETURN: Exit edit mode and save changes".to_string(),
"* Ctrl-r: Enter Range Selection mode".to_string(),
"* v: Enter Range Selection mode with the start of the range already selected".to_string(),
"* v: Enter Range Selection mode with the start of the range already selected"
.to_string(),
"* ESC: Exit edit mode and discard changes".to_string(),
"Otherwise edit as normal".to_string(),
],
@ -445,11 +449,10 @@ impl<'ws> Workspace<'ws> {
Ok(Some(Cmd::RenameSheet(idx, name))) => {
match idx {
Some(idx) => {
self.book.set_sheet_name(idx, name)?;
self.book.set_sheet_name(idx as u32, name)?;
}
_ => {
self.book
.set_sheet_name(self.book.current_sheet as usize, name)?;
self.book.set_sheet_name(self.book.current_sheet, name)?;
}
}
Ok(None)
@ -462,57 +465,53 @@ impl<'ws> Workspace<'ws> {
self.book.select_sheet_by_name(name);
Ok(None)
}
Ok(Some(Cmd::Quit)) => {
Ok(Some(ExitCode::SUCCESS))
}
Ok(Some(Cmd::ColorRows(_count, color))) => {
let row_count = _count.unwrap_or(1);
Ok(Some(Cmd::Quit)) => Ok(Some(ExitCode::SUCCESS)),
Ok(Some(Cmd::ColorRows(count, color))) => {
let row_count = count.unwrap_or(1);
let row = self.book.location.row;
for r in row..(row+row_count) {
let mut style = if let Some(style) = self.book.get_row_style(self.book.current_sheet, r)? {
style
} else {
self.book.create_style()
};
style.fill.bg_color = Some(color.to_string());
self.book.set_row_style(&style, self.book.current_sheet, r)?;
for r in row..(row + row_count) {
self.book.set_row_style(
&[("fill.bg_color", &color)],
self.book.current_sheet,
r,
)?;
}
Ok(None)
}
Ok(Some(Cmd::ColorColumns(_count, color))) => {
let col_count = _count.unwrap_or(1);
Ok(Some(Cmd::ColorColumns(count, color))) => {
let col_count = count.unwrap_or(1);
let col = self.book.location.col;
for c in col..(col+col_count) {
let mut style = if let Some(style) = self.book.get_column_style(self.book.current_sheet, c)? {
style
} else {
self.book.create_style()
};
style.fill.bg_color = Some(color.to_string());
self.book.set_col_style(&style, self.book.current_sheet, c)?;
for c in col..(col + col_count) {
self.book.set_col_style(
&[("fill.bg_color", &color)],
self.book.current_sheet,
c,
)?;
}
Ok(None)
}
Ok(Some(Cmd::ColorCell(color))) => {
if let Some((start, end)) = self.state.range_select.get_range() {
for ri in start.row..=end.row {
for ci in start.col..=end.col {
let address = Address { row: ri, col: ci };
let sheet = self.book.current_sheet;
let mut style = self.book.get_cell_style(sheet, &address)
.expect("I think this should be impossible.").clone();
style.fill.bg_color = Some(color.to_string());
self.book.set_cell_style(&style, sheet, &address)?;
}
let sheet = self.book.current_sheet;
let area = if let Some((start, end)) = self.state.range_select.get_range() {
Area {
sheet,
row: start.row as i32,
column: start.col as i32,
width: (end.col - start.col + 1) as i32,
height: (end.row - start.row + 1) as i32,
}
} else {
let address = self.book.location.clone();
let sheet = self.book.current_sheet;
let mut style = self.book.get_cell_style(sheet, &address)
.expect("I think this should be impossible.").clone();
style.fill.bg_color = Some(color.to_string());
self.book.set_cell_style(&style, sheet, &address)?;
}
Area {
sheet,
row: address.row as i32,
column: address.col as i32,
width: 1,
height: 1,
}
};
self.book
.set_cell_style(&[("fill.bg_color", &color)], &area)?;
Ok(None)
}
Ok(None) => {
@ -550,7 +549,7 @@ impl<'ws> Workspace<'ws> {
self.handle_numeric_prefix(d);
}
KeyCode::Char('D') => {
if let Some((start, end)) = self.state.range_select.get_range() {
if let Some((start, end)) = dbg!(self.state.range_select.get_range()) {
self.book.clear_cell_range_all(
self.state
.range_select
@ -622,12 +621,7 @@ impl<'ws> Workspace<'ws> {
})?;
self.state.range_select.sheet = Some(self.book.current_sheet);
}
KeyCode::Char('C')
if key
.modifiers
.contains(KeyModifiers::CONTROL) =>
{
// TODO(zaphar): Share the algorithm below between both copies
KeyCode::Char('C') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.copy_range(true)?;
self.exit_range_select_mode()?;
}
@ -644,7 +638,10 @@ impl<'ws> Workspace<'ws> {
self.exit_range_select_mode()?;
}
KeyCode::Char('x') => {
if let (Some(from), Some(to)) = (self.state.range_select.start.as_ref(), self.state.range_select.end.as_ref()) {
if let (Some(from), Some(to)) = (
self.state.range_select.start.as_ref(),
self.state.range_select.end.as_ref(),
) {
self.book.extend_to(from, to)?;
}
self.exit_range_select_mode()?;
@ -663,20 +660,15 @@ impl<'ws> Workspace<'ws> {
fn copy_range(&mut self, formatted: bool) -> Result<(), anyhow::Error> {
self.update_range_selection()?;
match &self.state.range_select.get_range() {
Some((
start,
end,
)) => {
Some((start, end)) => {
let mut rows = Vec::new();
for row in (AddressRange { start, end, }).as_rows() {
for row in (AddressRange { start, end }).as_rows() {
let mut cols = Vec::new();
for cell in row {
cols.push(if formatted {
self.book
.get_cell_addr_rendered(&cell)?
self.book.get_cell_addr_rendered(&cell)?
} else {
self.book
.get_cell_addr_contents(&cell)?
self.book.get_cell_addr_contents(&cell)?
});
}
rows.push(cols);
@ -685,11 +677,9 @@ impl<'ws> Workspace<'ws> {
}
None => {
self.state.clipboard = Some(ClipboardContents::Cell(if formatted {
self.book
.get_current_cell_rendered()?
self.book.get_current_cell_rendered()?
} else {
self.book
.get_current_cell_contents()?
self.book.get_current_cell_contents()?
}));
}
}
@ -755,11 +745,7 @@ impl<'ws> Workspace<'ws> {
self.book.get_current_cell_rendered()?,
));
}
KeyCode::Char('C')
if key
.modifiers
.contains(KeyModifiers::CONTROL) =>
{
KeyCode::Char('C') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.state.clipboard = Some(ClipboardContents::Cell(
self.book.get_current_cell_rendered()?,
));
@ -873,7 +859,13 @@ impl<'ws> Workspace<'ws> {
}
KeyCode::Char('g') => {
// TODO(zaphar): This really needs a better state machine.
if self.state.char_queue.first().map(|c| *c == 'g').unwrap_or(false) {
if self
.state
.char_queue
.first()
.map(|c| *c == 'g')
.unwrap_or(false)
{
self.state.char_queue.pop();
self.move_to_top()?;
} else {

View File

@ -8,14 +8,14 @@ use super::{Address, Book, Viewport, ViewportState};
#[test]
fn test_viewport_get_visible_columns() {
let mut state = ViewportState::default();
let book = Book::new(
let book = Book::from_model(
Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"),
);
let default_size = book.get_col_size(1).expect("Failed to get column size");
let width = dbg!(dbg!(default_size) * 12 / 2);
let app_state = AppState::default();
let viewport =
Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { row: 1, col: 17 });
let viewport = Viewport::new(&book, Some(&app_state.range_select))
.with_selected(Address { row: 1, col: 17 });
let cols = viewport
.get_visible_columns((width + 5) as u16, &mut state)
.expect("Failed to get visible columns");
@ -26,13 +26,13 @@ fn test_viewport_get_visible_columns() {
#[test]
fn test_viewport_get_visible_rows() {
let mut state = dbg!(ViewportState::default());
let book = Book::new(
let book = Book::from_model(
Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"),
);
let height = 6;
let app_state = AppState::default();
let viewport =
Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { row: 17, col: 1 });
let viewport = Viewport::new(&book, Some(&app_state.range_select))
.with_selected(Address { row: 17, col: 1 });
let rows = dbg!(viewport.get_visible_rows(height as u16, &mut state));
assert_eq!(height - 1, rows.len());
assert_eq!(
@ -45,7 +45,7 @@ fn test_viewport_get_visible_rows() {
#[test]
fn test_viewport_visible_columns_after_length_change() {
let mut state = ViewportState::default();
let mut book = Book::new(
let mut book = Book::from_model(
Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"),
);
let default_size = book.get_col_size(1).expect("Failed to get column size");
@ -65,8 +65,8 @@ fn test_viewport_visible_columns_after_length_change() {
.expect("Failed to set column size");
{
let app_state = AppState::default();
let viewport =
Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { row: 1, col: 1 });
let viewport = Viewport::new(&book, Some(&app_state.range_select))
.with_selected(Address { row: 1, col: 1 });
let cols = viewport
.get_visible_columns((width + 5) as u16, &mut state)
.expect("Failed to get visible columns");
@ -97,7 +97,9 @@ fn test_color_mapping() {
("darkgrey", Color::DarkGray),
("darkgray", Color::DarkGray),
("#35f15b", Color::Rgb(53, 241, 91)),
].map(|(s, c)| (Some(s.to_string()), c)) {
]
.map(|(s, c)| (Some(s.to_string()), c))
{
assert_eq!(super::viewport::map_color(s.as_ref(), Color::Gray), c);
}
}

View File

@ -7,14 +7,9 @@ use ratatui::{
widgets::{Block, Cell, Row, StatefulWidget, Table, Widget},
};
use crate::book;
use super::{Address, Book, RangeSelection};
// TODO(zaphar): Move this to the book module.
// NOTE(zaphar): This is stolen from ironcalc but ironcalc doesn't expose it
// publically.
pub(crate) const LAST_COLUMN: usize = 16_384;
pub(crate) const LAST_ROW: usize = 1_048_576;
/// A visible column to show in our Viewport.
#[derive(Clone, Debug)]
pub struct VisibleColumn {
@ -68,7 +63,7 @@ impl<'ws> Viewport<'ws> {
let start_row = std::cmp::min(self.selected.row, state.prev_corner.row);
let mut start = start_row;
let mut end = start_row;
for row_idx in start_row..=LAST_ROW {
for row_idx in start_row..=(book::LAST_ROW as usize) {
let updated_length = length + 1;
if updated_length <= height {
length = updated_length;
@ -95,7 +90,7 @@ impl<'ws> Viewport<'ws> {
// We start out with a length of 5 already reserved
let mut length = 5;
let start_idx = std::cmp::min(self.selected.col, state.prev_corner.col);
for idx in start_idx..=LAST_COLUMN {
for idx in start_idx..=(book::LAST_COLUMN as usize) {
let size = self.book.get_col_size(idx)? as u16;
let updated_length = length + size;
let col = VisibleColumn { idx, length: size };
@ -248,7 +243,6 @@ pub(crate) fn map_color(color: Option<&String>, otherwise: Color) -> Color {
candidate => {
// TODO(jeremy): Should we support more syntaxes than hex string?
// hsl(...) ??
// rgb(...) ??
if candidate.starts_with("#") {
if let Ok(rgb) = colorsys::Rgb::from_hex_str(candidate) {
// Note that the colorsys rgb model clamps the f64 values to no more

View File

@ -2,6 +2,8 @@ use std::process::ExitCode;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use crate::book;
use crate::ui::cmd::parse_color;
use crate::ui::{Address, Modality};
use super::cmd::{parse, Cmd};
@ -33,6 +35,10 @@ impl InputScript {
self.event(construct_key_event(KeyCode::Tab))
}
pub fn enter(self) -> Self {
self.event(construct_key_event(KeyCode::Enter))
}
pub fn modified_char(self, c: char, mods: KeyModifiers) -> Self {
self.event(construct_modified_key_event(KeyCode::Char(c), mods))
}
@ -42,10 +48,6 @@ impl InputScript {
self
}
pub fn enter(self) -> Self {
self.event(construct_key_event(KeyCode::Enter))
}
pub fn esc(self) -> Self {
self.event(construct_key_event(KeyCode::Esc))
}
@ -267,7 +269,7 @@ fn test_cmd_color_rows_with_color() {
let output = result.unwrap();
assert!(output.is_some());
let cmd = output.unwrap();
assert_eq!(cmd, Cmd::ColorRows(None, "red"));
assert_eq!(cmd, Cmd::ColorRows(None, parse_color("red").unwrap()));
}
#[test]
@ -278,7 +280,7 @@ fn test_cmd_color_rows_with_idx_and_color() {
let output = result.unwrap();
assert!(output.is_some());
let cmd = output.unwrap();
assert_eq!(cmd, Cmd::ColorRows(Some(1), "red"));
assert_eq!(cmd, Cmd::ColorRows(Some(1), parse_color("red").unwrap()));
}
#[test]
@ -289,7 +291,7 @@ fn test_cmd_color_columns_with_color() {
let output = result.unwrap();
assert!(output.is_some());
let cmd = output.unwrap();
assert_eq!(cmd, Cmd::ColorColumns(None, "red"));
assert_eq!(cmd, Cmd::ColorColumns(None, parse_color("red").unwrap()));
}
#[test]
@ -300,10 +302,9 @@ fn test_cmd_color_columns_with_idx_and_color() {
let output = result.unwrap();
assert!(output.is_some());
let cmd = output.unwrap();
assert_eq!(cmd, Cmd::ColorColumns(Some(1), "red"));
assert_eq!(cmd, Cmd::ColorColumns(Some(1), parse_color("red").unwrap()));
}
#[test]
fn test_input_navitation_enter_key() {
let mut ws = new_workspace();
@ -1003,8 +1004,7 @@ macro_rules! assert_range_clear {
.run(&mut ws)
.expect("Failed to handle script");
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
$script.run(&mut ws)
.expect("Failed to handle script");
$script.run(&mut ws).expect("Failed to handle script");
assert_eq!(
"".to_string(),
ws.book
@ -1022,18 +1022,21 @@ macro_rules! assert_range_clear {
#[test]
fn test_range_select_clear_upper_d() {
assert_range_clear!(script()
.char('j')
.char('l')
.char('D'));
assert_range_clear!(script().char('j').char('l').char('D'));
}
#[test]
fn test_range_select_movement() {
let mut ws = new_workspace();
ws.book.new_sheet(Some("s2")).expect("Unable create s2 sheet");
ws.book.new_sheet(Some("s3")).expect("Unable create s3 sheet");
script().ctrl('r').run(&mut ws)
ws.book
.new_sheet(Some("s2"))
.expect("Unable create s2 sheet");
ws.book
.new_sheet(Some("s3"))
.expect("Unable create s3 sheet");
script()
.ctrl('r')
.run(&mut ws)
.expect("failed to run script");
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
script()
@ -1063,10 +1066,7 @@ fn test_range_select_movement() {
#[test]
fn test_range_select_clear_lower_d() {
assert_range_clear!(script()
.char('j')
.char('l')
.char('d'));
assert_range_clear!(script().char('j').char('l').char('d'));
}
macro_rules! assert_range_copy {
@ -1074,8 +1074,12 @@ macro_rules! assert_range_copy {
let mut ws = new_workspace();
let top_left_addr = Address { row: 2, col: 2 };
let bot_right_addr = Address { row: 4, col: 4 };
ws.book.update_cell(&top_left_addr, "top_left").expect("Failed to update top left");
ws.book.update_cell(&bot_right_addr, "bot_right").expect("Failed to update top left");
ws.book
.update_cell(&top_left_addr, "top_left")
.expect("Failed to update top left");
ws.book
.update_cell(&bot_right_addr, "bot_right")
.expect("Failed to update top left");
assert!(ws.state.clipboard.is_none());
script()
.ctrl('r')
@ -1084,7 +1088,14 @@ macro_rules! assert_range_copy {
.char(' ')
.run(&mut ws)
.expect("failed to run script");
assert_eq!(&top_left_addr, ws.state.range_select.start.as_ref().expect("Didn't find a start of range"));
assert_eq!(
&top_left_addr,
ws.state
.range_select
.start
.as_ref()
.expect("Didn't find a start of range")
);
script()
.char('2')
.char('j')
@ -1092,27 +1103,53 @@ macro_rules! assert_range_copy {
.char('l')
.run(&mut ws)
.expect("failed to run script");
assert_eq!(&bot_right_addr, ws.state.range_select.end.as_ref().expect("Didn't find a start of range"));
assert_eq!(&Address { row: 1, col: 1}, ws.state.range_select.original_location
.as_ref().expect("Expected an original location"));
assert_eq!(0, ws.state.range_select.original_sheet.
expect("Expected an original sheet"));
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.iter().last());
assert_eq!(
&bot_right_addr,
ws.state
.range_select
.end
.as_ref()
.expect("Didn't find a start of range")
);
assert_eq!(
&Address { row: 1, col: 1 },
ws.state
.range_select
.original_location
.as_ref()
.expect("Expected an original location")
);
assert_eq!(
0,
ws.state
.range_select
.original_sheet
.expect("Expected an original sheet")
);
assert_eq!(
Some(&Modality::RangeSelect),
ws.state.modality_stack.iter().last()
);
dbg!(ws.state.range_select.get_range());
$script.run(&mut ws)
.expect("failed to run script");
$script.run(&mut ws).expect("failed to run script");
assert!(ws.state.clipboard.is_some());
match ws.state.clipboard.unwrap() {
crate::ui::ClipboardContents::Cell(_) => assert!(false, "Not rows in Clipboard"),
crate::ui::ClipboardContents::Range(rows) => {
assert_eq!(vec![
vec!["top_left".to_string(), "".to_string(), "".to_string()],
vec!["".to_string(), "".to_string(), "".to_string()],
vec!["".to_string(), "".to_string(), "bot_right".to_string()],
], rows);
},
assert_eq!(
vec![
vec!["top_left".to_string(), "".to_string(), "".to_string()],
vec!["".to_string(), "".to_string(), "".to_string()],
vec!["".to_string(), "".to_string(), "bot_right".to_string()],
],
rows
);
}
}
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.iter().last());
assert_eq!(
Some(&Modality::Navigate),
ws.state.modality_stack.iter().last()
);
}};
}
@ -1139,7 +1176,9 @@ fn test_range_select_copy_capital_c() {
#[test]
fn test_extend_to_range() {
let mut ws = new_workspace();
ws.book.edit_current_cell("=B1+1").expect("Failed to edit cell");
ws.book
.edit_current_cell("=B1+1")
.expect("Failed to edit cell");
ws.book.evaluate();
script()
.char('v')
@ -1147,11 +1186,93 @@ fn test_extend_to_range() {
.char('x')
.run(&mut ws)
.expect("Unable to run script");
let extended_cell = ws.book.get_cell_addr_contents(&Address { row: 2, col: 1 })
let extended_cell = ws
.book
.get_cell_addr_contents(&Address { row: 2, col: 1 })
.expect("Failed to get cell contents");
assert_eq!("=B2+1".to_string(), extended_cell);
}
#[test]
fn test_color_cells() {
let mut ws = new_workspace();
script()
.char('v')
.chars("jjll")
.char(':')
.chars("color-cell red")
.enter()
.run(&mut ws)
.expect("Unable to run script");
for ri in 1..=3 {
for ci in 1..=3 {
let style = ws
.book
.get_cell_style(ws.book.current_sheet, &Address { row: ri, col: ci })
.expect("failed to get style");
assert_eq!(
"#800000",
style
.fill
.bg_color
.expect(&format!("No background color set for {}:{}", ri, ci))
.as_str()
);
}
}
}
#[test]
fn test_color_row() {
let mut ws = new_workspace();
script()
.char(':')
.chars("color-rows red")
.enter()
.run(&mut ws)
.expect("Unable to run script");
for ci in [1, book::LAST_COLUMN] {
let style = ws
.book
.get_cell_style(ws.book.current_sheet, &Address { row: 1, col: ci as usize })
.expect("failed to get style");
assert_eq!(
"#800000",
style
.fill
.bg_color
.expect(&format!("No background color set for {}:{}", 1, ci))
.as_str()
);
}
}
#[test]
fn test_color_col() {
let mut ws = new_workspace();
script()
.char(':')
.chars("color-columns red")
.enter()
.run(&mut ws)
.expect("Unable to run script");
for ri in [1, book::LAST_ROW] {
let style = ws
.book
.get_cell_style(ws.book.current_sheet, &Address { row: ri as usize, col: 1 })
.expect("failed to get style");
assert_eq!(
"#800000",
style
.fill
.bg_color
.expect(&format!("No background color set for {}:{}", ri, 1))
.as_str()
);
}
}
fn new_workspace<'a>() -> Workspace<'a> {
Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook")
}