mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 13:29:48 -04:00
Compare commits
25 Commits
aa0a1512f6
...
2aa9fd69b1
Author | SHA1 | Date | |
---|---|---|---|
2aa9fd69b1 | |||
621c35d7c4 | |||
4623a911f4 | |||
ba5ea3c627 | |||
dae3d71c54 | |||
8da0ebda4e | |||
db0397b8f4 | |||
c3f84b10ad | |||
b5e0362a4e | |||
5ebdc6e70c | |||
e09b0f90e9 | |||
f392cb743f | |||
49c768dd76 | |||
d456501565 | |||
5d23410f00 | |||
7ffd420029 | |||
3219e01176 | |||
f6c9e95fda | |||
e7169dcb44 | |||
e798350cd2 | |||
8dd6f6d614 | |||
43f07f58bc | |||
d8b3191612 | |||
7a5bd63fde | |||
0a6807493c |
372
Cargo.lock
generated
372
Cargo.lock
generated
@ -28,15 +28,6 @@ dependencies = [
|
|||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aho-corasick"
|
|
||||||
version = "0.6.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@ -67,6 +58,19 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ansi-to-tui"
|
||||||
|
version = "7.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
"ratatui",
|
||||||
|
"simdutf8",
|
||||||
|
"smallvec",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.17"
|
version = "0.6.17"
|
||||||
@ -152,12 +156,27 @@ dependencies = [
|
|||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bincode"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcode"
|
name = "bitcode"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
@ -293,9 +312,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono-tz"
|
name = "chrono-tz"
|
||||||
version = "0.9.0"
|
version = "0.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
|
checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz-build",
|
"chrono-tz-build",
|
||||||
@ -304,12 +323,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono-tz-build"
|
name = "chrono-tz-build"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
|
checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"parse-zoneinfo",
|
"parse-zoneinfo",
|
||||||
"phf",
|
|
||||||
"phf_codegen",
|
"phf_codegen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -483,19 +501,6 @@ dependencies = [
|
|||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "csvx"
|
name = "csvx"
|
||||||
version = "0.1.17"
|
version = "0.1.17"
|
||||||
@ -573,6 +578,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@ -742,6 +753,15 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getopts"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-width 0.1.14",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@ -869,8 +889,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironcalc"
|
name = "ironcalc"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
source = "git+https://github.com/ironcalc/IronCalc#98dc557a017b2ad640fb46eece17afda14177e59"
|
source = "git+https://github.com/ironcalc/IronCalc#b2c5027f56a16a0c606b01a071b816b941972aef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcode",
|
"bitcode",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -885,18 +905,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironcalc_base"
|
name = "ironcalc_base"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
source = "git+https://github.com/ironcalc/IronCalc#98dc557a017b2ad640fb46eece17afda14177e59"
|
source = "git+https://github.com/ironcalc/IronCalc#b2c5027f56a16a0c606b01a071b816b941972aef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcode",
|
"bitcode",
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"csv",
|
"csv",
|
||||||
"csv-sniffer",
|
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand",
|
"rand",
|
||||||
"regex 1.11.1",
|
"regex",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@ -949,18 +968,18 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazy_static"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.161"
|
version = "0.2.161"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
|
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linked-hash-map"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@ -1004,6 +1023,12 @@ version = "2.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@ -1026,6 +1051,16 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -1056,6 +1091,28 @@ version = "1.20.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "onig"
|
||||||
|
version = "6.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"onig_sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "onig_sys"
|
||||||
|
version = "69.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@ -1085,7 +1142,7 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
|
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"regex 1.11.1",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1173,6 +1230,19 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plist"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"indexmap",
|
||||||
|
"quick-xml",
|
||||||
|
"serde",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -1188,6 +1258,16 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||||
|
dependencies = [
|
||||||
|
"diff",
|
||||||
|
"yansi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.2.0"
|
version = "3.2.0"
|
||||||
@ -1206,6 +1286,34 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"getopts",
|
||||||
|
"memchr",
|
||||||
|
"pulldown-cmark-escape",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark-escape"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.37"
|
version = "1.0.37"
|
||||||
@ -1284,29 +1392,16 @@ dependencies = [
|
|||||||
"bitflags 2.6.0",
|
"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",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick 1.1.3",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata",
|
"regex-automata",
|
||||||
"regex-syntax 0.8.5",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1315,18 +1410,9 @@ version = "0.4.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick 1.1.3",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax 0.8.5",
|
"regex-syntax",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-syntax"
|
|
||||||
version = "0.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7"
|
|
||||||
dependencies = [
|
|
||||||
"ucd-util",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1370,7 +1456,7 @@ dependencies = [
|
|||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"regex 1.11.1",
|
"regex",
|
||||||
"relative-path",
|
"relative-path",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"syn",
|
"syn",
|
||||||
@ -1417,6 +1503,15 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -1498,6 +1593,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"slice-utils",
|
"slice-utils",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tui-markdown",
|
||||||
"tui-popup",
|
"tui-popup",
|
||||||
"tui-prompts",
|
"tui-prompts",
|
||||||
"tui-textarea",
|
"tui-textarea",
|
||||||
@ -1539,6 +1635,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simdutf8"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@ -1616,6 +1718,28 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syntect"
|
||||||
|
version = "5.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1"
|
||||||
|
dependencies = [
|
||||||
|
"bincode",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"flate2",
|
||||||
|
"fnv",
|
||||||
|
"once_cell",
|
||||||
|
"onig",
|
||||||
|
"plist",
|
||||||
|
"regex-syntax",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"walkdir",
|
||||||
|
"yaml-rust",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.65"
|
version = "1.0.65"
|
||||||
@ -1636,15 +1760,6 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.36"
|
version = "0.3.36"
|
||||||
@ -1652,10 +1767,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
|
"itoa",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
"serde",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1664,6 +1781,16 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.8"
|
version = "0.6.8"
|
||||||
@ -1681,6 +1808,53 @@ dependencies = [
|
|||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tui-markdown"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faf7ca4141b6846fae9ca363e7cf00277978d999bf85bfd40b4f569305994c6b"
|
||||||
|
dependencies = [
|
||||||
|
"ansi-to-tui",
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"pretty_assertions",
|
||||||
|
"pulldown-cmark",
|
||||||
|
"ratatui",
|
||||||
|
"rstest",
|
||||||
|
"syntect",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tui-popup"
|
name = "tui-popup"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@ -1723,10 +1897,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ucd-util"
|
name = "unicase"
|
||||||
version = "0.1.10"
|
version = "2.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003"
|
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
@ -1763,12 +1937,6 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8-ranges"
|
|
||||||
version = "1.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@ -1781,6 +1949,16 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
@ -1858,6 +2036,15 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -1964,6 +2151,21 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaml-rust"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||||
|
dependencies = [
|
||||||
|
"linked-hash-map",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yansi"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.7.35"
|
version = "0.7.35"
|
||||||
|
@ -10,13 +10,15 @@ anyhow = { version = "1.0.91", features = ["backtrace"] }
|
|||||||
clap = { version = "4.5.20", features = ["derive"] }
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
crossterm = { version = "0.28.1", features = ["event-stream", "serde"] }
|
crossterm = { version = "0.28.1", features = ["event-stream", "serde"] }
|
||||||
csvx = "0.1.17"
|
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" }
|
ironcalc = { git = "https://github.com/ironcalc/IronCalc" }
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
thiserror = "1.0.65"
|
thiserror = "1.0.65"
|
||||||
tui-textarea = "0.7.0"
|
tui-textarea = "0.7.0"
|
||||||
tui-prompts = "0.5.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"
|
tui-popup = "0.6.0"
|
||||||
serde_json = "1.0.133"
|
serde_json = "1.0.133"
|
||||||
colorsys = "0.6.7"
|
colorsys = "0.6.7"
|
||||||
|
tui-markdown = { version = "0.3.1", features = [] }
|
||||||
|
20
docs/command.md
Normal file
20
docs/command.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Command Mode
|
||||||
|
|
||||||
|
You enter command mode by typing `:` while in navigation mode. You can then
|
||||||
|
type a command and hit `Enter` to execute it or `Esc` to cancel.
|
||||||
|
|
||||||
|
The currently supported commands are:
|
||||||
|
|
||||||
|
* `write [path]` save the current spreadsheet. If the path is provided it will save it to that path. If omitted it will save to the path you are currently editing. `w` is a shorthand alias for this command.
|
||||||
|
* `insert-rows [number]` Inserts a row into the sheet at your current row. If the number is provided then inserts that many rows. If omitted then just inserts one.
|
||||||
|
* `insert-cols [number]` Just line `insert-rows` but for columns.
|
||||||
|
* `rename-sheet [idx] <name>` rename a sheet. If the idx is provide then renames that sheet. If omitted then it renames the current sheet.
|
||||||
|
* `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.
|
||||||
|
* `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
|
||||||
|
prompt you if the current spreadsheet has not been saved yet. So your changes
|
||||||
|
will be discarded if you have not saved first.</aside>
|
||||||
|
|
31
docs/edit.md
Normal file
31
docs/edit.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Edit Mode
|
||||||
|
|
||||||
|
You enter Edit mode by hitting `e` or `i` while in navigation mode. Type
|
||||||
|
what you want into the cell.
|
||||||
|
|
||||||
|
Starting with:
|
||||||
|
|
||||||
|
* `=` will treat what you type as a formula.
|
||||||
|
* `$` will treat it as us currency.
|
||||||
|
|
||||||
|
Typing a number will treat the contents as a number. While typing non-numeric
|
||||||
|
text will treat it as text content.
|
||||||
|
|
||||||
|
<aside>We do not yet support modifying the type of a cell after the fact. We
|
||||||
|
may add this in the future.</aside>
|
||||||
|
|
||||||
|
For the most part this should work the same way you expect a spreadsheet to
|
||||||
|
work.
|
||||||
|
|
||||||
|
* `Enter` will update the cell contents.
|
||||||
|
* `Esc` will cancel editing the cell and leave it unedited.
|
||||||
|
* `Ctrl-p` will paste the range selection if it exists into the cell.
|
||||||
|
|
||||||
|
`Ctrl-r` will enter range select mode when editing a formula. You can navigate
|
||||||
|
around the sheet and hit space to select that cell in the sheet to set the
|
||||||
|
start of the range. Navigate some more and hit space to set the end of the
|
||||||
|
range.
|
||||||
|
|
||||||
|
You can find the functions we support documented here:
|
||||||
|
[ironcalc docs](https://docs.ironcalc.com/functions/lookup-and-reference.html)
|
||||||
|
|
124
docs/index.md
124
docs/index.md
@ -33,123 +33,9 @@ The sheetui user interface is loosely inspired by vim. It is a modal interface
|
|||||||
that is entirely keyboard driven. At nearly any time you can type `Alt-h` to
|
that is entirely keyboard driven. At nearly any time you can type `Alt-h` to
|
||||||
get some context sensitive help.
|
get some context sensitive help.
|
||||||
|
|
||||||
### Navigation Mode
|
### Modal Docs
|
||||||
|
|
||||||
The interface will start out in navigation mode. You can navigate around the
|
* [Navigation](./navigation.md)
|
||||||
table and between the sheets using the following keybinds:
|
* [Edit](./edit.md)
|
||||||
|
* [Visual](./visual.md)
|
||||||
**Cell Navigation**
|
* [Command](./command.md)
|
||||||
|
|
||||||
* `h`, ⬆️, and `TAB` will move one cell to the left.
|
|
||||||
* `l` and, ➡️ will move one cell to the right.
|
|
||||||
* `j`, ⬇️, and `Enter` will move one cell down.
|
|
||||||
* `k` ⬆️, will move one cell up.
|
|
||||||
* `d` will delete the contents of the selected cell leaving style untouched
|
|
||||||
* `D` will delete the contents of the selected cell including any style
|
|
||||||
* `gg` will go to the top row in the current column
|
|
||||||
|
|
||||||
**Sheet Navigation**
|
|
||||||
|
|
||||||
* `Ctrl-n` moves to the next sheet
|
|
||||||
* `Ctrl-p` moves to the prev sheet
|
|
||||||
|
|
||||||
Sheet navigation moving will loop around when you reach the ends.
|
|
||||||
|
|
||||||
**Numeric prefixes**
|
|
||||||
|
|
||||||
You can prefix each of the keybinds above with a numeric prefix to do them that
|
|
||||||
many times. So typing `123h` will move to the left 123 times. Hitting `Esc`
|
|
||||||
will clear the numeric prefix if you want to cancel it.
|
|
||||||
|
|
||||||
**Modifying the Sheet or Cells**
|
|
||||||
|
|
||||||
* `e` or `i` will enter CellEdit mode for the current cell.
|
|
||||||
* `Ctrl-h` will shorten the width of the column you are on.
|
|
||||||
* `Ctrl-l` will lengthen the width of the column you are on.
|
|
||||||
|
|
||||||
**Other Keybindings**
|
|
||||||
|
|
||||||
* `Ctrl-r` will enter range selection mode.
|
|
||||||
* `v` will enter range selection mode with the start of the range already selected.
|
|
||||||
* `Ctrl-s` will save the sheet.
|
|
||||||
* `Ctrl-c`, `y` Copy the cell or range contents.
|
|
||||||
* `Ctrl-v`, `p` Paste into the sheet.
|
|
||||||
* `Ctrl-Shift-C` Copy the cell or range formatted content.
|
|
||||||
* `q` will exit the application.
|
|
||||||
* `:` will enter CommandMode.
|
|
||||||
|
|
||||||
Range selections made from navigation mode will be available to paste into a Cell Edit.
|
|
||||||
|
|
||||||
<aside>Note that for `q` this will not currently prompt you if the sheet is not
|
|
||||||
saved.</aside>
|
|
||||||
|
|
||||||
### CellEdit Mode
|
|
||||||
|
|
||||||
You enter CellEdit mode by hitting `e` or `i` while in navigation mode. Type
|
|
||||||
what you want into the cell.
|
|
||||||
|
|
||||||
Starting with:
|
|
||||||
|
|
||||||
* `=` will treat what you type as a formula.
|
|
||||||
* `$` will treat it as us currency.
|
|
||||||
|
|
||||||
Typing a number will treat the contents as a number. While typing non-numeric
|
|
||||||
text will treat it as text content.
|
|
||||||
|
|
||||||
<aside>We do not yet support modifying the type of a cell after the fact. We
|
|
||||||
may add this in the future.</aside>
|
|
||||||
|
|
||||||
For the most part this should work the same way you expect a spreadsheet to
|
|
||||||
work.
|
|
||||||
|
|
||||||
* `Enter` will update the cell contents.
|
|
||||||
* `Esc` will cancel editing the cell and leave it unedited.
|
|
||||||
* `Ctrl-p` will paste the range selection if it exists into the cell.
|
|
||||||
|
|
||||||
`Ctrl-r` will enter range select mode when editing a formula. You can navigate
|
|
||||||
around the sheet and hit space to select that cell in the sheet to set the
|
|
||||||
start of the range. Navigate some more and hit space to set the end of the
|
|
||||||
range.
|
|
||||||
|
|
||||||
You can find the functions we support documented here:
|
|
||||||
[ironcalc docs](https://docs.ironcalc.com/functions/lookup-and-reference.html)
|
|
||||||
|
|
||||||
### Command Mode
|
|
||||||
|
|
||||||
You enter command mode by typing `:` while in navigation mode. You can then
|
|
||||||
type a command and hit `Enter` to execute it or `Esc` to cancel.
|
|
||||||
|
|
||||||
The currently supported commands are:
|
|
||||||
|
|
||||||
* `write [path]` save the current spreadsheet. If the path is provided it will save it to that path. If omitted it will save to the path you are currently editing. `w` is a shorthand alias for this command.
|
|
||||||
* `insert-rows [number]` Inserts a row into the sheet at your current row. If the number is provided then inserts that many rows. If omitted then just inserts one.
|
|
||||||
* `insert-cols [number]` Just line `insert-rows` but for columns.
|
|
||||||
* `rename-sheet [idx] <name>` rename a sheet. If the idx is provide then renames that sheet. If omitted then it renames the current sheet.
|
|
||||||
* `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.
|
|
||||||
* `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
|
|
||||||
prompt you if the current spreadsheet has not been saved yet. So your changes
|
|
||||||
will be discarded if you have not saved first.</aside>
|
|
||||||
|
|
||||||
### Range Select Mode
|
|
||||||
|
|
||||||
Range Select mode copies a range reference for use later or delete a range's contents. You can enter range
|
|
||||||
select mode from CellEdit mode with `CTRL-r`.
|
|
||||||
|
|
||||||
* `h`, `j`, `k`, `l` will navigate around the sheet.
|
|
||||||
* `Ctrl-n`, `Ctrl-p` will navigate between sheets.
|
|
||||||
* `Ctrl-c`, `y` Copy the cell or range contents.
|
|
||||||
* `Ctrl-Shift-C`, 'Y' Copy the cell or range formatted content.
|
|
||||||
* `The spacebar will select the start and end of the range respectively.
|
|
||||||
* `d` will delete the contents of the range leaving any style untouched
|
|
||||||
* `D` will delete the contents of the range including any style
|
|
||||||
|
|
||||||
When you have selected the end of the range you will exit range select mode and
|
|
||||||
the range reference will be placed into the cell contents you are editing.
|
|
||||||
|
|
||||||
<aside>We only support continuous ranges for the moment. Planned for
|
|
||||||
discontinuous ranges still needs the interaction interface to be
|
|
||||||
determined.</aside>
|
|
||||||
|
23
docs/intro.md
Normal file
23
docs/intro.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Intro
|
||||||
|
|
||||||
|
## Supported formats
|
||||||
|
|
||||||
|
Currently we only support the [ironcalc](https://docs.ironcalc.com/) xlsx
|
||||||
|
features for spreadsheet. I plan to handle csv import and export at some point.
|
||||||
|
I also might support other export formats as well but for the moment just csv
|
||||||
|
and it's variants such as tsv are in the roadmap.
|
||||||
|
|
||||||
|
## User Interface
|
||||||
|
|
||||||
|
The sheetui user interface is loosely inspired by vim. It is a modal interface
|
||||||
|
that is entirely keyboard driven. At nearly any time you can type `Alt-h` to
|
||||||
|
get some context sensitive help.
|
||||||
|
|
||||||
|
## Modal Docs
|
||||||
|
|
||||||
|
To get help on each modality in command mode `:` type
|
||||||
|
|
||||||
|
* `help navigate`
|
||||||
|
* `help edit`
|
||||||
|
* `help command`
|
||||||
|
* `help visual`
|
51
docs/navigation.md
Normal file
51
docs/navigation.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Navigation Mode
|
||||||
|
|
||||||
|
The interface will start out in navigation mode. You can navigate around the
|
||||||
|
table and between the sheets using the following keybinds:
|
||||||
|
|
||||||
|
## Cell Navigation
|
||||||
|
|
||||||
|
* `h`, ⬆️, and `TAB` will move one cell to the left.
|
||||||
|
* `l` and, ➡️ will move one cell to the right.
|
||||||
|
* `j`, ⬇️, and `Enter` will move one cell down.
|
||||||
|
* `k` ⬆️, will move one cell up.
|
||||||
|
* `d` will delete the contents of the selected cell leaving style untouched
|
||||||
|
* `D` will delete the contents of the selected cell including any style
|
||||||
|
* `gg` will go to the top row in the current column
|
||||||
|
|
||||||
|
## Sheet Navigation
|
||||||
|
|
||||||
|
* `Ctrl-n` moves to the next sheet
|
||||||
|
* `Ctrl-p` moves to the prev sheet
|
||||||
|
|
||||||
|
Sheet navigation moving will loop around when you reach the ends.
|
||||||
|
|
||||||
|
## Numeric prefixes
|
||||||
|
|
||||||
|
You can prefix each of the keybinds above with a numeric prefix to do them that
|
||||||
|
many times. So typing `123h` will move to the left 123 times. Hitting `Esc`
|
||||||
|
will clear the numeric prefix if you want to cancel it.
|
||||||
|
|
||||||
|
**Modifying the Sheet or Cells**
|
||||||
|
|
||||||
|
* `e` or `i` will enter CellEdit mode for the current cell.
|
||||||
|
* 'I' will toggle italic on the cell. 'B' will toggle bold.
|
||||||
|
* `Ctrl-h` will shorten the width of the column you are on.
|
||||||
|
* `Ctrl-l` will lengthen the width of the column you are on.
|
||||||
|
|
||||||
|
## Other Keybindings
|
||||||
|
|
||||||
|
* `Ctrl-r` will enter range selection mode.
|
||||||
|
* `v` will enter range selection mode with the start of the range already selected.
|
||||||
|
* `Ctrl-s` will save the sheet.
|
||||||
|
* `Ctrl-c`, `y` Copy the cell or range contents.
|
||||||
|
* `Ctrl-v`, `p` Paste into the sheet.
|
||||||
|
* `Ctrl-Shift-C` Copy the cell or range formatted content.
|
||||||
|
* `q` will exit the application.
|
||||||
|
* `:` will enter CommandMode.
|
||||||
|
|
||||||
|
Range selections made from navigation mode will be available to paste into a Cell Edit.
|
||||||
|
|
||||||
|
<aside>Note that for `q` this will not currently prompt you if the sheet is not
|
||||||
|
saved.</aside>
|
||||||
|
|
19
docs/visual.md
Normal file
19
docs/visual.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Range Select Mode
|
||||||
|
|
||||||
|
Range Select mode copies a range reference for use later or delete a range's contents. You can enter range
|
||||||
|
select mode from CellEdit mode with `CTRL-r`.
|
||||||
|
|
||||||
|
* `h`, `j`, `k`, `l` will navigate around the sheet.
|
||||||
|
* `Ctrl-n`, `Ctrl-p` will navigate between sheets.
|
||||||
|
* `Ctrl-c`, `y` Copy the cell or range contents.
|
||||||
|
* `Ctrl-Shift-C`, 'Y' Copy the cell or range formatted content.
|
||||||
|
* `The spacebar will select the start and end of the range respectively.
|
||||||
|
* `d` will delete the contents of the range leaving any style untouched
|
||||||
|
* `D` will delete the contents of the range including any style
|
||||||
|
|
||||||
|
When you have selected the end of the range you will exit range select mode and
|
||||||
|
the range reference will be placed into the cell contents you are editing.
|
||||||
|
|
||||||
|
<aside>We only support continuous ranges for the moment. Planned for
|
||||||
|
discontinuous ranges still needs the interaction interface to be
|
||||||
|
determined.</aside>
|
Binary file not shown.
367
src/book/mod.rs
367
src/book/mod.rs
@ -3,9 +3,10 @@ use std::cmp::max;
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use ironcalc::{
|
use ironcalc::{
|
||||||
base::{
|
base::{
|
||||||
types::{Border, Col, Fill, Font, Row, SheetData, Style, Worksheet},
|
expressions::types::Area,
|
||||||
|
types::{SheetData, Style, Worksheet},
|
||||||
worksheet::WorksheetDimension,
|
worksheet::WorksheetDimension,
|
||||||
Model,
|
Model, UserModel,
|
||||||
},
|
},
|
||||||
export::save_xlsx_to_writer,
|
export::save_xlsx_to_writer,
|
||||||
import::load_from_xlsx,
|
import::load_from_xlsx,
|
||||||
@ -16,7 +17,12 @@ use crate::ui::Address;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod 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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AddressRange<'book> {
|
pub struct AddressRange<'book> {
|
||||||
@ -37,7 +43,7 @@ impl<'book> AddressRange<'book> {
|
|||||||
}
|
}
|
||||||
rows
|
rows
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_series(&self) -> Vec<Address> {
|
pub fn as_series(&self) -> Vec<Address> {
|
||||||
let (row_range, col_range) = self.get_ranges();
|
let (row_range, col_range) = self.get_ranges();
|
||||||
let mut rows = Vec::with_capacity(row_range.len() * col_range.len());
|
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.
|
/// A spreadsheet book with some internal state tracking.
|
||||||
pub struct Book {
|
pub struct Book {
|
||||||
pub(crate) model: Model,
|
pub(crate) model: UserModel,
|
||||||
pub current_sheet: u32,
|
pub current_sheet: u32,
|
||||||
pub location: crate::ui::Address,
|
pub location: crate::ui::Address,
|
||||||
}
|
}
|
||||||
|
|
||||||
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: UserModel) -> Self {
|
||||||
Self {
|
Self {
|
||||||
model,
|
model,
|
||||||
current_sheet: 0,
|
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.
|
/// Construct a new book from an xlsx file.
|
||||||
pub fn new_from_xlsx(path: &str) -> Result<Self> {
|
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.
|
/// Evaluate the spreadsheet calculating formulas and style changes.
|
||||||
@ -104,10 +118,9 @@ impl Book {
|
|||||||
self.model.evaluate();
|
self.model.evaluate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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_with_locale(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::from_model(load_from_xlsx(path, locale, tz)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save book to an xlsx file.
|
/// Save book to an xlsx file.
|
||||||
@ -116,7 +129,7 @@ impl Book {
|
|||||||
let file_path = std::path::Path::new(path);
|
let file_path = std::path::Path::new(path);
|
||||||
let file = std::fs::File::create(file_path)?;
|
let file = std::fs::File::create(file_path)?;
|
||||||
let writer = std::io::BufWriter::new(file);
|
let writer = std::io::BufWriter::new(file);
|
||||||
save_xlsx_to_writer(&self.model, writer)?;
|
save_xlsx_to_writer(self.model.get_model(), writer)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,10 +137,9 @@ impl Book {
|
|||||||
/// is the sheet name and the u32 is the sheet index.
|
/// is the sheet name and the u32 is the sheet index.
|
||||||
pub fn get_all_sheets_identifiers(&self) -> Vec<(String, u32)> {
|
pub fn get_all_sheets_identifiers(&self) -> Vec<(String, u32)> {
|
||||||
self.model
|
self.model
|
||||||
.workbook
|
.get_worksheets_properties()
|
||||||
.worksheets
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|sheet| (sheet.get_name(), sheet.get_sheet_id()))
|
.map(|sheet| (sheet.name.to_owned(), sheet.sheet_id))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,16 +148,22 @@ impl Book {
|
|||||||
Ok(&self.get_sheet()?.name)
|
Ok(&self.get_sheet()?.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_sheet_name(&mut self, idx: usize, sheet_name: &str) -> Result<()> {
|
pub fn set_sheet_name(&mut self, idx: u32, sheet_name: &str) -> Result<()> {
|
||||||
self.get_sheet_by_idx_mut(idx)?.set_name(sheet_name);
|
self.model
|
||||||
|
.rename_sheet(idx, sheet_name)
|
||||||
|
.map_err(|e| anyhow!(e))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_sheet(&mut self, sheet_name: Option<&str>) -> Result<()> {
|
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 {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +192,7 @@ impl Book {
|
|||||||
{
|
{
|
||||||
let contents = self
|
let contents = self
|
||||||
.model
|
.model
|
||||||
|
.get_model()
|
||||||
.extend_to(
|
.extend_to(
|
||||||
self.current_sheet,
|
self.current_sheet,
|
||||||
from.row as i32,
|
from.row as i32,
|
||||||
@ -187,7 +206,7 @@ impl Book {
|
|||||||
self.current_sheet,
|
self.current_sheet,
|
||||||
cell.row as i32,
|
cell.row as i32,
|
||||||
cell.col as i32,
|
cell.col as i32,
|
||||||
contents,
|
&contents,
|
||||||
)
|
)
|
||||||
.map_err(|e| anyhow!(e))?;
|
.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<()> {
|
pub fn clear_cell_contents(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.model
|
.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))?)
|
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_cell_range(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> {
|
pub fn clear_cell_range(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> {
|
||||||
for row in start.row..=end.row {
|
let area = calculate_area(sheet, &start, &end);
|
||||||
for col in start.col..=end.col {
|
self.model
|
||||||
self.clear_cell_contents(sheet, Address { row, col })?;
|
.range_clear_contents(&area)
|
||||||
}
|
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?;
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_cell_all(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> {
|
pub fn clear_cell_all(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.model
|
.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))?)
|
.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<()> {
|
pub fn clear_cell_range_all(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> {
|
||||||
for row in start.row..=end.row {
|
let area = calculate_area(sheet, &start, &end);
|
||||||
for col in start.col..=end.col {
|
self.model
|
||||||
self.clear_cell_all(sheet, Address { row, col })?;
|
.range_clear_all(&area)
|
||||||
}
|
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?;
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,114 +273,105 @@ impl Book {
|
|||||||
// TODO(jwall): This is modeled a little weird. We should probably record
|
// 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
|
// the error *somewhere* but for the user there is nothing to be done except
|
||||||
// not use a style.
|
// 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,
|
Err(_) => None,
|
||||||
Ok(s) => Some(s),
|
Ok(s) => Some(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_column(&self, sheet: u32, col: usize) -> Result<Option<&Col>> {
|
/// Set the cell style
|
||||||
Ok(self.model.workbook.worksheet(sheet)
|
/// Valid style paths are:
|
||||||
.map_err(|e| anyhow!("{}", e))?.cols.get(col))
|
/// * fill.bg_color background color
|
||||||
}
|
/// * fill.fg_color foreground color
|
||||||
|
/// * font.b bold
|
||||||
fn get_row(&self, sheet: u32, col: usize) -> Result<Option<&Row>> {
|
/// * font.i italicize
|
||||||
Ok(self.model.workbook.worksheet(sheet)
|
/// * font.strike strikethrough
|
||||||
.map_err(|e| anyhow!("{}", e))?.rows.get(col))
|
/// * font.color font color
|
||||||
}
|
/// * num_fmt number format
|
||||||
|
/// * alignment turn off alignment
|
||||||
pub fn get_column_style(&self, sheet: u32, col: usize) -> Result<Option<Style>> {
|
/// * alignment.horizontal make alignment horzontal
|
||||||
// TODO(jwall): This is modeled a little weird. We should probably record
|
/// * alignment.vertical make alignment vertical
|
||||||
// the error *somewhere* but for the user there is nothing to be done except
|
/// * alignment.wrap_text wrap cell text
|
||||||
// not use a style.
|
pub fn set_cell_style(&mut self, style: &[(&str, &str)], area: &Area) -> Result<()> {
|
||||||
if let Some(col) = self.get_column(sheet, col)? {
|
for (path, val) in style {
|
||||||
if let Some(style_idx) = col.style.map(|idx| idx as usize) {
|
self.model
|
||||||
let styles = &self.model.workbook.styles;
|
.update_range_style(area, path, val)
|
||||||
if styles.cell_style_xfs.len() <= style_idx {
|
.map_err(|s| anyhow!("Unable to format cell {}", s))?;
|
||||||
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 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_col_style(&mut self, style: &Style, sheet: u32, col: usize) -> Result<()> {
|
fn get_col_range(&self, sheet: u32, col_idx: usize) -> Area {
|
||||||
let idx = self.create_or_get_style_idx(style);
|
Area {
|
||||||
let sheet = self.model.workbook.worksheet_mut(sheet)
|
sheet,
|
||||||
.map_err(|e| anyhow!("{}", e))?;
|
row: 1,
|
||||||
let width = sheet.get_column_width(col as i32)
|
column: col_idx as i32,
|
||||||
.map_err(|e| anyhow!("{}", e))?;
|
width: 1,
|
||||||
sheet.set_column_style(col as i32, idx)
|
height: LAST_ROW,
|
||||||
.map_err(|e| anyhow!("{}", e))?;
|
}
|
||||||
sheet.set_column_width(col as i32, width)
|
}
|
||||||
.map_err(|e| anyhow!("{}", e))?;
|
|
||||||
|
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<()> {
|
||||||
|
let area = self.get_col_range(sheet, col_idx);
|
||||||
|
self.set_cell_style(style, &area)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_row_style(&mut self, style: &Style, sheet: u32, row: usize) -> Result<()> {
|
/// Set the row style
|
||||||
let idx = self.create_or_get_style_idx(style);
|
/// Valid style paths are:
|
||||||
self.model.workbook.worksheet_mut(sheet)
|
/// * fill.bg_color background color
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
/// * fill.fg_color foreground color
|
||||||
.set_row_style(row as i32, idx)
|
/// * font.b bold
|
||||||
.map_err(|e| anyhow!("{}", e))?;
|
/// * 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<()> {
|
||||||
|
let area = self.get_row_range(sheet, row_idx);
|
||||||
|
self.set_cell_style(style, &area)?;
|
||||||
Ok(())
|
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.
|
/// Get a cells rendered content for display.
|
||||||
pub fn get_cell_addr_rendered(&self, Address { row, col }: &Address) -> Result<String> {
|
pub fn get_cell_addr_rendered(&self, Address { row, col }: &Address) -> Result<String> {
|
||||||
Ok(self
|
Ok(self
|
||||||
@ -382,20 +402,21 @@ impl Book {
|
|||||||
|
|
||||||
/// Update the current cell in a book.
|
/// Update the current cell in a book.
|
||||||
/// This update won't be reflected until you call `Book::evaluate`.
|
/// 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)?;
|
self.update_cell(&self.location.clone(), value)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update an entry in the current sheet for a book.
|
/// Update an entry in the current sheet for a book.
|
||||||
/// This update won't be reflected until you call `Book::evaluate`.
|
/// 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
|
self.model
|
||||||
.set_user_input(
|
.set_user_input(
|
||||||
self.current_sheet,
|
self.current_sheet,
|
||||||
location.row as i32,
|
location.row as i32,
|
||||||
location.col 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))?;
|
.map_err(|e| anyhow!("Invalid cell contents: {}", e))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -403,9 +424,11 @@ impl Book {
|
|||||||
|
|
||||||
/// Insert `count` rows at a `row_idx`.
|
/// Insert `count` rows at a `row_idx`.
|
||||||
pub fn insert_rows(&mut self, row_idx: usize, count: usize) -> Result<()> {
|
pub fn insert_rows(&mut self, row_idx: usize, count: usize) -> Result<()> {
|
||||||
self.model
|
for i in 0..count {
|
||||||
.insert_rows(self.current_sheet, row_idx as i32, count as i32)
|
self.model
|
||||||
.map_err(|e| anyhow!("Unable to insert row(s): {}", e))?;
|
.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 {
|
if self.location.row >= row_idx {
|
||||||
self.move_to(&Address {
|
self.move_to(&Address {
|
||||||
row: self.location.row + count,
|
row: self.location.row + count,
|
||||||
@ -417,9 +440,11 @@ impl Book {
|
|||||||
|
|
||||||
/// Insert `count` columns at a `col_idx`.
|
/// Insert `count` columns at a `col_idx`.
|
||||||
pub fn insert_columns(&mut self, col_idx: usize, count: usize) -> Result<()> {
|
pub fn insert_columns(&mut self, col_idx: usize, count: usize) -> Result<()> {
|
||||||
self.model
|
for i in 0..count {
|
||||||
.insert_columns(self.current_sheet, col_idx as i32, count as i32)
|
self.model
|
||||||
.map_err(|e| anyhow!("Unable to insert column(s): {}", e))?;
|
.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 {
|
if self.location.col >= col_idx {
|
||||||
self.move_to(&Address {
|
self.move_to(&Address {
|
||||||
row: self.location.row,
|
row: self.location.row,
|
||||||
@ -436,16 +461,33 @@ impl Book {
|
|||||||
|
|
||||||
/// Get column size
|
/// Get column size
|
||||||
pub fn get_col_size(&self, idx: usize) -> Result<usize> {
|
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
|
Ok((self
|
||||||
.get_sheet()?
|
.model
|
||||||
.get_column_width(idx as i32)
|
.get_column_width(sheet, idx as i32)
|
||||||
.map_err(|e| anyhow!("Error getting column width: {:?}", e))?
|
.map_err(|e| anyhow!("Error getting column width: {:?}", e))?
|
||||||
/ COL_PIXELS) as usize)
|
/ COL_PIXELS) as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_col_size(&mut self, idx: usize, cols: usize) -> Result<()> {
|
pub fn set_col_size(&mut self, col: usize, width: usize) -> Result<()> {
|
||||||
self.get_sheet_mut()?
|
self.set_column_size_for_sheet(self.current_sheet, col, width)
|
||||||
.set_column_width(idx as i32, cols as f64 * COL_PIXELS)
|
}
|
||||||
|
|
||||||
|
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))?;
|
.map_err(|e| anyhow!("Error setting column width: {:?}", e))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -468,6 +510,7 @@ impl Book {
|
|||||||
pub fn select_sheet_by_name(&mut self, name: &str) -> bool {
|
pub fn select_sheet_by_name(&mut self, name: &str) -> bool {
|
||||||
if let Some((idx, _sheet)) = self
|
if let Some((idx, _sheet)) = self
|
||||||
.model
|
.model
|
||||||
|
.get_model()
|
||||||
.workbook
|
.workbook
|
||||||
.worksheets
|
.worksheets
|
||||||
.iter()
|
.iter()
|
||||||
@ -482,25 +525,31 @@ impl Book {
|
|||||||
|
|
||||||
/// Get all sheet names
|
/// Get all sheet names
|
||||||
pub fn get_sheet_names(&self) -> Vec<String> {
|
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) {
|
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;
|
let mut next = self.current_sheet + 1;
|
||||||
if next == len {
|
if next == len {
|
||||||
next = 0;
|
next = 0;
|
||||||
}
|
}
|
||||||
|
self.model
|
||||||
|
.set_selected_sheet(next)
|
||||||
|
.expect("Unexpected error selecting sheet");
|
||||||
self.current_sheet = next;
|
self.current_sheet = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_prev_sheet(&mut self) {
|
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 {
|
let next = if self.current_sheet == 0 {
|
||||||
len - 1
|
len - 1
|
||||||
} else {
|
} else {
|
||||||
self.current_sheet - 1
|
self.current_sheet - 1
|
||||||
};
|
};
|
||||||
|
self.model
|
||||||
|
.set_selected_sheet(next)
|
||||||
|
.expect("Unexpected error selecting sheet");
|
||||||
self.current_sheet = next;
|
self.current_sheet = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,12 +557,16 @@ impl Book {
|
|||||||
pub fn select_sheet_by_id(&mut self, id: u32) -> bool {
|
pub fn select_sheet_by_id(&mut self, id: u32) -> bool {
|
||||||
if let Some((idx, _sheet)) = self
|
if let Some((idx, _sheet)) = self
|
||||||
.model
|
.model
|
||||||
|
.get_model()
|
||||||
.workbook
|
.workbook
|
||||||
.worksheets
|
.worksheets
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_idx, sheet)| sheet.sheet_id == id)
|
.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;
|
self.current_sheet = idx as u32;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -522,42 +575,46 @@ impl Book {
|
|||||||
|
|
||||||
/// Get the current `Worksheet`.
|
/// Get the current `Worksheet`.
|
||||||
pub(crate) fn get_sheet(&self) -> Result<&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
|
Ok(self
|
||||||
.model
|
.model
|
||||||
|
.get_model()
|
||||||
.workbook
|
.workbook
|
||||||
.worksheet(self.current_sheet)
|
.worksheet(self.current_sheet)
|
||||||
.map_err(|s| anyhow!("Invalid Worksheet id: {}: error: {}", self.current_sheet, s))?)
|
.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> {
|
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
|
Ok(&self
|
||||||
.model
|
.model
|
||||||
|
.get_model()
|
||||||
.workbook
|
.workbook
|
||||||
.worksheet(idx as u32)
|
.worksheet(idx as u32)
|
||||||
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?
|
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?
|
||||||
.name)
|
.name)
|
||||||
}
|
}
|
||||||
pub(crate) fn get_sheet_by_idx_mut(&mut self, idx: usize) -> Result<&mut Worksheet> {
|
}
|
||||||
Ok(self
|
|
||||||
.model
|
fn calculate_area(sheet: u32, start: &Address, end: &Address) -> Area {
|
||||||
.workbook
|
let area = Area {
|
||||||
.worksheet_mut(idx as u32)
|
sheet,
|
||||||
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
|
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 {
|
impl Default for Book {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut book =
|
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.update_cell(&Address { row: 1, col: 1 }, "").unwrap();
|
||||||
book
|
book
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,9 @@ pub enum Cmd<'a> {
|
|||||||
Write(Option<&'a str>),
|
Write(Option<&'a str>),
|
||||||
InsertRows(usize),
|
InsertRows(usize),
|
||||||
InsertColumns(usize),
|
InsertColumns(usize),
|
||||||
ColorRows(Option<usize>, &'a str),
|
ColorRows(Option<usize>, String),
|
||||||
ColorColumns(Option<usize>, &'a str),
|
ColorColumns(Option<usize>, String),
|
||||||
ColorCell(&'a str),
|
ColorCell(String),
|
||||||
RenameSheet(Option<usize>, &'a str),
|
RenameSheet(Option<usize>, &'a str),
|
||||||
NewSheet(Option<&'a str>),
|
NewSheet(Option<&'a str>),
|
||||||
SelectSheet(&'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) {
|
if input.remaining() > 0 && !is_ws(&mut input) {
|
||||||
return Err("Invalid command: Did you mean to type `color-cell <color>`?");
|
return Err("Invalid command: Did you mean to type `color-cell <color>`?");
|
||||||
}
|
}
|
||||||
let arg = input.span(0..).trim();
|
let arg = parse_color(input.span(0..).trim())?;
|
||||||
if arg.len() == 0 {
|
|
||||||
return Err("Invalid command: Did you mean to type `color-cell <color>`?");
|
|
||||||
}
|
|
||||||
return Ok(Some(Cmd::ColorCell(arg)));
|
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>`?");
|
return Err("Invalid command: Did you mean to type `color-rows [count] <color>`?");
|
||||||
}
|
}
|
||||||
let (idx, rest) = try_consume_usize(input.clone());
|
let (idx, rest) = try_consume_usize(input.clone());
|
||||||
let arg = rest.span(0..).trim();
|
let arg = parse_color(rest.span(0..).trim())?;
|
||||||
if arg.is_empty() {
|
|
||||||
return Err("Invalid command: `color-rows` requires a color argument");
|
|
||||||
}
|
|
||||||
return Ok(Some(Cmd::ColorRows(idx, arg)));
|
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>`?");
|
return Err("Invalid command: Did you mean to type `color-columns [count] <color>`?");
|
||||||
}
|
}
|
||||||
let (idx, rest) = try_consume_usize(input.clone());
|
let (idx, rest) = try_consume_usize(input.clone());
|
||||||
let arg = rest.span(0..).trim();
|
let arg = parse_color(rest.span(0..).trim())?;
|
||||||
if arg.is_empty() {
|
|
||||||
return Err("Invalid command: `color-columns` requires a color argument");
|
|
||||||
}
|
|
||||||
return Ok(Some(Cmd::ColorColumns(idx, arg)));
|
return Ok(Some(Cmd::ColorColumns(idx, arg)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_consume_usize<'cmd, 'i: 'cmd>(
|
pub(crate) fn parse_color(color: &str) -> Result<String, &'static str> {
|
||||||
mut input: StrCursor<'i>,
|
use colorsys::{Ansi256, Rgb};
|
||||||
) -> (Option<usize>, StrCursor<'i>) {
|
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 mut out = String::new();
|
||||||
let original_input = input.clone();
|
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);
|
out.push(*input.next().unwrap() as char);
|
||||||
}
|
}
|
||||||
if out.len() > 0 {
|
if out.len() > 0 {
|
||||||
|
12
src/ui/help/mod.rs
Normal file
12
src/ui/help/mod.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
use ratatui::text::Text;
|
||||||
|
use tui_markdown;
|
||||||
|
|
||||||
|
pub fn render_topic(topic: &str) -> Text<'static> {
|
||||||
|
match topic {
|
||||||
|
"navigate" => tui_markdown::from_str(include_str!("../../../docs/navigation.md")),
|
||||||
|
"edit" => tui_markdown::from_str(include_str!("../../../docs/edit.md")),
|
||||||
|
"command" => tui_markdown::from_str(include_str!("../../../docs/command.md")),
|
||||||
|
"visual" => tui_markdown::from_str(include_str!("../../../docs/visual.md")),
|
||||||
|
_ => tui_markdown::from_str(include_str!("../../../docs/intro.md")),
|
||||||
|
}
|
||||||
|
}
|
243
src/ui/mod.rs
243
src/ui/mod.rs
@ -1,20 +1,18 @@
|
|||||||
//! Ui rendering logic
|
//! Ui rendering logic
|
||||||
use std::{path::PathBuf, process::ExitCode};
|
use std::{path::PathBuf, process::ExitCode};
|
||||||
|
|
||||||
use crate::book::{AddressRange, Book};
|
use crate::book::{self, AddressRange, Book};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use ironcalc::base::Model;
|
use ironcalc::base::{expressions::types::Area, Model};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer, layout::{Constraint, Flex, Layout}, style::{Modifier, Style}, text::{Line, Text}, widgets::Block
|
||||||
layout::{Constraint, Flex, Layout},
|
|
||||||
style::{Modifier, Style},
|
|
||||||
widgets::Block,
|
|
||||||
};
|
};
|
||||||
use tui_prompts::{State, Status, TextPrompt, TextState};
|
use tui_prompts::{State, Status, TextPrompt, TextState};
|
||||||
use tui_textarea::{CursorMove, TextArea};
|
use tui_textarea::{CursorMove, TextArea};
|
||||||
|
|
||||||
|
mod help;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -81,7 +79,7 @@ pub struct AppState<'ws> {
|
|||||||
pub char_queue: Vec<char>,
|
pub char_queue: Vec<char>,
|
||||||
pub range_select: RangeSelection,
|
pub range_select: RangeSelection,
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
popup: Vec<String>,
|
popup: Text<'ws>,
|
||||||
clipboard: Option<ClipboardContents>,
|
clipboard: Option<ClipboardContents>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +129,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.
|
/// The Address in a Table.
|
||||||
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
|
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
|
||||||
pub struct Address {
|
pub struct Address {
|
||||||
@ -187,7 +185,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
|
|
||||||
pub fn new_empty(locale: &str, tz: &str) -> Result<Self> {
|
pub fn new_empty(locale: &str, tz: &str) -> Result<Self> {
|
||||||
Ok(Self::new(
|
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(),
|
PathBuf::default(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -235,7 +233,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
/// Move a row down in the current sheet.
|
/// Move a row down in the current sheet.
|
||||||
pub fn move_down(&mut self) -> Result<()> {
|
pub fn move_down(&mut self) -> Result<()> {
|
||||||
let mut loc = self.book.location.clone();
|
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;
|
loc.row += 1;
|
||||||
self.book.move_to(&loc)?;
|
self.book.move_to(&loc)?;
|
||||||
}
|
}
|
||||||
@ -244,10 +242,13 @@ impl<'ws> Workspace<'ws> {
|
|||||||
|
|
||||||
/// Move to the top row without changing columns
|
/// Move to the top row without changing columns
|
||||||
pub fn move_to_top(&mut self) -> Result<()> {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move a row up in the current sheet.
|
/// Move a row up in the current sheet.
|
||||||
pub fn move_up(&mut self) -> Result<()> {
|
pub fn move_up(&mut self) -> Result<()> {
|
||||||
let mut loc = self.book.location.clone();
|
let mut loc = self.book.location.clone();
|
||||||
@ -271,7 +272,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
/// Move a column to the left in the current sheet.
|
/// Move a column to the left in the current sheet.
|
||||||
pub fn move_right(&mut self) -> Result<()> {
|
pub fn move_right(&mut self) -> Result<()> {
|
||||||
let mut loc = self.book.location.clone();
|
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;
|
loc.col += 1;
|
||||||
self.book.move_to(&loc)?;
|
self.book.move_to(&loc)?;
|
||||||
}
|
}
|
||||||
@ -293,54 +294,16 @@ impl<'ws> Workspace<'ws> {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_help_text(&self) -> Vec<String> {
|
fn render_help_text(&self) -> Text<'static> {
|
||||||
// TODO(zaphar): We should be sourcing these from our actual help documentation.
|
// TODO(zaphar): We should be sourcing these from our actual help documentation.
|
||||||
// Ideally we would also render the markdown content properly.
|
// Ideally we would also render the markdown content properly.
|
||||||
// https://github.com/zaphar/sheetsui/issues/22
|
// https://github.com/zaphar/sheetsui/issues/22
|
||||||
match self.state.modality() {
|
match self.state.modality() {
|
||||||
Modality::Navigate => vec![
|
Modality::Navigate => help::render_topic("navigate"),
|
||||||
"Navigate Mode:".to_string(),
|
Modality::CellEdit => help::render_topic("edit"),
|
||||||
"* e,i: Enter edit mode for current cell".to_string(),
|
Modality::Command => help::render_topic("command"),
|
||||||
"* ENTER/RETURN: Go down one cell".to_string(),
|
Modality::RangeSelect => help::render_topic("visual"),
|
||||||
"* TAB: Go over one cell".to_string(),
|
_ => help::render_topic(""),
|
||||||
"* h,j,k,l: vim style navigation".to_string(),
|
|
||||||
"* d: clear cell contents leaving style untouched".to_string(),
|
|
||||||
"* D: clear cell contents including style".to_string(),
|
|
||||||
"* CTRl-r: Add a row".to_string(),
|
|
||||||
"* CTRl-c: Add a column".to_string(),
|
|
||||||
"* CTRl-l: Grow column width by 1".to_string(),
|
|
||||||
"* CTRl-h: Shrink column width by 1".to_string(),
|
|
||||||
"* CTRl-n: Next sheet. Starts over at beginning if at end.".to_string(),
|
|
||||||
"* CTRl-p: Previous sheet. Starts over at end if at beginning.".to_string(),
|
|
||||||
"* ALT-h: Previous sheet. Starts over at end if at beginning.".to_string(),
|
|
||||||
"* q exit".to_string(),
|
|
||||||
"* Ctrl-S Save sheet".to_string(),
|
|
||||||
],
|
|
||||||
Modality::CellEdit => vec![
|
|
||||||
"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(),
|
|
||||||
"* ESC: Exit edit mode and discard changes".to_string(),
|
|
||||||
"Otherwise edit as normal".to_string(),
|
|
||||||
],
|
|
||||||
Modality::Command => vec![
|
|
||||||
"Command Mode:".to_string(),
|
|
||||||
"* ESC: Exit command mode".to_string(),
|
|
||||||
"* CTRL-?: Exit command mode".to_string(),
|
|
||||||
"* ENTER/RETURN: run command and exit command mode".to_string(),
|
|
||||||
],
|
|
||||||
Modality::RangeSelect => vec![
|
|
||||||
"Range Selection Mode:".to_string(),
|
|
||||||
"* ESC: Exit command mode".to_string(),
|
|
||||||
"* h,j,k,l: vim style navigation".to_string(),
|
|
||||||
"* d: delete the contents of the range leaving style untouched".to_string(),
|
|
||||||
"* D: clear cell contents including style".to_string(),
|
|
||||||
"* Spacebar: Select start and end of range".to_string(),
|
|
||||||
"* CTRl-n: Next sheet. Starts over at beginning if at end.".to_string(),
|
|
||||||
"* CTRl-p: Previous sheet. Starts over at end if at beginning.".to_string(),
|
|
||||||
],
|
|
||||||
_ => vec!["General help".to_string()],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -420,8 +383,8 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.load_into(path)?;
|
self.load_into(path)?;
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Ok(Some(Cmd::Help(_maybe_topic))) => {
|
Ok(Some(Cmd::Help(maybe_topic))) => {
|
||||||
self.enter_dialog_mode(vec!["TODO help topic".to_owned()]);
|
self.enter_dialog_mode(help::render_topic(maybe_topic.unwrap_or("")));
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Ok(Some(Cmd::Write(maybe_path))) => {
|
Ok(Some(Cmd::Write(maybe_path))) => {
|
||||||
@ -445,11 +408,10 @@ impl<'ws> Workspace<'ws> {
|
|||||||
Ok(Some(Cmd::RenameSheet(idx, name))) => {
|
Ok(Some(Cmd::RenameSheet(idx, name))) => {
|
||||||
match idx {
|
match idx {
|
||||||
Some(idx) => {
|
Some(idx) => {
|
||||||
self.book.set_sheet_name(idx, name)?;
|
self.book.set_sheet_name(idx as u32, name)?;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
self.book
|
self.book.set_sheet_name(self.book.current_sheet, name)?;
|
||||||
.set_sheet_name(self.book.current_sheet as usize, name)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@ -462,65 +424,61 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.book.select_sheet_by_name(name);
|
self.book.select_sheet_by_name(name);
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Ok(Some(Cmd::Quit)) => {
|
Ok(Some(Cmd::Quit)) => Ok(Some(ExitCode::SUCCESS)),
|
||||||
Ok(Some(ExitCode::SUCCESS))
|
Ok(Some(Cmd::ColorRows(count, color))) => {
|
||||||
}
|
let row_count = count.unwrap_or(1);
|
||||||
Ok(Some(Cmd::ColorRows(_count, color))) => {
|
|
||||||
let row_count = _count.unwrap_or(1);
|
|
||||||
let row = self.book.location.row;
|
let row = self.book.location.row;
|
||||||
for r in row..(row+row_count) {
|
for r in row..(row + row_count) {
|
||||||
let mut style = if let Some(style) = self.book.get_row_style(self.book.current_sheet, r)? {
|
self.book.set_row_style(
|
||||||
style
|
&[("fill.bg_color", &color)],
|
||||||
} else {
|
self.book.current_sheet,
|
||||||
self.book.create_style()
|
r,
|
||||||
};
|
)?;
|
||||||
style.fill.bg_color = Some(color.to_string());
|
|
||||||
self.book.set_row_style(&style, self.book.current_sheet, r)?;
|
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Ok(Some(Cmd::ColorColumns(_count, color))) => {
|
Ok(Some(Cmd::ColorColumns(count, color))) => {
|
||||||
let col_count = _count.unwrap_or(1);
|
let col_count = count.unwrap_or(1);
|
||||||
let col = self.book.location.col;
|
let col = self.book.location.col;
|
||||||
for c in col..(col+col_count) {
|
for c in col..(col + col_count) {
|
||||||
let mut style = if let Some(style) = self.book.get_column_style(self.book.current_sheet, c)? {
|
self.book.set_col_style(
|
||||||
style
|
&[("fill.bg_color", &color)],
|
||||||
} else {
|
self.book.current_sheet,
|
||||||
self.book.create_style()
|
c,
|
||||||
};
|
)?;
|
||||||
style.fill.bg_color = Some(color.to_string());
|
|
||||||
self.book.set_col_style(&style, self.book.current_sheet, c)?;
|
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Ok(Some(Cmd::ColorCell(color))) => {
|
Ok(Some(Cmd::ColorCell(color))) => {
|
||||||
if let Some((start, end)) = self.state.range_select.get_range() {
|
let sheet = self.book.current_sheet;
|
||||||
for ri in start.row..=end.row {
|
let area = if let Some((start, end)) = self.state.range_select.get_range() {
|
||||||
for ci in start.col..=end.col {
|
Area {
|
||||||
let address = Address { row: ri, col: ci };
|
sheet,
|
||||||
let sheet = self.book.current_sheet;
|
row: start.row as i32,
|
||||||
let mut style = self.book.get_cell_style(sheet, &address)
|
column: start.col as i32,
|
||||||
.expect("I think this should be impossible.").clone();
|
width: (end.col - start.col + 1) as i32,
|
||||||
style.fill.bg_color = Some(color.to_string());
|
height: (end.row - start.row + 1) as i32,
|
||||||
self.book.set_cell_style(&style, sheet, &address)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let address = self.book.location.clone();
|
let address = self.book.location.clone();
|
||||||
let sheet = self.book.current_sheet;
|
Area {
|
||||||
let mut style = self.book.get_cell_style(sheet, &address)
|
sheet,
|
||||||
.expect("I think this should be impossible.").clone();
|
row: address.row as i32,
|
||||||
style.fill.bg_color = Some(color.to_string());
|
column: address.col as i32,
|
||||||
self.book.set_cell_style(&style, sheet, &address)?;
|
width: 1,
|
||||||
}
|
height: 1,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.book
|
||||||
|
.set_cell_style(&[("fill.bg_color", &color)], &area)?;
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
self.enter_dialog_mode(vec![format!("Unrecognized commmand {}", cmd_text)]);
|
self.enter_dialog_mode(vec![Line::from(format!("Unrecognized commmand {}", cmd_text))]);
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
self.enter_dialog_mode(vec![msg.to_owned()]);
|
self.enter_dialog_mode(vec![Line::from(msg.to_owned())]);
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -550,7 +508,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.handle_numeric_prefix(d);
|
self.handle_numeric_prefix(d);
|
||||||
}
|
}
|
||||||
KeyCode::Char('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.book.clear_cell_range_all(
|
||||||
self.state
|
self.state
|
||||||
.range_select
|
.range_select
|
||||||
@ -622,12 +580,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
})?;
|
})?;
|
||||||
self.state.range_select.sheet = Some(self.book.current_sheet);
|
self.state.range_select.sheet = Some(self.book.current_sheet);
|
||||||
}
|
}
|
||||||
KeyCode::Char('C')
|
KeyCode::Char('C') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
if key
|
|
||||||
.modifiers
|
|
||||||
.contains(KeyModifiers::CONTROL) =>
|
|
||||||
{
|
|
||||||
// TODO(zaphar): Share the algorithm below between both copies
|
|
||||||
self.copy_range(true)?;
|
self.copy_range(true)?;
|
||||||
self.exit_range_select_mode()?;
|
self.exit_range_select_mode()?;
|
||||||
}
|
}
|
||||||
@ -644,7 +597,10 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.exit_range_select_mode()?;
|
self.exit_range_select_mode()?;
|
||||||
}
|
}
|
||||||
KeyCode::Char('x') => {
|
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.book.extend_to(from, to)?;
|
||||||
}
|
}
|
||||||
self.exit_range_select_mode()?;
|
self.exit_range_select_mode()?;
|
||||||
@ -663,20 +619,15 @@ impl<'ws> Workspace<'ws> {
|
|||||||
fn copy_range(&mut self, formatted: bool) -> Result<(), anyhow::Error> {
|
fn copy_range(&mut self, formatted: bool) -> Result<(), anyhow::Error> {
|
||||||
self.update_range_selection()?;
|
self.update_range_selection()?;
|
||||||
match &self.state.range_select.get_range() {
|
match &self.state.range_select.get_range() {
|
||||||
Some((
|
Some((start, end)) => {
|
||||||
start,
|
|
||||||
end,
|
|
||||||
)) => {
|
|
||||||
let mut rows = Vec::new();
|
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();
|
let mut cols = Vec::new();
|
||||||
for cell in row {
|
for cell in row {
|
||||||
cols.push(if formatted {
|
cols.push(if formatted {
|
||||||
self.book
|
self.book.get_cell_addr_rendered(&cell)?
|
||||||
.get_cell_addr_rendered(&cell)?
|
|
||||||
} else {
|
} else {
|
||||||
self.book
|
self.book.get_cell_addr_contents(&cell)?
|
||||||
.get_cell_addr_contents(&cell)?
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
rows.push(cols);
|
rows.push(cols);
|
||||||
@ -685,11 +636,9 @@ impl<'ws> Workspace<'ws> {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.state.clipboard = Some(ClipboardContents::Cell(if formatted {
|
self.state.clipboard = Some(ClipboardContents::Cell(if formatted {
|
||||||
self.book
|
self.book.get_current_cell_rendered()?
|
||||||
.get_current_cell_rendered()?
|
|
||||||
} else {
|
} else {
|
||||||
self.book
|
self.book.get_current_cell_contents()?
|
||||||
.get_current_cell_contents()?
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -720,6 +669,16 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.state.reset_n_prefix();
|
self.state.reset_n_prefix();
|
||||||
self.state.char_queue.clear();
|
self.state.char_queue.clear();
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('B') => {
|
||||||
|
let address = self.book.location.clone();
|
||||||
|
let style = self.book.get_cell_style(self.book.current_sheet, &address).map(|s| s.font.b);
|
||||||
|
self.toggle_bool_style(style, "font.b", &address)?;
|
||||||
|
}
|
||||||
|
KeyCode::Char('I') => {
|
||||||
|
let address = self.book.location.clone();
|
||||||
|
let style = self.book.get_cell_style(self.book.current_sheet, &address).map(|s| s.font.i);
|
||||||
|
self.toggle_bool_style(style, "font.i", &address)?;
|
||||||
|
}
|
||||||
KeyCode::Char(d) if d.is_ascii_digit() => {
|
KeyCode::Char(d) if d.is_ascii_digit() => {
|
||||||
self.handle_numeric_prefix(d);
|
self.handle_numeric_prefix(d);
|
||||||
}
|
}
|
||||||
@ -755,11 +714,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.book.get_current_cell_rendered()?,
|
self.book.get_current_cell_rendered()?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
KeyCode::Char('C')
|
KeyCode::Char('C') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
if key
|
|
||||||
.modifiers
|
|
||||||
.contains(KeyModifiers::CONTROL) =>
|
|
||||||
{
|
|
||||||
self.state.clipboard = Some(ClipboardContents::Cell(
|
self.state.clipboard = Some(ClipboardContents::Cell(
|
||||||
self.book.get_current_cell_rendered()?,
|
self.book.get_current_cell_rendered()?,
|
||||||
));
|
));
|
||||||
@ -873,7 +828,13 @@ impl<'ws> Workspace<'ws> {
|
|||||||
}
|
}
|
||||||
KeyCode::Char('g') => {
|
KeyCode::Char('g') => {
|
||||||
// TODO(zaphar): This really needs a better state machine.
|
// 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.state.char_queue.pop();
|
||||||
self.move_to_top()?;
|
self.move_to_top()?;
|
||||||
} else {
|
} else {
|
||||||
@ -889,6 +850,24 @@ impl<'ws> Workspace<'ws> {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn toggle_bool_style(&mut self, current_val: Option<bool>, path: &str, address: &Address) -> Result<(), anyhow::Error> {
|
||||||
|
let value = if let Some(b_val) = current_val {
|
||||||
|
if b_val { "false" } else { "true" }
|
||||||
|
} else {
|
||||||
|
"true"
|
||||||
|
};
|
||||||
|
self.book.set_cell_style(
|
||||||
|
&[(path, value)],
|
||||||
|
&Area {
|
||||||
|
sheet: self.book.current_sheet,
|
||||||
|
row: address.row as i32,
|
||||||
|
column: address.col as i32,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn paste_range(&mut self) -> Result<(), anyhow::Error> {
|
fn paste_range(&mut self) -> Result<(), anyhow::Error> {
|
||||||
match &self.state.clipboard {
|
match &self.state.clipboard {
|
||||||
Some(ClipboardContents::Cell(contents)) => {
|
Some(ClipboardContents::Cell(contents)) => {
|
||||||
@ -939,8 +918,8 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.state.command_state.focus();
|
self.state.command_state.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enter_dialog_mode(&mut self, msg: Vec<String>) {
|
fn enter_dialog_mode<T: Into<Text<'ws>>>(&mut self, msg: T) {
|
||||||
self.state.popup = msg;
|
self.state.popup = msg.into();
|
||||||
self.state.modality_stack.push(Modality::Dialog);
|
self.state.modality_stack.push(Modality::Dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,14 +8,14 @@ use super::{Address, Book, Viewport, ViewportState};
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_viewport_get_visible_columns() {
|
fn test_viewport_get_visible_columns() {
|
||||||
let mut state = ViewportState::default();
|
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"),
|
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 default_size = book.get_col_size(1).expect("Failed to get column size");
|
||||||
let width = dbg!(dbg!(default_size) * 12 / 2);
|
let width = dbg!(dbg!(default_size) * 12 / 2);
|
||||||
let app_state = AppState::default();
|
let app_state = AppState::default();
|
||||||
let viewport =
|
let viewport = Viewport::new(&book, Some(&app_state.range_select))
|
||||||
Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { row: 1, col: 17 });
|
.with_selected(Address { row: 1, col: 17 });
|
||||||
let cols = viewport
|
let cols = viewport
|
||||||
.get_visible_columns((width + 5) as u16, &mut state)
|
.get_visible_columns((width + 5) as u16, &mut state)
|
||||||
.expect("Failed to get visible columns");
|
.expect("Failed to get visible columns");
|
||||||
@ -26,13 +26,13 @@ fn test_viewport_get_visible_columns() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_viewport_get_visible_rows() {
|
fn test_viewport_get_visible_rows() {
|
||||||
let mut state = dbg!(ViewportState::default());
|
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"),
|
Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"),
|
||||||
);
|
);
|
||||||
let height = 6;
|
let height = 6;
|
||||||
let app_state = AppState::default();
|
let app_state = AppState::default();
|
||||||
let viewport =
|
let viewport = Viewport::new(&book, Some(&app_state.range_select))
|
||||||
Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { row: 17, col: 1 });
|
.with_selected(Address { row: 17, col: 1 });
|
||||||
let rows = dbg!(viewport.get_visible_rows(height as u16, &mut state));
|
let rows = dbg!(viewport.get_visible_rows(height as u16, &mut state));
|
||||||
assert_eq!(height - 1, rows.len());
|
assert_eq!(height - 1, rows.len());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -45,7 +45,7 @@ fn test_viewport_get_visible_rows() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_viewport_visible_columns_after_length_change() {
|
fn test_viewport_visible_columns_after_length_change() {
|
||||||
let mut state = ViewportState::default();
|
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"),
|
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 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");
|
.expect("Failed to set column size");
|
||||||
{
|
{
|
||||||
let app_state = AppState::default();
|
let app_state = AppState::default();
|
||||||
let viewport =
|
let viewport = Viewport::new(&book, Some(&app_state.range_select))
|
||||||
Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { row: 1, col: 1 });
|
.with_selected(Address { row: 1, col: 1 });
|
||||||
let cols = viewport
|
let cols = viewport
|
||||||
.get_visible_columns((width + 5) as u16, &mut state)
|
.get_visible_columns((width + 5) as u16, &mut state)
|
||||||
.expect("Failed to get visible columns");
|
.expect("Failed to get visible columns");
|
||||||
@ -97,7 +97,9 @@ fn test_color_mapping() {
|
|||||||
("darkgrey", Color::DarkGray),
|
("darkgrey", Color::DarkGray),
|
||||||
("darkgray", Color::DarkGray),
|
("darkgray", Color::DarkGray),
|
||||||
("#35f15b", Color::Rgb(53, 241, 91)),
|
("#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);
|
assert_eq!(super::viewport::map_color(s.as_ref(), Color::Gray), c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,9 @@ use ratatui::{
|
|||||||
widgets::{Block, Cell, Row, StatefulWidget, Table, Widget},
|
widgets::{Block, Cell, Row, StatefulWidget, Table, Widget},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::book;
|
||||||
use super::{Address, Book, RangeSelection};
|
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.
|
/// A visible column to show in our Viewport.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct VisibleColumn {
|
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 start_row = std::cmp::min(self.selected.row, state.prev_corner.row);
|
||||||
let mut start = start_row;
|
let mut start = start_row;
|
||||||
let mut end = 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;
|
let updated_length = length + 1;
|
||||||
if updated_length <= height {
|
if updated_length <= height {
|
||||||
length = updated_length;
|
length = updated_length;
|
||||||
@ -95,7 +90,7 @@ impl<'ws> Viewport<'ws> {
|
|||||||
// We start out with a length of 5 already reserved
|
// We start out with a length of 5 already reserved
|
||||||
let mut length = 5;
|
let mut length = 5;
|
||||||
let start_idx = std::cmp::min(self.selected.col, state.prev_corner.col);
|
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 size = self.book.get_col_size(idx)? as u16;
|
||||||
let updated_length = length + size;
|
let updated_length = length + size;
|
||||||
let col = VisibleColumn { idx, length: size };
|
let col = VisibleColumn { idx, length: size };
|
||||||
@ -198,15 +193,28 @@ impl<'ws> Viewport<'ws> {
|
|||||||
ci: usize,
|
ci: usize,
|
||||||
mut cell: Cell<'widget>,
|
mut cell: Cell<'widget>,
|
||||||
) -> Cell<'widget> {
|
) -> Cell<'widget> {
|
||||||
let style = self
|
// TODO(zaphar): Should probably create somekind of formatter abstraction.
|
||||||
|
if let Some(style) = self
|
||||||
.book
|
.book
|
||||||
.get_cell_style(self.book.current_sheet, &Address { row: ri, col: ci });
|
.get_cell_style(self.book.current_sheet, &Address { row: ri, col: ci }) {
|
||||||
|
cell = self.compute_cell_colors(&style, ri, ci, cell);
|
||||||
|
cell = if style.font.b {
|
||||||
|
cell.bold()
|
||||||
|
} else { cell };
|
||||||
|
cell = if style.font.i {
|
||||||
|
cell.italic()
|
||||||
|
} else { cell };
|
||||||
|
}
|
||||||
|
cell
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_cell_colors<'widget>(&self, style: &ironcalc::base::types::Style, ri: usize, ci: usize, mut cell: Cell<'widget>) -> Cell<'widget> {
|
||||||
let bg_color = map_color(
|
let bg_color = map_color(
|
||||||
style.as_ref().map(|s| s.fill.bg_color.as_ref()).flatten(),
|
style.fill.bg_color.as_ref(),
|
||||||
Color::Rgb(35, 33, 54),
|
Color::Rgb(35, 33, 54),
|
||||||
);
|
);
|
||||||
let fg_color = map_color(
|
let fg_color = map_color(
|
||||||
style.as_ref().map(|s| s.fill.fg_color.as_ref()).flatten(),
|
style.fill.fg_color.as_ref(),
|
||||||
Color::White,
|
Color::White,
|
||||||
);
|
);
|
||||||
if let Some((start, end)) = &self.range_selection.map_or(None, |r| r.get_range()) {
|
if let Some((start, end)) = &self.range_selection.map_or(None, |r| r.get_range()) {
|
||||||
@ -217,12 +225,12 @@ impl<'ws> Viewport<'ws> {
|
|||||||
} else {
|
} else {
|
||||||
cell = cell.bg(bg_color).fg(fg_color);
|
cell = cell.bg(bg_color).fg(fg_color);
|
||||||
}
|
}
|
||||||
match (self.book.location.row == ri, self.book.location.col == ci) {
|
cell = match (self.book.location.row == ri, self.book.location.col == ci) {
|
||||||
(true, true) => cell.fg(Color::White).bg(Color::Rgb(57, 61, 71)),
|
(true, true) => cell.fg(Color::White).bg(Color::Rgb(57, 61, 71)),
|
||||||
// TODO(zaphar): Support ironcalc style options
|
// TODO(zaphar): Support ironcalc style options
|
||||||
_ => cell,
|
_ => cell,
|
||||||
}
|
};
|
||||||
.bold()
|
cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +256,6 @@ pub(crate) fn map_color(color: Option<&String>, otherwise: Color) -> Color {
|
|||||||
candidate => {
|
candidate => {
|
||||||
// TODO(jeremy): Should we support more syntaxes than hex string?
|
// TODO(jeremy): Should we support more syntaxes than hex string?
|
||||||
// hsl(...) ??
|
// hsl(...) ??
|
||||||
// rgb(...) ??
|
|
||||||
if candidate.starts_with("#") {
|
if candidate.starts_with("#") {
|
||||||
if let Ok(rgb) = colorsys::Rgb::from_hex_str(candidate) {
|
if let Ok(rgb) = colorsys::Rgb::from_hex_str(candidate) {
|
||||||
// Note that the colorsys rgb model clamps the f64 values to no more
|
// Note that the colorsys rgb model clamps the f64 values to no more
|
||||||
|
270
src/ui/test.rs
270
src/ui/test.rs
@ -2,6 +2,8 @@ use std::process::ExitCode;
|
|||||||
|
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
use crate::book;
|
||||||
|
use crate::ui::cmd::parse_color;
|
||||||
use crate::ui::{Address, Modality};
|
use crate::ui::{Address, Modality};
|
||||||
|
|
||||||
use super::cmd::{parse, Cmd};
|
use super::cmd::{parse, Cmd};
|
||||||
@ -33,6 +35,10 @@ impl InputScript {
|
|||||||
self.event(construct_key_event(KeyCode::Tab))
|
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 {
|
pub fn modified_char(self, c: char, mods: KeyModifiers) -> Self {
|
||||||
self.event(construct_modified_key_event(KeyCode::Char(c), mods))
|
self.event(construct_modified_key_event(KeyCode::Char(c), mods))
|
||||||
}
|
}
|
||||||
@ -42,10 +48,6 @@ impl InputScript {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enter(self) -> Self {
|
|
||||||
self.event(construct_key_event(KeyCode::Enter))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn esc(self) -> Self {
|
pub fn esc(self) -> Self {
|
||||||
self.event(construct_key_event(KeyCode::Esc))
|
self.event(construct_key_event(KeyCode::Esc))
|
||||||
}
|
}
|
||||||
@ -267,7 +269,7 @@ fn test_cmd_color_rows_with_color() {
|
|||||||
let output = result.unwrap();
|
let output = result.unwrap();
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
let cmd = output.unwrap();
|
let cmd = output.unwrap();
|
||||||
assert_eq!(cmd, Cmd::ColorRows(None, "red"));
|
assert_eq!(cmd, Cmd::ColorRows(None, parse_color("red").unwrap()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -278,7 +280,7 @@ fn test_cmd_color_rows_with_idx_and_color() {
|
|||||||
let output = result.unwrap();
|
let output = result.unwrap();
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
let cmd = output.unwrap();
|
let cmd = output.unwrap();
|
||||||
assert_eq!(cmd, Cmd::ColorRows(Some(1), "red"));
|
assert_eq!(cmd, Cmd::ColorRows(Some(1), parse_color("red").unwrap()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -289,7 +291,7 @@ fn test_cmd_color_columns_with_color() {
|
|||||||
let output = result.unwrap();
|
let output = result.unwrap();
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
let cmd = output.unwrap();
|
let cmd = output.unwrap();
|
||||||
assert_eq!(cmd, Cmd::ColorColumns(None, "red"));
|
assert_eq!(cmd, Cmd::ColorColumns(None, parse_color("red").unwrap()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -300,10 +302,9 @@ fn test_cmd_color_columns_with_idx_and_color() {
|
|||||||
let output = result.unwrap();
|
let output = result.unwrap();
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
let cmd = output.unwrap();
|
let cmd = output.unwrap();
|
||||||
assert_eq!(cmd, Cmd::ColorColumns(Some(1), "red"));
|
assert_eq!(cmd, Cmd::ColorColumns(Some(1), parse_color("red").unwrap()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_input_navitation_enter_key() {
|
fn test_input_navitation_enter_key() {
|
||||||
let mut ws = new_workspace();
|
let mut ws = new_workspace();
|
||||||
@ -319,7 +320,7 @@ fn test_input_navitation_enter_key() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_input_navitation_tab_key() {
|
fn test_input_navitation_tab_key() {
|
||||||
let mut ws = new_workspace();
|
let mut ws = new_workspace();
|
||||||
let col = dbg!(ws.book.location.col);
|
let col = ws.book.location.col;
|
||||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||||
script()
|
script()
|
||||||
.tab()
|
.tab()
|
||||||
@ -351,7 +352,7 @@ fn test_input_navitation_shift_enter_key() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_input_navitation_shift_tab_key() {
|
fn test_input_navitation_shift_tab_key() {
|
||||||
let mut ws = new_workspace();
|
let mut ws = new_workspace();
|
||||||
let col = dbg!(ws.book.location.col);
|
let col = ws.book.location.col;
|
||||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||||
script()
|
script()
|
||||||
.tab()
|
.tab()
|
||||||
@ -930,7 +931,6 @@ fn test_edit_mode_paste() {
|
|||||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||||
ws.state.range_select.start = Some(Address { row: 1, col: 1 });
|
ws.state.range_select.start = Some(Address { row: 1, col: 1 });
|
||||||
ws.state.range_select.end = Some(Address { row: 2, col: 2 });
|
ws.state.range_select.end = Some(Address { row: 2, col: 2 });
|
||||||
dbg!(ws.selected_range_to_string());
|
|
||||||
script()
|
script()
|
||||||
.char('e')
|
.char('e')
|
||||||
.ctrl('p')
|
.ctrl('p')
|
||||||
@ -1003,8 +1003,7 @@ macro_rules! assert_range_clear {
|
|||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("Failed to handle script");
|
.expect("Failed to handle script");
|
||||||
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
|
||||||
$script.run(&mut ws)
|
$script.run(&mut ws).expect("Failed to handle script");
|
||||||
.expect("Failed to handle script");
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"".to_string(),
|
"".to_string(),
|
||||||
ws.book
|
ws.book
|
||||||
@ -1022,18 +1021,21 @@ macro_rules! assert_range_clear {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_range_select_clear_upper_d() {
|
fn test_range_select_clear_upper_d() {
|
||||||
assert_range_clear!(script()
|
assert_range_clear!(script().char('j').char('l').char('D'));
|
||||||
.char('j')
|
|
||||||
.char('l')
|
|
||||||
.char('D'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_range_select_movement() {
|
fn test_range_select_movement() {
|
||||||
let mut ws = new_workspace();
|
let mut ws = new_workspace();
|
||||||
ws.book.new_sheet(Some("s2")).expect("Unable create s2 sheet");
|
ws.book
|
||||||
ws.book.new_sheet(Some("s3")).expect("Unable create s3 sheet");
|
.new_sheet(Some("s2"))
|
||||||
script().ctrl('r').run(&mut ws)
|
.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");
|
.expect("failed to run script");
|
||||||
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
|
||||||
script()
|
script()
|
||||||
@ -1063,10 +1065,7 @@ fn test_range_select_movement() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_range_select_clear_lower_d() {
|
fn test_range_select_clear_lower_d() {
|
||||||
assert_range_clear!(script()
|
assert_range_clear!(script().char('j').char('l').char('d'));
|
||||||
.char('j')
|
|
||||||
.char('l')
|
|
||||||
.char('d'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! assert_range_copy {
|
macro_rules! assert_range_copy {
|
||||||
@ -1074,8 +1073,12 @@ macro_rules! assert_range_copy {
|
|||||||
let mut ws = new_workspace();
|
let mut ws = new_workspace();
|
||||||
let top_left_addr = Address { row: 2, col: 2 };
|
let top_left_addr = Address { row: 2, col: 2 };
|
||||||
let bot_right_addr = Address { row: 4, col: 4 };
|
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
|
||||||
ws.book.update_cell(&bot_right_addr, "bot_right").expect("Failed to update top left");
|
.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());
|
assert!(ws.state.clipboard.is_none());
|
||||||
script()
|
script()
|
||||||
.ctrl('r')
|
.ctrl('r')
|
||||||
@ -1084,7 +1087,14 @@ macro_rules! assert_range_copy {
|
|||||||
.char(' ')
|
.char(' ')
|
||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("failed to run script");
|
.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()
|
script()
|
||||||
.char('2')
|
.char('2')
|
||||||
.char('j')
|
.char('j')
|
||||||
@ -1092,27 +1102,52 @@ macro_rules! assert_range_copy {
|
|||||||
.char('l')
|
.char('l')
|
||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("failed to run script");
|
.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!(
|
||||||
assert_eq!(&Address { row: 1, col: 1}, ws.state.range_select.original_location
|
&bot_right_addr,
|
||||||
.as_ref().expect("Expected an original location"));
|
ws.state
|
||||||
assert_eq!(0, ws.state.range_select.original_sheet.
|
.range_select
|
||||||
expect("Expected an original sheet"));
|
.end
|
||||||
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.iter().last());
|
.as_ref()
|
||||||
dbg!(ws.state.range_select.get_range());
|
.expect("Didn't find a start of range")
|
||||||
$script.run(&mut ws)
|
);
|
||||||
.expect("failed to run script");
|
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()
|
||||||
|
);
|
||||||
|
$script.run(&mut ws).expect("failed to run script");
|
||||||
assert!(ws.state.clipboard.is_some());
|
assert!(ws.state.clipboard.is_some());
|
||||||
match ws.state.clipboard.unwrap() {
|
match ws.state.clipboard.unwrap() {
|
||||||
crate::ui::ClipboardContents::Cell(_) => assert!(false, "Not rows in Clipboard"),
|
crate::ui::ClipboardContents::Cell(_) => assert!(false, "Not rows in Clipboard"),
|
||||||
crate::ui::ClipboardContents::Range(rows) => {
|
crate::ui::ClipboardContents::Range(rows) => {
|
||||||
assert_eq!(vec![
|
assert_eq!(
|
||||||
vec!["top_left".to_string(), "".to_string(), "".to_string()],
|
vec![
|
||||||
vec!["".to_string(), "".to_string(), "".to_string()],
|
vec!["top_left".to_string(), "".to_string(), "".to_string()],
|
||||||
vec!["".to_string(), "".to_string(), "bot_right".to_string()],
|
vec!["".to_string(), "".to_string(), "".to_string()],
|
||||||
], rows);
|
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 +1174,9 @@ fn test_range_select_copy_capital_c() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_extend_to_range() {
|
fn test_extend_to_range() {
|
||||||
let mut ws = new_workspace();
|
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();
|
ws.book.evaluate();
|
||||||
script()
|
script()
|
||||||
.char('v')
|
.char('v')
|
||||||
@ -1147,11 +1184,152 @@ fn test_extend_to_range() {
|
|||||||
.char('x')
|
.char('x')
|
||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("Unable to run script");
|
.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");
|
.expect("Failed to get cell contents");
|
||||||
assert_eq!("=B2+1".to_string(), extended_cell);
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bold_text() {
|
||||||
|
let mut ws = new_workspace();
|
||||||
|
let before_style = ws
|
||||||
|
.book
|
||||||
|
.get_cell_style(0, &Address { row: 1, col: 1 })
|
||||||
|
.expect("Failed to get style");
|
||||||
|
assert!(!before_style.font.b);
|
||||||
|
script()
|
||||||
|
.char('B')
|
||||||
|
.run(&mut ws)
|
||||||
|
.expect("Unable to run script");
|
||||||
|
let style = ws
|
||||||
|
.book
|
||||||
|
.get_cell_style(0, &Address { row: 1, col: 1 })
|
||||||
|
.expect("Failed to get style");
|
||||||
|
assert!(style.font.b);
|
||||||
|
script()
|
||||||
|
.char('B')
|
||||||
|
.run(&mut ws)
|
||||||
|
.expect("Unable to run script");
|
||||||
|
assert!(!before_style.font.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_italic_text() {
|
||||||
|
let mut ws = new_workspace();
|
||||||
|
let before_style = ws
|
||||||
|
.book
|
||||||
|
.get_cell_style(0, &Address { row: 1, col: 1 })
|
||||||
|
.expect("Failed to get style");
|
||||||
|
assert!(!before_style.font.i);
|
||||||
|
script()
|
||||||
|
.char('I')
|
||||||
|
.run(&mut ws)
|
||||||
|
.expect("Unable to run script");
|
||||||
|
let style = ws
|
||||||
|
.book
|
||||||
|
.get_cell_style(0, &Address { row: 1, col: 1 })
|
||||||
|
.expect("Failed to get style");
|
||||||
|
assert!(style.font.i);
|
||||||
|
script()
|
||||||
|
.char('I')
|
||||||
|
.run(&mut ws)
|
||||||
|
.expect("Unable to run script");
|
||||||
|
assert!(!before_style.font.i);
|
||||||
|
}
|
||||||
|
|
||||||
fn new_workspace<'a>() -> Workspace<'a> {
|
fn new_workspace<'a>() -> Workspace<'a> {
|
||||||
Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook")
|
Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook")
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user