Compare commits

...

25 Commits

Author SHA1 Message Date
2aa9fd69b1 wip: update docs for bold and italic 2025-02-27 18:49:32 -05:00
621c35d7c4 wip: different keybinds for bold and italic 2025-02-27 18:45:58 -05:00
4623a911f4 wip: styling: resusable toggle func 2025-02-26 20:16:17 -05:00
ba5ea3c627 wip: styling: toggle bold and italic 2025-02-26 20:04:06 -05:00
dae3d71c54 wip: styling: use ctrl-i or ctrl-b 2025-02-26 18:21:48 -05:00
8da0ebda4e chore: cleanup: formatting and unnecessary file 2025-02-26 18:14:09 -05:00
db0397b8f4 chore: update example sheet 2025-02-26 18:13:20 -05:00
c3f84b10ad wip: styling: bold text 2025-02-26 18:13:15 -05:00
b5e0362a4e
Merge pull request #26 from zaphar/help
Help documentation improvements
2025-02-18 20:03:12 -05:00
5ebdc6e70c wip: divvy up the help docs 2025-02-18 19:57:11 -05:00
e09b0f90e9 wip: bring in tui-markdown 2025-02-17 17:44:32 -05:00
f392cb743f wip: Use the UserModel
Closes #25
2025-02-15 18:27:59 -05:00
49c768dd76 fix: nix build naersk no like the rev syntax 2025-02-15 09:43:29 -05:00
d456501565 update: ironcalc version for styling api fixes 2025-02-15 09:11:16 -05:00
5d23410f00 wip: calcuate_area shouldnt 2025-02-14 19:36:54 -05:00
7ffd420029 docs: style path documentation 2025-02-13 19:42:43 -05:00
3219e01176 wip: support column and row styling 2025-02-10 19:38:00 -05:00
f6c9e95fda chore: formatting pass 2025-02-08 18:33:51 -05:00
e7169dcb44 wip: convert named colors to hex strings and fix off by one 2025-02-08 18:14:45 -05:00
e798350cd2 wip: fix area calculation bug 2025-02-07 19:30:07 -05:00
8dd6f6d614 wip: cleanup, todos, unused code, formatting 2025-02-07 19:22:42 -05:00
43f07f58bc wip: sheet renaming and col sizing with UserModel 2025-02-07 17:55:30 -05:00
d8b3191612 wip: new_sheet and cell styling with UserModel 2025-02-07 17:34:19 -05:00
7a5bd63fde wip: Convert over to UserModel
Step 1: update ironcalc version and plug it in
2025-02-07 17:32:36 -05:00
0a6807493c
Merge pull request #24 from zaphar/cell_styling
Cell styling
2025-01-28 21:13:14 -05:00
16 changed files with 1086 additions and 583 deletions

372
Cargo.lock generated
View File

