From 08e35f209d1a69c04012712b677a9f6ae3373f0f Mon Sep 17 00:00:00 2001 From: Nicolas Hatcher Date: Wed, 1 May 2024 08:26:49 +0200 Subject: [PATCH] UPDATE: Introducing TironCalc, or Tiron for friends --- .gitignore | 3 +- Cargo.lock | 432 +++++++++++++++++++++++++++++- Cargo.toml | 1 + base/src/user_model.rs | 21 +- tironcalc/Cargo.toml | 12 + tironcalc/README.md | 52 ++++ tironcalc/screenshot.png | Bin 0 -> 10705 bytes tironcalc/src/main.rs | 556 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 1062 insertions(+), 15 deletions(-) create mode 100644 tironcalc/Cargo.toml create mode 100644 tironcalc/README.md create mode 100644 tironcalc/screenshot.png create mode 100644 tironcalc/src/main.rs diff --git a/.gitignore b/.gitignore index 5d0c59e..83b3ef1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/* -.DS_Store \ No newline at end of file +**/node_modules/* +.DS_Store diff --git a/Cargo.lock b/Cargo.lock index 2192a4b..7883ae9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,18 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -28,6 +40,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -76,6 +94,12 @@ dependencies = [ "syn", ] +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "block-buffer" version = "0.10.4" @@ -124,6 +148,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.90" @@ -151,7 +190,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-targets", + "windows-targets 0.52.4", ] [[package]] @@ -186,6 +225,19 @@ dependencies = [ "inout", ] +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -232,6 +284,31 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -299,6 +376,22 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hmac" version = "0.12.1" @@ -331,6 +424,12 @@ dependencies = [ "cc", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "inout" version = "0.1.3" @@ -411,12 +510,31 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.7.2" @@ -432,6 +550,18 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -453,6 +583,29 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "parking_lot" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.4", +] + [[package]] name = "parse-zoneinfo" version = "0.3.0" @@ -473,6 +626,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pbkdf2" version = "0.11.0" @@ -589,6 +748,35 @@ dependencies = [ "getrandom", ] +[[package]] +name = "ratatui" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "itertools", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.10.4" @@ -624,6 +812,12 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" +[[package]] +name = "rustversion" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" + [[package]] name = "ryu" version = "1.0.17" @@ -636,6 +830,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.197" @@ -700,12 +900,86 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "stability" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.5.0" @@ -762,6 +1036,26 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "tiron" +version = "0.1.3" +dependencies = [ + "crossterm", + "ironcalc", + "ratatui", + "tui-input", +] + +[[package]] +name = "tui-input" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e785f863a3af4c800a2a669d0b64c879b538738e352607e2624d03f868dc01" +dependencies = [ + "crossterm", + "unicode-width", +] + [[package]] name = "typenum" version = "1.17.0" @@ -774,6 +1068,18 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" + [[package]] name = "uuid" version = "1.8.0" @@ -908,13 +1214,59 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -923,57 +1275,119 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 5fd35ee..36090c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "base", "xlsx", + "tironcalc", "bindings/wasm", ] diff --git a/base/src/user_model.rs b/base/src/user_model.rs index 9351f53..ccde357 100644 --- a/base/src/user_model.rs +++ b/base/src/user_model.rs @@ -292,7 +292,10 @@ fn vertical(value: &str) -> Result { /// # } /// ``` pub struct UserModel { - model: Model, + /// The underlying model + /// See also: + /// * [Model] + pub model: Model, history: History, send_queue: Vec, pause_evaluation: bool, @@ -645,7 +648,9 @@ impl UserModel { pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), String> { let diff_list = vec![Diff::InsertRow { sheet, row }]; self.push_diff_list(diff_list); - self.model.insert_rows(sheet, row, 1) + self.model.insert_rows(sheet, row, 1)?; + self.model.evaluate(); + Ok(()) } /// Deletes a row @@ -672,7 +677,9 @@ impl UserModel { old_data, }]; self.push_diff_list(diff_list); - self.model.delete_rows(sheet, row, 1) + self.model.delete_rows(sheet, row, 1)?; + self.model.evaluate(); + Ok(()) } /// Inserts a column @@ -682,7 +689,9 @@ impl UserModel { pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), String> { let diff_list = vec![Diff::InsertColumn { sheet, column }]; self.push_diff_list(diff_list); - self.model.insert_columns(sheet, column, 1) + self.model.insert_columns(sheet, column, 1)?; + self.model.evaluate(); + Ok(()) } /// Deletes a column @@ -727,7 +736,9 @@ impl UserModel { }), }]; self.push_diff_list(diff_list); - self.model.delete_columns(sheet, column, 1) + self.model.delete_columns(sheet, column, 1)?; + self.model.evaluate(); + Ok(()) } /// Sets the width of a column diff --git a/tironcalc/Cargo.toml b/tironcalc/Cargo.toml new file mode 100644 index 0000000..1de181f --- /dev/null +++ b/tironcalc/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tiron" +version = "0.1.3" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +crossterm = "0.27.0" +ironcalc = { path = "../xlsx"} +ratatui = "0.26.2" +tui-input = "0.8.0" diff --git a/tironcalc/README.md b/tironcalc/README.md new file mode 100644 index 0000000..9dce4ba --- /dev/null +++ b/tironcalc/README.md @@ -0,0 +1,52 @@ +# TironCalc + +[![Discord chat][discord-badge]][discord-url] + +[discord-badge]: https://img.shields.io/discord/1206947691058171904.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/zZYWfh3RHJ + +TironCalc, or Tiron for friends, is a TUI (Terminal User Interface) for IronCalc. Based on [ratatui](https://github.com/ratatui-org/ratatui) + +![TironCalc Screenshot](screenshot.png) + +## Build + +``` +cargo build --release +``` + +You will find the binary at `./target/release/tiron`. + +## Documentation + +Start empty project: + +``` +$ tiron +``` + +Load an existing Excel file: + +``` +$ tiron example.xlsx +``` + +- `e` to edit a cell and enter the value or formula. +- `q` to quit and save +- `+` to add a sheet +- `s` to go to the next sheet +- `PgUp/PgDown` to navigate rows faster +- `u` undo changes +- `U` redo changes +- `r` insert row +- `c` insert column +- `C` delete column +- `R` delete row +- ` + + +## Inspiration + +James Gosling of Java fame created [sc](https://en.wikipedia.org/wiki/Sc_(spreadsheet_calculator)) the spreadsheet calculator. + +Andrés Martinelli has been maintaining [sc-im](https://github.com/andmarti1424/sc-im), the spreadsheet calculator improvised. diff --git a/tironcalc/screenshot.png b/tironcalc/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..6bf31588c29ac1ac3a7c229ffcd24d86343c9042 GIT binary patch literal 10705 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 TNAOKe2ngAaijtM$MuGnaJ{|(S literal 0 HcmV?d00001 diff --git a/tironcalc/src/main.rs b/tironcalc/src/main.rs new file mode 100644 index 0000000..aeaaf34 --- /dev/null +++ b/tironcalc/src/main.rs @@ -0,0 +1,556 @@ +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ironcalc::{ + base::{expressions::utils::number_to_column, Model, UserModel}, + export::save_to_xlsx, + import::{load_from_icalc, load_from_xlsx}, +}; +use ratatui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style, Stylize}, + text::{Line, Span}, + widgets::{Block, BorderType, Borders, Cell, Clear, Paragraph, Row, Table}, + Terminal, +}; +use std::thread; +use std::time::{Duration, Instant}; +use std::{io, sync::mpsc}; +use tui_input::{backend::crossterm::EventHandler, Input}; + +use std::env; + +enum Event { + Input(I), + Tick, +} + +#[derive(PartialEq)] +enum CursorMode { + Navigate, + Input, + Popup, +} + +struct SelectedRange { + sheet: u32, + row: i32, + column: i32, + min_row: i32, + min_column: i32, + max_row: i32, + max_column: i32, +} + +struct SheetState { + row: i32, + column: i32, + min_row: i32, + min_column: i32, + max_row: i32, + max_column: i32, +} + +struct ModelState { + selected_sheet: u32, + sheet_states: Vec, +} + +impl ModelState { + pub fn new(sheet_count: usize) -> ModelState { + let mut sheet_states = vec![]; + for _ in 0..sheet_count { + sheet_states.push(SheetState { + row: 1, + column: 1, + min_row: 1, + min_column: 1, + max_row: 1, + max_column: 1, + }); + } + ModelState { + selected_sheet: 0, + sheet_states, + } + } + + pub fn get_selected_range(&self) -> SelectedRange { + let sheet = self.selected_sheet; + let sheet_state = self.sheet_states.get(sheet as usize).unwrap(); + + SelectedRange { + sheet, + row: sheet_state.row, + column: sheet_state.column, + min_column: sheet_state.min_column, + min_row: sheet_state.min_row, + max_column: sheet_state.max_column, + max_row: sheet_state.max_row, + } + } + + pub fn set_selected_sheet(&mut self, selected_sheet: u32) { + self.selected_sheet = selected_sheet; + } + + pub fn get_selected_sheet(&self) -> u32 { + self.selected_sheet + } + + pub fn move_up(&mut self) { + let sheet = self.selected_sheet; + let mut sheet_state = &mut self.sheet_states.get(sheet as usize).unwrap(); + sheet_state.column -= 1; + + } + + pub fn move_down(&mut self) { + + } + + pub fn move_left(&mut self) { + + } + + pub fn move_right(&mut self) { + + } + + pub fn move_shift_up(&mut self) { + + } + + pub fn move_shift_down(&mut self) { + + } + + pub fn move_shift_left(&mut self) { + + } + + pub fn move_shift_right(&mut self) { + + } + + +} + +fn main() -> Result<(), Box> { + enable_raw_mode()?; + + let args: Vec = env::args().collect(); + let mut file_name = "model.xlsx"; + let model = if args.len() > 1 { + file_name = &args[1]; + if file_name.ends_with(".ic") { + load_from_icalc(file_name).unwrap() + } else { + load_from_xlsx(file_name, "en", "UTC").unwrap() + } + } else { + Model::new_empty(file_name, "en", "UTC").unwrap() + }; + let mut user_model = UserModel::from_model(model); + let mut state = ModelState::new(user_model.get_worksheets_properties().len()); + // let mut selected_sheet = 0; + // let mut selected_row_index = 1; + // let mut selected_column_index = 1; + let mut minimum_row_index = 1; + let mut minimum_column_index = 1; + let sheet_list_width = 20; + let column_width: u16 = 11; + let mut cursor_mode = CursorMode::Navigate; + let mut input_formula = Input::default(); + + let mut input_file_name: Input = file_name.into(); + + let mut popup_open = false; + + let (tx, rx) = mpsc::channel(); + let tick_rate = Duration::from_millis(200); + thread::spawn(move || { + let mut last_tick = Instant::now(); + loop { + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + + if event::poll(timeout).expect("poll works") { + if let CEvent::Key(key) = event::read().expect("can read events") { + tx.send(Event::Input(key)).expect("can send events"); + } + } + + if last_tick.elapsed() >= tick_rate && tx.send(Event::Tick).is_ok() { + last_tick = Instant::now(); + } + } + }); + + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + terminal.clear()?; + + let header_style = Style::default().fg(Color::Yellow).bg(Color::White); + let selected_header_style = Style::default().bg(Color::Yellow).fg(Color::White); + + let selected_cell_style = Style::default().fg(Color::Yellow).bg(Color::LightCyan); + + let background_style = Style::default().bg(Color::Black); + let selected_sheet_style = Style::default().bg(Color::White).fg(Color::LightMagenta); + let non_selected_sheet_style = Style::default().fg(Color::White); + let mut sheet_properties = user_model.get_worksheets_properties(); + loop { + terminal.draw(|rect| { + let size = rect.size(); + + let global_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(sheet_list_width), Constraint::Min(3)].as_ref()) + .split(size); + + // Sheet list to the left + let sheets = Block::default() + .borders(Borders::ALL) + .style(Style::default().fg(Color::White)) + .title("Sheets") + .border_type(BorderType::Plain) + .style(background_style); + let mut rows = vec![]; + (0..sheet_properties.len()).for_each(|sheet_index| { + let sheet_name = &sheet_properties[sheet_index].name; + let style = if sheet_index == state.get_selected_sheet() { + selected_sheet_style + } else { + non_selected_sheet_style + }; + rows.push(Row::new(vec![Cell::from(sheet_name.clone()).style(style)])); + }); + let widths = &[Constraint::Length(100)]; + let sheet_list = Table::new(rows, widths).block(sheets).column_spacing(0); + + rect.render_widget(sheet_list, global_chunks[0]); + + // The spreadsheet is the formula bar at the top and the sheet data + let spreadsheet_chunks = Layout::default() + .direction(Direction::Vertical) + .margin(0) + .constraints([Constraint::Length(1), Constraint::Min(2)].as_ref()) + .split(global_chunks[1]); + + let spreadsheet_width = size.width - sheet_list_width; + let spreadsheet_heigh = size.height - 1; + let row_count = spreadsheet_heigh - 1; + + let first_row_width: u16 = 3; + let column_count = + f64::ceil(((spreadsheet_width - first_row_width) as f64) / (column_width as f64)) + as i32; + let mut rows = vec![]; + // The first row in the column headers + let mut row = Vec::new(); + // The first cell in that row is the top left square of the spreadsheet + row.push(Cell::from("")); + let mut maximum_column_index = minimum_column_index + column_count - 1; + let mut maximum_row_index = minimum_row_index + row_count - 1; + + // We want to make sure the selected cell is visible. + if selected_column_index > maximum_column_index { + maximum_column_index = selected_column_index; + minimum_column_index = maximum_column_index - column_count + 1; + } else if selected_column_index < minimum_column_index { + minimum_column_index = selected_column_index; + maximum_column_index = minimum_column_index + column_count - 1; + } + if selected_row_index >= maximum_row_index { + maximum_row_index = selected_row_index; + minimum_row_index = maximum_row_index - row_count + 1; + } else if selected_row_index < minimum_row_index { + minimum_row_index = selected_row_index; + maximum_row_index = minimum_row_index + row_count - 1; + } + for column_index in minimum_column_index..=maximum_column_index { + let column_str = number_to_column(column_index); + let style = if column_index == selected_column_index { + selected_header_style + } else { + header_style + }; + row.push(Cell::from(format!(" {}", column_str.unwrap())).style(style)); + } + rows.push(Row::new(row)); + for row_index in minimum_row_index..=maximum_row_index { + let mut row = Vec::new(); + let style = if row_index == selected_row_index { + selected_header_style + } else { + header_style + }; + row.push(Cell::from(format!("{}", row_index)).style(style)); + for column_index in minimum_column_index..=maximum_column_index { + let value = user_model + .get_formatted_cell_value( + selected_sheet as u32, + row_index as i32, + column_index, + ) + .unwrap(); + // let cell_style = user_model + // .get_cell_style(selected_sheet as u32, row_index as i32, column_index) + // .unwrap(); + let style = if selected_row_index == row_index + && selected_column_index == column_index + { + selected_cell_style + } else { + // let bg_color = match cell_style.fill.fg_color { + // Some(s) => Color::from_str(&s).unwrap(), + // None => Color::White, + // }; + // let fg_color = match cell_style.font.color { + // Some(s) => Color::from_str(&s).unwrap(), + // None => Color::Black, + // }; + let bg_color = Color::White; + let fg_color = Color::Black; + Style::default().fg(fg_color).bg(bg_color) + }; + row.push(Cell::from(value.to_string()).style(style)); + } + rows.push(Row::new(row)); + } + let mut widths = Vec::new(); + widths.push(Constraint::Length(first_row_width)); + for _ in 0..column_count { + widths.push(Constraint::Length(column_width)); + } + let spreadsheet = Table::new(rows, widths) + .block(Block::default().style(Style::default().bg(Color::Black))) + .column_spacing(0); + + let text = if cursor_mode != CursorMode::Input { + user_model + .get_cell_content( + selected_sheet as u32, + selected_row_index as i32, + selected_column_index, + ) + .unwrap() + } else { + input_formula.value().to_string() + }; + let cell_address_text = format!( + "{}{}: ", + number_to_column(selected_column_index).unwrap(), + selected_row_index, + ); + let formula_bar_text = format!("{}{}", cell_address_text, text,); + let formula_bar = Paragraph::new(vec![Line::from(vec![Span::raw(formula_bar_text)])]); + rect.render_widget(formula_bar.block(Block::default()), spreadsheet_chunks[0]); + rect.render_widget(spreadsheet, spreadsheet_chunks[1]); + if cursor_mode == CursorMode::Input { + let area = spreadsheet_chunks[0]; + rect.set_cursor( + area.x + + (input_formula.visual_cursor() as u16) + + cell_address_text.len() as u16, + area.y, + ) + } + + if popup_open { + let area = centered_rect(60, 20, size); + rect.render_widget(Clear, area); + let input_text = input_file_name.value(); + let text = vec![ + Line::from(vec![input_text.fg(Color::Yellow)]), + "".into(), + Line::from(vec![ + "ESC".green(), + " to abort. ".into(), + "END".green(), + " to quit without saving. ".into(), + "Enter".green(), + " to save and quit".into(), + ]), + ]; + rect.render_widget( + Paragraph::new(text).block(Block::bordered().title("Save as")), + area, + ); + rect.set_cursor( + // Put cursor past the end of the input text + area.x + (input_file_name.visual_cursor() as u16) + 1, + // Move one line own, from the border to the input line + area.y + 1, + ) + } + })?; + + match cursor_mode { + CursorMode::Popup => { + match rx.recv()? { + Event::Input(event) => match event.code { + KeyCode::End => { + terminal.clear()?; + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + break; + } + KeyCode::Enter => { + terminal.clear()?; + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + let _ = save_to_xlsx(&user_model.model, input_file_name.value()); + break; + } + KeyCode::Esc => { + popup_open = false; + cursor_mode = CursorMode::Navigate; + } + _ => { + input_file_name.handle_event(&CEvent::Key(event)); + } + }, + Event::Tick => {} + } + } + CursorMode::Navigate => { + match rx.recv()? { + Event::Input(event) => match event.code { + KeyCode::Char('q') => { + popup_open = true; + cursor_mode = CursorMode::Popup; + } + KeyCode::Down => { + selected_row_index += 1; + } + KeyCode::Up => { + if selected_row_index > 1 { + selected_row_index -= 1; + } + } + KeyCode::Right => { + selected_column_index += 1; + } + KeyCode::Left => { + if selected_column_index > 1 { + selected_column_index -= 1; + } + } + KeyCode::PageDown => { + selected_row_index += 10; + } + KeyCode::PageUp => { + if selected_row_index > 10 { + selected_row_index -= 10; + } else { + selected_row_index = 1; + } + } + KeyCode::Char('s') => { + selected_sheet += 1; + if selected_sheet >= sheet_properties.len() { + selected_sheet = 0; + } + } + KeyCode::Char('a') => { + selected_sheet = selected_sheet.saturating_sub(1); + } + KeyCode::Char('u') => user_model.undo().unwrap(), + KeyCode::Char('U') => user_model.redo().unwrap(), + KeyCode::Char('c') => user_model + .insert_column(selected_sheet as u32, selected_column_index as i32) + .unwrap(), + KeyCode::Char('C') => user_model + .delete_column(selected_sheet as u32, selected_column_index as i32) + .unwrap(), + KeyCode::Char('r') => user_model + .insert_row(selected_sheet as u32, selected_row_index as i32) + .unwrap(), + KeyCode::Char('R') => user_model + .delete_row(selected_sheet as u32, selected_row_index as i32) + .unwrap(), + KeyCode::Char('e') => { + cursor_mode = CursorMode::Input; + let input_str = user_model + .get_cell_content( + selected_sheet as u32, + selected_row_index as i32, + selected_column_index, + ) + .unwrap(); + // .unwrap_or_default(); + input_formula = input_formula.with_value(input_str); + } + KeyCode::Char('+') => { + user_model.new_sheet(); + sheet_properties = user_model.get_worksheets_properties(); + } + _ => { + // println!("{:?}", event); + } + }, + Event::Tick => {} + } + } + CursorMode::Input => match rx.recv()? { + Event::Input(event) => match event.code { + KeyCode::Enter => { + cursor_mode = CursorMode::Navigate; + let value = input_formula.value().to_string(); + let sheet = selected_sheet as i32; + let row = selected_row_index as i32; + let column = selected_column_index; + user_model + .set_user_input(sheet as u32, row, column, &value) + .unwrap(); + user_model.evaluate(); + } + _ => { + input_formula.handle_event(&CEvent::Key(event)); + } + }, + Event::Tick => {} + }, + } + } + + Ok(()) +} + +// helper function to create a centered rect using up certain percentage of the available rect `r` +fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let popup_layout = Layout::vertical([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + + Layout::horizontal([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_layout[1])[1] +}