@ -28,15 +28,6 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
dependencies = [
"memchr",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@ -67,6 +58,19 @@ dependencies = [
"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]]
name = "anstream"
version = "0.6.17"
@ -152,12 +156,27 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bitcode"
version = "0.6.3"
@ -293,9 +312,9 @@ dependencies = [
[[package]]
name = "chrono-tz"
version = "0.9.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f"
dependencies = [
"chrono",
"chrono-tz-build",
@ -304,12 +323,11 @@ dependencies = [
[[package]]
name = "chrono-tz-build"
version = "0.3.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7"
dependencies = [
"parse-zoneinfo",
"phf",
"phf_codegen",
]
@ -483,19 +501,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "csv-sniffer"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b8e952164bb270a505d6cb6136624174c34cfb9abd16e0011f5e53058317f39"
dependencies = [
"bitflags 1.3.2",
"csv",
"csv-core",
"memchr",
"regex 0.2.11",
]
[[package]]
name = "csvx"
version = "0.1.17"
@ -573,6 +578,12 @@ dependencies = [
"syn",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "digest"
version = "0.10.7"
@ -742,6 +753,15 @@ dependencies = [
"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]]
name = "getrandom"
version = "0.2.15"
@ -869,8 +889,8 @@ dependencies = [
[[package]]
name = "ironcalc"
version = "0.2.0"
source = "git+https://github.com/ironcalc/IronCalc#98dc557a017b2ad640fb46eece17afda14177e59"
version = "0.3.0"
source = "git+https://github.com/ironcalc/IronCalc#b2c5027f56a16a0c606b01a071b816b941972aef"
dependencies = [
"bitcode",
"chrono",
@ -885,18 +905,17 @@ dependencies = [
[[package]]
name = "ironcalc_base"
version = "0.2.0"
source = "git+https://github.com/ironcalc/IronCalc#98dc557a017b2ad640fb46eece17afda14177e59"
version = "0.3.0"
source = "git+https://github.com/ironcalc/IronCalc#b2c5027f56a16a0c606b01a071b816b941972aef"
dependencies = [
"bitcode",
"chrono",
"chrono-tz",
"csv",
"csv-sniffer",
"js-sys",
"once_cell",
"rand",
"regex 1.11.1",
"regex",
"ryu",
"serde",
]
@ -949,18 +968,18 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
@ -1004,6 +1023,12 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.0"
@ -1026,6 +1051,16 @@ dependencies = [
"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]]
name = "num-conv"
version = "0.1.0"
@ -1056,6 +1091,28 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "parking_lot"
version = "0.12.3"
@ -1085,7 +1142,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
dependencies = [
"regex 1.11.1",
"regex",
]
[[package]]
@ -1173,6 +1230,19 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "powerfmt"
version = "0.2.0"
@ -1188,6 +1258,16 @@ dependencies = [
"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]]
name = "proc-macro-crate"
version = "3.2.0"
@ -1206,6 +1286,34 @@ dependencies = [
"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]]
name = "quote"
version = "1.0.37"
@ -1284,29 +1392,16 @@ dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "regex"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384"
dependencies = [
"aho-corasick 0.6.10",
"memchr",
"regex-syntax 0.5.6",
"thread_local",
"utf8-ranges",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick 1.1.3",
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax 0.8.5",
"regex-syntax",
]
[[package]]
@ -1315,18 +1410,9 @@ version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick 1.1.3",
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-syntax"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7"
dependencies = [
"ucd-util",
"regex-syntax",
]
[[package]]
@ -1370,7 +1456,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"regex 1.11.1",
"regex",
"relative-path",
"rustc_version",
"syn",
@ -1417,6 +1503,15 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "scopeguard"
version = "1.2.0"
@ -1498,6 +1593,7 @@ dependencies = [
"serde_json",
"slice-utils",
"thiserror",
"tui-markdown",
"tui-popup",
"tui-prompts",
"tui-textarea",
@ -1539,6 +1635,12 @@ dependencies = [
"libc",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "0.3.11"
@ -1616,6 +1718,28 @@ dependencies = [
"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]]
name = "thiserror"
version = "1.0.65"
@ -1636,15 +1760,6 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
dependencies = [
"lazy_static",
]
[[package]]
name = "time"
version = "0.3.36"
@ -1652,10 +1767,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
@ -1664,6 +1781,16 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "toml_datetime"
version = "0.6.8"
@ -1681,6 +1808,53 @@ dependencies = [
"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]]
name = "tui-popup"
version = "0.6.0"
@ -1723,10 +1897,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-util"
version = "0.1.10"
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
@ -1763,12 +1937,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "utf8-ranges"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -1781,6 +1949,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "wasi"
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"
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]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@ -1964,6 +2151,21 @@ dependencies = [
"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]]
name = "zerocopy"
version = "0.7.35"

View File

@ -10,13 +10,15 @@ anyhow = { version = "1.0.91", features = ["backtrace"] }
clap = { version = "4.5.20", features = ["derive"] }
crossterm = { version = "0.28.1", features = ["event-stream", "serde"] }
csvx = "0.1.17"
# this revision introduces a way to get the Model back out of the UserModel
ironcalc = { git = "https://github.com/ironcalc/IronCalc" }
futures = "0.3.31"
ratatui = "0.29.0"
thiserror = "1.0.65"
tui-textarea = "0.7.0"
tui-prompts = "0.5.0"
slice-utils = { git = "https://dev.zaphar.net/zaphar/slice-cursor-rs.git", ref = "main" }
slice-utils = { git = "https://dev.zaphar.net/zaphar/slice-cursor-rs.git" }
tui-popup = "0.6.0"
serde_json = "1.0.133"
colorsys = "0.6.7"
tui-markdown = { version = "0.3.1", features = [] }

20
docs/command.md Normal file
View 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
View 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)

View File

@ -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
get some context sensitive help.
### Navigation Mode
### Modal Docs
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.
* `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>
* [Navigation](./navigation.md)
* [Edit](./edit.md)
* [Visual](./visual.md)
* [Command](./command.md)

23
docs/intro.md Normal file
View 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
View 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
View 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.

View File

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

View File

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

12
src/ui/help/mod.rs Normal file
View 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")),
}
}

View File

@ -1,20 +1,18 @@
//! Ui rendering logic
use std::{path::PathBuf, process::ExitCode};
use crate::book::{AddressRange, Book};
use crate::book::{self, AddressRange, Book};
use anyhow::{anyhow, Result};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use ironcalc::base::Model;
use ironcalc::base::{expressions::types::Area, Model};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Flex, Layout},
style::{Modifier, Style},
widgets::Block,
buffer::Buffer, layout::{Constraint, Flex, Layout}, style::{Modifier, Style}, text::{Line, Text}, widgets::Block
};
use tui_prompts::{State, Status, TextPrompt, TextState};
use tui_textarea::{CursorMove, TextArea};
mod help;
mod cmd;
pub mod render;
#[cfg(test)]
@ -81,7 +79,7 @@ pub struct AppState<'ws> {
pub char_queue: Vec<char>,
pub range_select: RangeSelection,
dirty: bool,
popup: Vec<String>,
popup: Text<'ws>,
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.
#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
pub struct Address {
@ -187,7 +185,7 @@ impl<'ws> Workspace<'ws> {
pub fn new_empty(locale: &str, tz: &str) -> Result<Self> {
Ok(Self::new(
Book::new(Model::new_empty("", locale, tz).map_err(|e| anyhow!("{}", e))?),
Book::from_model(Model::new_empty("", locale, tz).map_err(|e| anyhow!("{}", e))?),
PathBuf::default(),
))
}
@ -235,7 +233,7 @@ impl<'ws> Workspace<'ws> {
/// Move a row down in the current sheet.
pub fn move_down(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
if loc.row < render::viewport::LAST_ROW {
if loc.row < (book::LAST_ROW as usize) {
loc.row += 1;
self.book.move_to(&loc)?;
}
@ -244,10 +242,13 @@ impl<'ws> Workspace<'ws> {
/// Move to the top row without changing columns
pub fn move_to_top(&mut self) -> Result<()> {
self.book.move_to(&Address { row: 1, col: self.book.location.col })?;
self.book.move_to(&Address {
row: 1,
col: self.book.location.col,
})?;
Ok(())
}
/// Move a row up in the current sheet.
pub fn move_up(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
@ -271,7 +272,7 @@ impl<'ws> Workspace<'ws> {
/// Move a column to the left in the current sheet.
pub fn move_right(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
if loc.col < render::viewport::LAST_COLUMN {
if loc.col < (book::LAST_COLUMN as usize) {
loc.col += 1;
self.book.move_to(&loc)?;
}
@ -293,54 +294,16 @@ impl<'ws> Workspace<'ws> {
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.
// Ideally we would also render the markdown content properly.
// https://github.com/zaphar/sheetsui/issues/22
match self.state.modality() {
Modality::Navigate => vec![
"Navigate Mode:".to_string(),
"* e,i: Enter edit mode for current cell".to_string(),
"* ENTER/RETURN: Go down one cell".to_string(),
"* TAB: Go over one cell".to_string(),
"* 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()],
Modality::Navigate => help::render_topic("navigate"),
Modality::CellEdit => help::render_topic("edit"),
Modality::Command => help::render_topic("command"),
Modality::RangeSelect => help::render_topic("visual"),
_ => help::render_topic(""),
}
}
@ -420,8 +383,8 @@ impl<'ws> Workspace<'ws> {
self.load_into(path)?;
Ok(None)
}
Ok(Some(Cmd::Help(_maybe_topic))) => {
self.enter_dialog_mode(vec!["TODO help topic".to_owned()]);
Ok(Some(Cmd::Help(maybe_topic))) => {
self.enter_dialog_mode(help::render_topic(maybe_topic.unwrap_or("")));
Ok(None)
}
Ok(Some(Cmd::Write(maybe_path))) => {
@ -445,11 +408,10 @@ impl<'ws> Workspace<'ws> {
Ok(Some(Cmd::RenameSheet(idx, name))) => {
match idx {
Some(idx) => {
self.book.set_sheet_name(idx, name)?;
self.book.set_sheet_name(idx as u32, name)?;
}
_ => {
self.book
.set_sheet_name(self.book.current_sheet as usize, name)?;
self.book.set_sheet_name(self.book.current_sheet, name)?;
}
}
Ok(None)
@ -462,65 +424,61 @@ impl<'ws> Workspace<'ws> {
self.book.select_sheet_by_name(name);
Ok(None)
}
Ok(Some(Cmd::Quit)) => {
Ok(Some(ExitCode::SUCCESS))
}
Ok(Some(Cmd::ColorRows(_count, color))) => {
let row_count = _count.unwrap_or(1);
Ok(Some(Cmd::Quit)) => Ok(Some(ExitCode::SUCCESS)),
Ok(Some(Cmd::ColorRows(count, color))) => {
let row_count = count.unwrap_or(1);
let row = self.book.location.row;
for r in row..(row+row_count) {
let mut style = if let Some(style) = self.book.get_row_style(self.book.current_sheet, r)? {
style
} else {
self.book.create_style()
};
style.fill.bg_color = Some(color.to_string());
self.book.set_row_style(&style, self.book.current_sheet, r)?;
for r in row..(row + row_count) {
self.book.set_row_style(
&[("fill.bg_color", &color)],
self.book.current_sheet,
r,
)?;
}
Ok(None)
}
Ok(Some(Cmd::ColorColumns(_count, color))) => {
let col_count = _count.unwrap_or(1);
Ok(Some(Cmd::ColorColumns(count, color))) => {
let col_count = count.unwrap_or(1);
let col = self.book.location.col;
for c in col..(col+col_count) {
let mut style = if let Some(style) = self.book.get_column_style(self.book.current_sheet, c)? {
style
} else {
self.book.create_style()
};
style.fill.bg_color = Some(color.to_string());
self.book.set_col_style(&style, self.book.current_sheet, c)?;
for c in col..(col + col_count) {
self.book.set_col_style(
&[("fill.bg_color", &color)],
self.book.current_sheet,
c,
)?;
}
Ok(None)
}
Ok(Some(Cmd::ColorCell(color))) => {
if let Some((start, end)) = self.state.range_select.get_range() {
for ri in start.row..=end.row {
for ci in start.col..=end.col {
let address = Address { row: ri, col: ci };
let sheet = self.book.current_sheet;
let mut style = self.book.get_cell_style(sheet, &address)
.expect("I think this should be impossible.").clone();
style.fill.bg_color = Some(color.to_string());
self.book.set_cell_style(&style, sheet, &address)?;
}
let sheet = self.book.current_sheet;
let area = if let Some((start, end)) = self.state.range_select.get_range() {
Area {
sheet,
row: start.row as i32,
column: start.col as i32,
width: (end.col - start.col + 1) as i32,
height: (end.row - start.row + 1) as i32,
}
} else {
let address = self.book.location.clone();
let sheet = self.book.current_sheet;
let mut style = self.book.get_cell_style(sheet, &address)
.expect("I think this should be impossible.").clone();
style.fill.bg_color = Some(color.to_string());
self.book.set_cell_style(&style, sheet, &address)?;
}
Area {
sheet,
row: address.row as i32,
column: address.col as i32,
width: 1,
height: 1,
}
};
self.book
.set_cell_style(&[("fill.bg_color", &color)], &area)?;
Ok(None)
}
Ok(None) => {
self.enter_dialog_mode(vec![format!("Unrecognized commmand {}", cmd_text)]);
self.enter_dialog_mode(vec![Line::from(format!("Unrecognized commmand {}", cmd_text))]);
Ok(None)
}
Err(msg) => {
self.enter_dialog_mode(vec![msg.to_owned()]);
self.enter_dialog_mode(vec![Line::from(msg.to_owned())]);
Ok(None)
}
}
@ -550,7 +508,7 @@ impl<'ws> Workspace<'ws> {
self.handle_numeric_prefix(d);
}
KeyCode::Char('D') => {
if let Some((start, end)) = self.state.range_select.get_range() {
if let Some((start, end)) = dbg!(self.state.range_select.get_range()) {
self.book.clear_cell_range_all(
self.state
.range_select
@ -622,12 +580,7 @@ impl<'ws> Workspace<'ws> {
})?;
self.state.range_select.sheet = Some(self.book.current_sheet);
}
KeyCode::Char('C')
if key
.modifiers
.contains(KeyModifiers::CONTROL) =>
{
// TODO(zaphar): Share the algorithm below between both copies
KeyCode::Char('C') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.copy_range(true)?;
self.exit_range_select_mode()?;
}
@ -644,7 +597,10 @@ impl<'ws> Workspace<'ws> {
self.exit_range_select_mode()?;
}
KeyCode::Char('x') => {
if let (Some(from), Some(to)) = (self.state.range_select.start.as_ref(), self.state.range_select.end.as_ref()) {
if let (Some(from), Some(to)) = (
self.state.range_select.start.as_ref(),
self.state.range_select.end.as_ref(),
) {
self.book.extend_to(from, to)?;
}
self.exit_range_select_mode()?;
@ -663,20 +619,15 @@ impl<'ws> Workspace<'ws> {
fn copy_range(&mut self, formatted: bool) -> Result<(), anyhow::Error> {
self.update_range_selection()?;
match &self.state.range_select.get_range() {
Some((
start,
end,
)) => {
Some((start, end)) => {
let mut rows = Vec::new();
for row in (AddressRange { start, end, }).as_rows() {
for row in (AddressRange { start, end }).as_rows() {
let mut cols = Vec::new();
for cell in row {
cols.push(if formatted {
self.book
.get_cell_addr_rendered(&cell)?
self.book.get_cell_addr_rendered(&cell)?
} else {
self.book
.get_cell_addr_contents(&cell)?
self.book.get_cell_addr_contents(&cell)?
});
}
rows.push(cols);
@ -685,11 +636,9 @@ impl<'ws> Workspace<'ws> {
}
None => {
self.state.clipboard = Some(ClipboardContents::Cell(if formatted {
self.book
.get_current_cell_rendered()?
self.book.get_current_cell_rendered()?
} else {
self.book
.get_current_cell_contents()?
self.book.get_current_cell_contents()?
}));
}
}
@ -720,6 +669,16 @@ impl<'ws> Workspace<'ws> {
self.state.reset_n_prefix();
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() => {
self.handle_numeric_prefix(d);
}
@ -755,11 +714,7 @@ impl<'ws> Workspace<'ws> {
self.book.get_current_cell_rendered()?,
));
}
KeyCode::Char('C')
if key
.modifiers
.contains(KeyModifiers::CONTROL) =>
{
KeyCode::Char('C') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.state.clipboard = Some(ClipboardContents::Cell(
self.book.get_current_cell_rendered()?,
));
@ -873,7 +828,13 @@ impl<'ws> Workspace<'ws> {
}
KeyCode::Char('g') => {
// TODO(zaphar): This really needs a better state machine.
if self.state.char_queue.first().map(|c| *c == 'g').unwrap_or(false) {
if self
.state
.char_queue
.first()
.map(|c| *c == 'g')
.unwrap_or(false)
{
self.state.char_queue.pop();
self.move_to_top()?;
} else {
@ -889,6 +850,24 @@ impl<'ws> Workspace<'ws> {
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> {
match &self.state.clipboard {
Some(ClipboardContents::Cell(contents)) => {
@ -939,8 +918,8 @@ impl<'ws> Workspace<'ws> {
self.state.command_state.focus();
}
fn enter_dialog_mode(&mut self, msg: Vec<String>) {
self.state.popup = msg;
fn enter_dialog_mode<T: Into<Text<'ws>>>(&mut self, msg: T) {
self.state.popup = msg.into();
self.state.modality_stack.push(Modality::Dialog);
}

View File

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

View File

@ -7,14 +7,9 @@ use ratatui::{
widgets::{Block, Cell, Row, StatefulWidget, Table, Widget},
};
use crate::book;
use super::{Address, Book, RangeSelection};
// TODO(zaphar): Move this to the book module.
// NOTE(zaphar): This is stolen from ironcalc but ironcalc doesn't expose it
// publically.
pub(crate) const LAST_COLUMN: usize = 16_384;
pub(crate) const LAST_ROW: usize = 1_048_576;
/// A visible column to show in our Viewport.
#[derive(Clone, Debug)]
pub struct VisibleColumn {
@ -68,7 +63,7 @@ impl<'ws> Viewport<'ws> {
let start_row = std::cmp::min(self.selected.row, state.prev_corner.row);
let mut start = start_row;
let mut end = start_row;
for row_idx in start_row..=LAST_ROW {
for row_idx in start_row..=(book::LAST_ROW as usize) {
let updated_length = length + 1;
if updated_length <= height {
length = updated_length;
@ -95,7 +90,7 @@ impl<'ws> Viewport<'ws> {
// We start out with a length of 5 already reserved
let mut length = 5;
let start_idx = std::cmp::min(self.selected.col, state.prev_corner.col);
for idx in start_idx..=LAST_COLUMN {
for idx in start_idx..=(book::LAST_COLUMN as usize) {
let size = self.book.get_col_size(idx)? as u16;
let updated_length = length + size;
let col = VisibleColumn { idx, length: size };
@ -198,15 +193,28 @@ impl<'ws> Viewport<'ws> {
ci: usize,
mut cell: Cell<'widget>,
) -> Cell<'widget> {
let style = self
// TODO(zaphar): Should probably create somekind of formatter abstraction.
if let Some(style) = self
.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(
style.as_ref().map(|s| s.fill.bg_color.as_ref()).flatten(),
style.fill.bg_color.as_ref(),
Color::Rgb(35, 33, 54),
);
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,
);
if let Some((start, end)) = &self.range_selection.map_or(None, |r| r.get_range()) {
@ -217,12 +225,12 @@ impl<'ws> Viewport<'ws> {
} else {
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)),
// TODO(zaphar): Support ironcalc style options
_ => cell,
}
.bold()
};
cell
}
}
@ -248,7 +256,6 @@ pub(crate) fn map_color(color: Option<&String>, otherwise: Color) -> Color {
candidate => {
// TODO(jeremy): Should we support more syntaxes than hex string?
// hsl(...) ??
// rgb(...) ??
if candidate.starts_with("#") {
if let Ok(rgb) = colorsys::Rgb::from_hex_str(candidate) {
// Note that the colorsys rgb model clamps the f64 values to no more

View File

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