diff options
| author | Nathan Reiner <nathan@nathanreiner.xyz> | 2024-07-25 00:40:37 +0200 |
|---|---|---|
| committer | Nathan Reiner <nathan@nathanreiner.xyz> | 2024-07-25 00:40:37 +0200 |
| commit | 58b305b9f6d13007d2ea62775054c95177f81092 (patch) | |
| tree | 2123cb5379aa69476b1e549e0bcacc08d67fba71 | |
first sketch
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 666 | ||||
| -rw-r--r-- | Cargo.toml | 8 | ||||
| -rw-r--r-- | src/app.rs | 81 | ||||
| -rw-r--r-- | src/lua/iobuffer.rs | 38 | ||||
| -rw-r--r-- | src/lua/mod.rs | 80 | ||||
| -rw-r--r-- | src/main.rs | 20 | ||||
| -rw-r--r-- | src/sheet/cell.rs | 198 | ||||
| -rw-r--r-- | src/sheet/mod.rs | 98 | ||||
| -rw-r--r-- | src/sheet/register.rs | 60 | ||||
| -rw-r--r-- | src/tui.rs | 22 | ||||
| -rw-r--r-- | src/widgets/luaeditor/buffer.rs | 134 | ||||
| -rw-r--r-- | src/widgets/luaeditor/cursor.rs | 102 | ||||
| -rw-r--r-- | src/widgets/luaeditor/mod.rs | 132 | ||||
| -rw-r--r-- | src/widgets/mod.rs | 2 | ||||
| -rw-r--r-- | src/widgets/sheetview.rs | 360 |
16 files changed, 2002 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a7d12ee --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,666 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[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 = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "serde", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[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 = "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 = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[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 = "mlua" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d111deb18a9c9bd33e1541309f4742523bfab01d276bfa9a27519f6de9c11dc7" +dependencies = [ + "bstr", + "mlua-sys", + "num-traits", + "once_cell", + "rustc-hash", +] + +[[package]] +name = "mlua-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a088ed0723df7567f569ba018c5d48c23c501f3878b190b04144dfa5ebfa8abc" +dependencies = [ + "cc", + "cfg-if", + "pkg-config", +] + +[[package]] +name = "neosheet" +version = "0.1.0" +dependencies = [ + "mlua", + "ratatui", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +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.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ratatui" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "itertools", + "lru", + "paste", + "stability", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[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.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +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-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[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-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]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bd83f5b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "neosheet" +version = "0.1.0" +edition = "2021" + +[dependencies] +mlua = { version = "0.9.9", features = ["luajit"] } +ratatui = "0.27.0" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..3544fdf --- /dev/null +++ b/src/app.rs @@ -0,0 +1,81 @@ +use std::{io, time::Duration}; + +use crate::{sheet::register::Register, tui, widgets::sheetview::SheetView}; + +use ratatui::{ + crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}, + prelude::*, + widgets::Block, +}; + +#[derive(Debug)] +pub struct App<'a> { + exit: bool, + view: SheetView<'a>, +} + +impl App<'_> { + pub fn new() -> Self { + let sheet_id = Register::create(200, 1000); + let sheet = Register::get(sheet_id).unwrap(); + + { + let mut sheet = sheet.write().unwrap(); + + for row in 0..sheet.height() { + for column in 0..sheet.width() { + sheet.set_cell(row, column, format!("{}/{}", row, column).into()); + } + } + } + + let view = SheetView::new(sheet_id).block(Block::bordered().title("Sheet")); + + Self { exit: false, view } + } + + pub fn run(&mut self, terminal: &mut tui::Tui) -> io::Result<()> { + while !self.exit { + terminal.draw(|frame| self.render_frame(frame))?; + self.handle_events()?; + } + + Ok(()) + } + + fn render_frame(&mut self, frame: &mut Frame) { + frame.render_widget(self, frame.size()); + } + + fn handle_events(&mut self) -> io::Result<()> { + if event::poll(Duration::from_millis(100))? { + match event::read()? { + Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { + self.handle_key_event(key_event) + } + _ => {} + } + } + Ok(()) + } + + fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Char('q') if key_event.modifiers == KeyModifiers::CONTROL => self.exit(), + _ => self.view.handle_key_event(key_event), + } + } + + fn exit(&mut self) { + self.exit = true; + } +} + +impl Widget for &mut App<'_> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + self.view.render(area, buf); + } +} diff --git a/src/lua/iobuffer.rs b/src/lua/iobuffer.rs new file mode 100644 index 0000000..1150e0d --- /dev/null +++ b/src/lua/iobuffer.rs @@ -0,0 +1,38 @@ +use std::sync::RwLock; + +pub struct IoBuffer { + buffer: String, +} + +impl IoBuffer { + const fn new() -> Self { + Self { + buffer: String::new(), + } + } + + pub fn get(&self) -> String { + self.buffer.clone() + } + + pub fn write<S>(&mut self, value: S) + where + S: AsRef<str> + { + self.buffer += value.as_ref(); + } + + pub fn writeln<S>(&mut self, value: S) + where + S: AsRef<str> + { + self.buffer += value.as_ref(); + self.buffer += "\n"; + } +} + +static STDOUT_BUFFER: RwLock<IoBuffer> = RwLock::new(IoBuffer::new()); + +pub fn iobuffer() -> &'static RwLock<IoBuffer> { + &STDOUT_BUFFER +} diff --git a/src/lua/mod.rs b/src/lua/mod.rs new file mode 100644 index 0000000..5d45ee9 --- /dev/null +++ b/src/lua/mod.rs @@ -0,0 +1,80 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use mlua::prelude::*; + +use crate::sheet::{cell::CellRef, register::Register}; + +pub mod iobuffer; + +fn print(_: &Lua, args: LuaMultiValue) -> LuaResult<()> { + let mut writer = iobuffer::iobuffer().write().unwrap(); + + for (i, arg) in args.iter().enumerate() { + if let Some(ud) = arg.as_userdata() { + if ud.is::<CellRef>() { + writer.write(ud.borrow::<CellRef>().unwrap().value().to_string()); + } else { + writer.write(format!("{:#?}", ud)); + } + } else { + writer.write(format!("{:#?}", arg)); + } + + if i < args.len() - 1 { + writer.write(", "); + } + } + writer.writeln(""); + Ok(()) +} + +pub fn new_instance() -> LuaResult<Lua> { + let lua = Lua::new(); + + let print_binding = lua.create_function(print)?; + lua.globals().set("print", print_binding)?; + + { + let math = lua.globals().get::<_, LuaTable>("math"); + let randomseed = math.unwrap().get::<_, LuaFunction>("randomseed").unwrap(); + + let seed = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); + randomseed.call::<_, ()>(seed).unwrap(); + } + + let neosheet = lua.create_table()?; + + let register = Register; + neosheet.set("sheets", register)?; + + lua.globals().set("neosheet", neosheet)?; + + Ok(lua) +} + +#[cfg(test)] +mod test { + use mlua::Function; + + use super::new_instance; + + #[test] + fn function_eval() { + let lua = new_instance().unwrap(); + let func: Function = lua + .load( + r#" + function(a, b, c) + return a + b * c + end + "#, + ) + .eval() + .unwrap(); + eprintln!("{}", func.call::<_, String>((1, 2, 3, 4)).unwrap()); + assert!(false); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..bbfafb1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,20 @@ +use mlua::prelude::*; +use std::io; + +pub mod app; +pub mod sheet; +pub mod tui; +pub mod widgets; +pub mod lua; + +fn run() -> io::Result<()> { + let mut terminal = tui::init()?; + let app_result = app::App::new().run(&mut terminal); + tui::restore()?; + app_result +} + +fn main() -> LuaResult<()> { + run()?; + Ok(()) +} diff --git a/src/sheet/cell.rs b/src/sheet/cell.rs new file mode 100644 index 0000000..413e299 --- /dev/null +++ b/src/sheet/cell.rs @@ -0,0 +1,198 @@ +use std::fmt::Display; + +use mlua::{FromLua, IntoLua, UserData, Value}; + +use super::register::{Register, SheetId}; + +#[derive(Debug, Clone)] +pub enum Cell { + String(String), + Number(f64), +} + +impl Cell { + pub fn new_empty() -> Self { + Cell::String("".to_string()) + } +} + +impl<'lua> IntoLua<'lua> for Cell { + fn into_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> { + match self { + Cell::String(s) => s.into_lua(lua), + Cell::Number(n) => n.into_lua(lua), + } + } +} + +impl<'lua> FromLua<'lua> for Cell { + fn from_lua(value: mlua::Value<'lua>, _: &'lua mlua::Lua) -> mlua::Result<Self> { + match value { + Value::Nil => Ok(Cell::new_empty()), + Value::Boolean(b) => Ok(Cell::String(b.to_string())), + Value::Integer(n) => Ok(Cell::String(n.to_string())), + Value::Number(n) => Ok(Cell::String(n.to_string())), + Value::String(s) => Ok(Cell::String(s.to_str()?.to_string())), + _ => Err(mlua::Error::runtime( + "cell content must be number or string", + )), + } + } +} + +impl Display for Cell { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Cell::String(s) => s.fmt(f), + Cell::Number(n) => n.fmt(f), + } + } +} + +impl From<String> for Cell { + fn from(value: String) -> Self { + Cell::String(value.clone()) + } +} + +impl From<&str> for Cell { + fn from(value: &str) -> Self { + Cell::String(value.to_string()) + } +} + +macro_rules! cell_from_integer { + ($($number:ty),+ $(,)?) => { + $( + impl From<$number> for Cell { + fn from(value: $number) -> Self { + Cell::Number(value as f64) + } + } + )* + }; +} + +#[rustfmt::skip] +cell_from_integer!( + i8, i16, i32, i64, i128, isize, + u8, u16, u32, u64, u128, usize, + f32, f64 +); + +#[derive(Debug, Clone, Copy)] +pub struct CellRef { + row: usize, + column: usize, + sheet_id: SheetId, +} + +impl CellRef { + pub fn new(sheet_id: SheetId, row: usize, column: usize) -> Option<Self> { + Register::get(sheet_id) + .map(|sheet| { + sheet.read().unwrap().get_cell(row, column).map(|_| Self { + sheet_id, + row, + column, + }) + }) + .unwrap_or(None) + } + + pub fn value(&self) -> Cell { + return Register::get(self.sheet_id) + .unwrap() + .read() + .unwrap() + .get_cell(self.row, self.column) + .unwrap() + .clone(); + } + + pub fn set_value(&mut self, value: Cell) { + Register::get(self.sheet_id) + .unwrap() + .write() + .unwrap() + .set_cell(self.row, self.column, value); + } + + pub fn left(&self) -> Option<Self> { + if self.column > 0 { + Self::new(self.sheet_id, self.row, self.column - 1) + } else { + None + } + } + + pub fn right(&self) -> Option<Self> { + Self::new(self.sheet_id, self.row, self.column + 1) + } + + pub fn up(&self) -> Option<Self> { + if self.row > 0 { + Self::new(self.sheet_id, self.row - 1, self.column) + } else { + None + } + } + + pub fn down(&self) -> Option<Self> { + Self::new(self.sheet_id, self.row + 1, self.column) + } + + pub fn begin(&self) -> Option<Self> { + Self::new(self.sheet_id, self.row, 0) + } + + pub fn end(&self) -> Option<Self> { + Self::new( + self.sheet_id, + self.row, + Register::get(self.sheet_id) + .unwrap() + .read() + .unwrap() + .width() + - 1, + ) + } + + pub fn top(&self) -> Option<Self> { + Self::new(self.sheet_id, 0, self.column) + } + + pub fn bottom(&self) -> Option<Self> { + Self::new( + self.sheet_id, + Register::get(self.sheet_id) + .unwrap() + .read() + .unwrap() + .height() + - 1, + self.column, + ) + } +} + +impl UserData for CellRef +where + Self: Sized, +{ + fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("row", |_, this| Ok(this.row)); + fields.add_field_method_get("column", |_, this| Ok(this.column)); + fields.add_field_method_get("value", |_, this| Ok(this.value())); + fields.add_field_method_set("value", |_, this, value| Ok(this.set_value(value))); + fields.add_field_method_get("left", |lua, this| this.left().into_lua(lua)); + fields.add_field_method_get("right", |lua, this| this.right().into_lua(lua)); + fields.add_field_method_get("up", |lua, this| this.up().into_lua(lua)); + fields.add_field_method_get("down", |lua, this| this.down().into_lua(lua)); + fields.add_field_method_get("begin", |lua, this| this.begin().into_lua(lua)); + fields.add_field_method_get("end", |lua, this| this.end().into_lua(lua)); + fields.add_field_method_get("top", |lua, this| this.top().into_lua(lua)); + fields.add_field_method_get("bottom", |lua, this| this.bottom().into_lua(lua)); + } +} diff --git a/src/sheet/mod.rs b/src/sheet/mod.rs new file mode 100644 index 0000000..015e3d8 --- /dev/null +++ b/src/sheet/mod.rs @@ -0,0 +1,98 @@ +use cell::{Cell, CellRef}; +use mlua::{IntoLua, UserData, Value}; +use register::{Register, SheetId}; + +pub mod cell; +pub mod register; + +#[derive(Debug, Default, Clone)] +pub struct Sheet { + id: register::SheetId, + rows: Vec<Vec<Cell>>, +} + +impl Sheet { + pub(self) fn new(width: usize, height: usize, id: register::SheetId) -> Self { + Self { + id, + rows: vec![vec![Cell::new_empty(); width]; height], + } + } + + pub fn set_cell(&mut self, row: usize, column: usize, cell: Cell) { + if row < self.height() && column < self.width() { + self.rows[row][column] = cell + } + } + + pub fn get_cell<'a>(&'a self, row: usize, column: usize) -> Option<&'a Cell> { + if let Some(r) = self.rows.get(row) { + if let Some(_) = r.get(column) { + return Some(&self.rows[row][column]); + } + } + + None + } + + pub fn get_ref(&self, row: usize, column: usize) -> Option<CellRef> { + CellRef::new(self.id, row, column) + } + + pub fn height(&self) -> usize { + self.rows.len() + } + + pub fn set_height(&mut self, mut height: usize) { + height = height.max(1); + self.rows + .resize(height, vec![Cell::new_empty(); self.width()]) + } + + pub fn width(&self) -> usize { + self.rows.get(0).map(|r| r.len()).unwrap_or(1) + } + + pub fn set_width(&mut self, mut width: usize) { + width = width.max(1); + for row in self.rows.iter_mut() { + row.resize(width, Cell::new_empty()); + } + } + + pub fn id(&self) -> register::SheetId { + self.id + } +} + +struct SheetLuaRef { + id: SheetId, +} + +impl SheetLuaRef { + pub fn new(id: SheetId) -> Self { + Self { id } + } +} + +impl UserData for SheetLuaRef { + fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("width", |_, luaref| { + Ok(Register::get(luaref.id).unwrap().read().unwrap().width()) + }); + + fields.add_field_method_get("height", |_, luaref| { + Ok(Register::get(luaref.id).unwrap().read().unwrap().height()) + }); + } + + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method_mut("cell", |lua, luaref, (row, column): (usize, usize)| { + if let Some(_) = Register::get(luaref.id) { + Ok(CellRef::new(luaref.id, row, column).into_lua(lua).unwrap()) + } else { + Ok(Value::Nil) + } + }) + } +} diff --git a/src/sheet/register.rs b/src/sheet/register.rs new file mode 100644 index 0000000..aa5245b --- /dev/null +++ b/src/sheet/register.rs @@ -0,0 +1,60 @@ +use std::sync::{Arc, RwLock}; + +use mlua::{UserData, Value}; + +use super::{Sheet, SheetLuaRef}; + +static REGISTER: RwLock<Vec<Arc<RwLock<Sheet>>>> = RwLock::new(Vec::new()); + +pub type SheetId = usize; + +#[derive(Debug)] +pub struct Register; + +impl Register { + pub fn create(width: usize, height: usize) -> SheetId { + let mut register = REGISTER.write().unwrap(); + + let id = register.len(); + let sheet = Sheet::new(width, height, id); + register.push(Arc::new(RwLock::new(sheet))); + id + } + + pub fn get<'a>(id: SheetId) -> Option<Arc<RwLock<Sheet>>> { + let register = REGISTER.read().unwrap(); + + if id < register.len() { + Some(Arc::clone(®ister[id])) + } else { + None + } + } +} + +impl UserData for Register { + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_function_mut("create", |lua, (width, height): (usize, usize)| { + let id = Register::create(width, height); + let luaref = SheetLuaRef::new(id); + if let Ok(ud) = lua.create_userdata(luaref) { + Ok(Value::UserData(ud)) + } else { + Ok(Value::Nil) + } + }); + + methods.add_function_mut("get", |lua, id: SheetId| { + if let Some(_) = Register::get(id) { + let luaref = SheetLuaRef::new(id); + if let Ok(ud) = lua.create_userdata(luaref) { + Ok(Value::UserData(ud)) + } else { + Ok(Value::Nil) + } + } else { + Ok(Value::Nil) + } + }) + } +} diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..5669855 --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,22 @@ +use std::io::{self, stdout, Stdout}; + +use ratatui::{backend::CrosstermBackend, crossterm::{terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand}, Terminal}; + + +pub type Tui = Terminal<CrosstermBackend<Stdout>>; + +pub fn init() -> io::Result<Tui> { + stdout().execute(EnterAlternateScreen)?; + enable_raw_mode()?; + let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; + + terminal.clear()?; + + Ok(terminal) +} + +pub fn restore() -> io::Result<()> { + stdout().execute(LeaveAlternateScreen)?; + disable_raw_mode()?; + Ok(()) +} diff --git a/src/widgets/luaeditor/buffer.rs b/src/widgets/luaeditor/buffer.rs new file mode 100644 index 0000000..bdbee8d --- /dev/null +++ b/src/widgets/luaeditor/buffer.rs @@ -0,0 +1,134 @@ +use std::str::FromStr; + +use super::cursor::{Cursor, CursorMove}; + +#[derive(Default, Debug)] +pub struct Buffer { + lines: Vec<String>, + cursor: Cursor, +} + +impl Buffer { + pub fn new() -> Self { + Self { + lines: Vec::new(), + cursor: Cursor::new().with_position(0, 0).with_max(0, 0) + } + } + + fn refresh_line_max(&mut self) { + self.cursor + .set_x_max(self.lines[self.cursor.y() as usize].len()) + } + + fn refresh_buffer_max(&mut self) { + self.cursor.set_y_max(self.lines.len() - 1); + } + + fn refresh_max(&mut self) { + self.refresh_line_max(); + self.refresh_buffer_max(); + } + + pub fn insert(&mut self, c: char) { + match c { + '\n' => { + let (a, b) = { + let line = self.current_line(); + let (a, b) = line.split_at(self.cursor.x()); + (a.to_string(), b.to_string()) + }; + + { + *self.current_line_mut() = a; + } + self.cursor.move_unchecked(CursorMove::Down(1)); + self.cursor.move_unchecked(CursorMove::Begin); + self.lines.insert(self.cursor().y(), b); + self.refresh_max() + } + _ => { + let x = self.cursor().x(); + self.current_line_mut().insert(x, c); + self.refresh_line_max(); + self.cursor.move_checked(CursorMove::Right(1)); + } + } + } + + pub fn delete(&mut self) { + if self.cursor.is_at_start() && !self.cursor.is_at_top() { + let old_line = self.current_line_mut().clone(); + self.lines.remove(self.cursor.y()); + self.cursor.move_checked(CursorMove::Up(1)); + self.refresh_line_max(); + + let new_x = self.current_line().len(); + self.current_line_mut().push_str(&old_line); + self.cursor.move_checked(CursorMove::Jump((new_x, self.cursor.y()))); + + self.refresh_buffer_max() + } else { + let x = self.cursor.x() - 1; + self.current_line_mut().remove(x); + self.refresh_line_max(); + self.cursor.move_checked(CursorMove::Left(1)); + } + + } + + pub fn line(&self, index: usize) -> Option<&String> { + self.lines.get(index) + } + + fn line_mut(&mut self, index: usize) -> Option<&mut String> { + self.lines.get_mut(index) + } + + pub fn current_line(&self) -> &String { + self.line(self.cursor.y() as usize).unwrap() + } + + fn current_line_mut(&mut self) -> &mut String { + self.line_mut(self.cursor.y() as usize).unwrap() + } + + pub fn cursor(&self) -> &Cursor { + &self.cursor + } + + pub fn move_cursor(&mut self, m: CursorMove) { + match m { + CursorMove::Up(_) + | CursorMove::Down(_) + | CursorMove::Top + | CursorMove::Bottom + | CursorMove::Jump(_) => { + self.cursor.move_checked(m); + self.cursor.set_x_max(self.current_line().len()); + self.cursor.correct_cursor_position(); + } + CursorMove::Left(_) | CursorMove::Right(_) | CursorMove::Begin | CursorMove::End => { + self.cursor.move_checked(m) + } + } + } + + pub fn lines(&self) -> &Vec<String> { + &self.lines + } +} + +impl FromStr for Buffer { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let lines: Vec<_> = s.lines().map(|s| s.to_string()).collect(); + let mut buffer = Self::new(); + buffer.lines = lines; + + buffer.refresh_max(); + + Ok(buffer) + } +} diff --git a/src/widgets/luaeditor/cursor.rs b/src/widgets/luaeditor/cursor.rs new file mode 100644 index 0000000..c4466d0 --- /dev/null +++ b/src/widgets/luaeditor/cursor.rs @@ -0,0 +1,102 @@ + +pub enum CursorMove { + Up(usize), + Down(usize), + Left(usize), + Right(usize), + Top, + Bottom, + Begin, + End, + Jump((usize, usize)), +} + +#[derive(Default, Debug)] +pub struct Cursor { + x: isize, + y: isize, + x_max: isize, + y_max: isize, +} + +impl Cursor { + pub fn new() -> Self { + Self { + x: 0, + y: 0, + x_max: 0, + y_max: 0, + } + } + + pub fn with_position(mut self, x: isize, y: isize) -> Self { + self.x = x; + self.y = y; + self + } + + pub fn with_max(mut self, x: isize, y: isize) -> Self { + self.x_max = x; + self.y_max = y; + self + } + + pub fn x(&self) -> usize { + self.x as usize + } + + pub fn y(&self) -> usize { + self.y as usize + } + + pub fn set_x_max(&mut self, max: usize) { + self.x_max = max as isize; + } + + pub fn set_y_max(&mut self, max: usize) { + self.y_max = max as isize; + } + + pub fn move_checked(&mut self, m: CursorMove) { + self.move_unchecked(m); + self.correct_cursor_position(); + } + + pub fn move_unchecked(&mut self, m: CursorMove) { + match m { + CursorMove::Up(n) => self.y -= n as isize, + CursorMove::Down(n) => self.y += n as isize, + CursorMove::Left(n) => self.x -= n as isize, + CursorMove::Right(n) => self.x += n as isize, + CursorMove::Top => self.y = 0, + CursorMove::Bottom => self.y = self.y_max, + CursorMove::Begin => self.x = 0, + CursorMove::End => self.x = self.x_max, + CursorMove::Jump((x, y)) => { + self.x = x as isize; + self.y = y as isize; + } + }; + } + + pub fn correct_cursor_position(&mut self) { + self.y = self.y.max(0).min(self.y_max); + self.x = self.x.max(0).min(self.x_max); + } + + pub fn is_at_start(&self) -> bool { + self.x == 0 + } + + pub fn is_at_end(&self) -> bool { + self.x == self.x_max + } + + pub fn is_at_top(&self) -> bool { + self.y == 0 + } + + pub fn is_at_bottom(&self) -> bool { + self.y == self.y_max + } +} diff --git a/src/widgets/luaeditor/mod.rs b/src/widgets/luaeditor/mod.rs new file mode 100644 index 0000000..723d8b5 --- /dev/null +++ b/src/widgets/luaeditor/mod.rs @@ -0,0 +1,132 @@ +use std::str::FromStr; + +use ratatui::{ + crossterm::event::{KeyCode, KeyEvent}, + prelude::BlockExt, + style::Stylize, + text::ToSpan, + widgets::{Block, Widget}, +}; + +pub mod buffer; +pub mod cursor; + +use buffer::Buffer; + +use self::cursor::CursorMove; + +#[derive(Debug)] +pub struct LuaEditor<'a> { + block: Option<Block<'a>>, + scroll: usize, + buffer: Buffer, +} + +impl<'a> LuaEditor<'a> { + pub fn new<S>(content: S) -> Self + where + S: AsRef<str>, + { + Self { + block: None, + scroll: 0, + buffer: Buffer::from_str(content.as_ref()).unwrap(), + } + } + + pub fn block(mut self, block: Option<Block<'a>>) -> Self { + self.block = block; + self + } + + pub fn handle_key_event(&mut self, event: KeyEvent) { + match event.code { + KeyCode::Char(c) => self.buffer.insert(c), + KeyCode::Backspace => { + self.buffer.delete(); + } + KeyCode::Enter => { + self.buffer.insert('\n'); + } + KeyCode::Left => self.buffer.move_cursor(CursorMove::Left(1)), + KeyCode::Right => self.buffer.move_cursor(CursorMove::Right(1)), + KeyCode::Up => self.buffer.move_cursor(CursorMove::Up(1)), + KeyCode::Down => self.buffer.move_cursor(CursorMove::Down(1)), + KeyCode::Home => {} + KeyCode::End => {} + KeyCode::PageUp => {} + KeyCode::PageDown => {} + KeyCode::Tab => self.buffer.insert('\t'), + KeyCode::BackTab => {} + KeyCode::Delete => {} + KeyCode::Insert => {} + KeyCode::F(_) => {} + KeyCode::Null => {} + KeyCode::Esc => {} + KeyCode::CapsLock => {} + KeyCode::ScrollLock => {} + KeyCode::NumLock => {} + KeyCode::PrintScreen => {} + KeyCode::Pause => {} + KeyCode::Menu => {} + KeyCode::KeypadBegin => {} + KeyCode::Media(_) => {} + KeyCode::Modifier(_) => {} + } + } + + pub fn text(&self) -> String { + self.buffer.lines().join("\n") + } +} + +impl Widget for &mut LuaEditor<'_> { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + self.block.render(area, buf); + let inner_area = self.block.inner_if_some(area); + + for (i, line) in self.buffer.lines().iter().enumerate().skip(self.scroll) { + let replace = &line.replace('\t', " "); + let span = replace.to_span(); + let mut span_area = inner_area.clone(); + span_area.height = 1; + span_area.y += i as u16; + + if !inner_area.contains(span_area.into()) { + break; + } + + span.render(span_area, buf) + } + + let mut cursor_area = inner_area.clone(); + cursor_area.width = 1; + cursor_area.height = 1; + cursor_area.y += self.buffer.cursor().y() as u16; + cursor_area.x += self.buffer.cursor().x() as u16; + + let (first, _) = self + .buffer + .current_line() + .split_at(self.buffer.cursor().x() as usize); + + for c in first.chars() { + if c == '\t' { + cursor_area.x += 1; + } + } + + self.buffer + .current_line() + .chars() + .nth(self.buffer.cursor().x() as usize) + .map(|c| if c == '\t' { ' ' } else { c }) + .unwrap_or(' ') + .to_span() + .reversed() + .render(cursor_area, buf); + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs new file mode 100644 index 0000000..8a9b1a0 --- /dev/null +++ b/src/widgets/mod.rs @@ -0,0 +1,2 @@ +pub mod sheetview; +pub mod luaeditor; diff --git a/src/widgets/sheetview.rs b/src/widgets/sheetview.rs new file mode 100644 index 0000000..57894a2 --- /dev/null +++ b/src/widgets/sheetview.rs @@ -0,0 +1,360 @@ +use std::{ + sync::{Arc, Mutex}, + thread::JoinHandle, +}; + +use layout::Offset; +use mlua::Function; +use ratatui::{ + crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, + prelude::*, + style::Stylize, + text::ToLine, + widgets::{block::BlockExt, Block, Clear, Gauge, Widget}, +}; + +use crate::sheet::{ + cell::Cell, + register::{Register, SheetId}, +}; + +use super::luaeditor::LuaEditor; + +const DEFAULT_COLUMN_WIDTH: u16 = 10; + +#[derive(Debug)] +pub struct SheetView<'a> { + block: Option<Block<'a>>, + luaeditor: Option<LuaEditor<'a>>, + sheet: SheetId, + selection: Option<(u16, u16)>, + cursor: (u16, u16), + scroll: (u16, u16), + error_window: Option<String>, + process_handle: Option<JoinHandle<Option<String>>>, + process_progress: Arc<Mutex<usize>>, +} + +impl<'a> SheetView<'a> { + pub fn new(sheet: SheetId) -> Self { + Self { + block: None, + luaeditor: None, + sheet, + selection: None, + cursor: (0, 0), + scroll: (0, 0), + error_window: None, + process_handle: None, + process_progress: Arc::new(Mutex::new(0)), + } + } + + pub fn block(mut self, block: Block<'a>) -> Self { + self.block = Some(block); + self + } + + pub fn sheet(mut self, sheet: SheetId) -> Self { + self.sheet = sheet; + self + } + + pub fn move_cursor_by(&mut self, delta: (i32, i32)) { + let lock = Register::get(self.sheet).unwrap(); + let sheet = lock.read().unwrap(); + + self.cursor.0 = ((self.cursor.0 as i32) + delta.0) + .max(0) + .min(sheet.height() as i32 - 1) as u16; + self.cursor.1 = ((self.cursor.1 as i32) + delta.1) + .max(0) + .min(sheet.width() as i32 - 1) as u16; + } + + pub fn select_mode(&mut self, flag: bool) { + if flag { + self.selection = Some(self.cursor); + } else { + self.selection = None; + } + } + + pub fn is_select_mode(&self) -> bool { + self.selection.is_some() + } + + pub fn toggle_select_mode(&mut self) { + self.select_mode(!self.is_select_mode()); + } + + pub fn selection(&self) -> Vec<(u16, u16)> { + let mut selection = Vec::new(); + + if let Some((row, column)) = self.selection_range() { + for i in row.0..=row.1 { + for j in column.0..=column.1 { + selection.push((i, j)) + } + } + } + + selection + } + + fn is_in_selection(&mut self, row: u16, column: u16) -> bool { + if let Some((row_range, column_range)) = self.selection_range() { + row >= row_range.0 + && row <= row_range.1 + && column >= column_range.0 + && column <= column_range.1 + } else { + false + } + } + + fn selection_range(&self) -> Option<((u16, u16), (u16, u16))> { + if let Some(selection) = self.selection { + let row = if selection.0 > self.cursor.0 { + (self.cursor.0, selection.0) + } else { + (selection.0, self.cursor.0) + }; + + let column = if selection.1 > self.cursor.1 { + (self.cursor.1, selection.1) + } else { + (selection.1, self.cursor.1) + }; + + Some((row, column)) + } else { + None + } + } + + fn join_process_handle_on_finished(&mut self) { + if let Some(handle) = self.process_handle.take() { + if handle.is_finished() { + self.error_window = handle.join().unwrap(); + self.process_handle = None; + } else { + self.process_handle = Some(handle); + } + } + } + + pub fn handle_key_event(&mut self, event: KeyEvent) { + if self.process_handle.is_some() { + self.join_process_handle_on_finished() + } else if let Some(_) = &self.error_window { + self.error_window = None; + } else if let Some(textarea) = &mut self.luaeditor { + match event.code { + KeyCode::Char('r') if event.modifiers == KeyModifiers::CONTROL => { + let script = textarea.text(); + *self.process_progress.lock().unwrap() = 0; + + let lua = crate::lua::new_instance().unwrap(); + let result = lua + .load(script.clone()) + .set_name("Temp Script") + .eval::<Function>(); + match result { + Ok(_) => { + let (width, height) = { + let lock = Register::get(self.sheet).unwrap(); + let sheet = lock.write().unwrap(); + (sheet.width(), sheet.height()) + }; + + let mut cells = Vec::new(); + + if let Some(_) = self.selection { + cells = self.selection() + } else { + for row in 0..height { + for column in 0..width { + cells.push((row as u16, column as u16)); + } + } + } + + { + let process_progress = Arc::clone(&self.process_progress); + let sheet = self.sheet; + self.process_handle = Some(std::thread::spawn(move || { + let lua = crate::lua::new_instance().unwrap(); + let func = lua + .load(script) + .set_name("Temp Script") + .eval::<Function>() + .unwrap(); + for (i, (row, column)) in cells.iter().enumerate() { + *process_progress.lock().unwrap() = i * 100 / cells.len(); + let cellref = { + let lock = Register::get(sheet).unwrap(); + let sheet = lock.read().unwrap(); + sheet.get_ref(*row as usize, *column as usize) + }; + if let Err(error) = func.call::<_, Cell>(cellref) { + return Some(error.to_string()); + } + } + + None + })); + } + } + Err(error) => { + self.error_window = Some(error.to_string()); + } + } + } + KeyCode::Esc => { + self.luaeditor = None; + } + _ => { + textarea.handle_key_event(event); + } + } + } else { + match event.code { + KeyCode::Char('j') => self.move_cursor_by((1, 0)), + KeyCode::Char('k') => self.move_cursor_by((-1, 0)), + KeyCode::Char('h') => self.move_cursor_by((0, -1)), + KeyCode::Char('l') => self.move_cursor_by((0, 1)), + KeyCode::Char('v') => self.toggle_select_mode(), + KeyCode::Char('s') => { + let editor = LuaEditor::new("function(cell)\n\t\nend") + .block(Some(Block::bordered().title(" Temp Script "))); + self.luaeditor = Some(editor) + } + _ => {} + } + } + } +} + +impl Widget for &mut SheetView<'_> { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + let lock = Register::get(self.sheet).unwrap(); + let sheet = lock.read().unwrap(); + + let splits = Layout::horizontal([Constraint::Min(1), Constraint::Length(50)]).split(area); + let block_area = if self.luaeditor.is_some() { + splits[0] + } else { + area + }; + + let sheet_area = self.block.inner_if_some(block_area); + + let viewport_rows = sheet.height().min(sheet_area.height as usize); + let viewport_columns = sheet + .width() + .min((sheet_area.width / DEFAULT_COLUMN_WIDTH) as usize); + + if self.cursor.0 >= viewport_rows as u16 + self.scroll.0 { + self.scroll.0 = self.cursor.0 - viewport_rows as u16 + 1; + } else if self.cursor.0 < self.scroll.0 { + self.scroll.0 = self.cursor.0; + } + + if self.cursor.1 >= viewport_columns as u16 + self.scroll.1 { + self.scroll.1 = self.cursor.1 - viewport_columns as u16 + 1; + } else if self.cursor.1 < self.scroll.1 { + self.scroll.1 = self.cursor.1; + } + + for row in 0..viewport_rows as u16 { + for column in 0..(viewport_columns + 1) as u16 { + let (cell_pos_y, cell_pos_x) = (row + self.scroll.0, column + self.scroll.1); + + if let Some(cell) = sheet.get_cell(cell_pos_y as usize, cell_pos_x as usize) { + let cell = cell.to_string() + " "; + + let line = if (cell_pos_y, cell_pos_x) == self.cursor { + cell.to_line().bg(Color::Rgb(120, 90, 90)).white() + } else if self.is_in_selection(cell_pos_y, cell_pos_x) { + cell.to_line().bg(Color::Rgb(120, 120, 90)).white() + } else if (row + column) % 2 == 0 { + cell.to_line().bg(Color::Rgb(30, 30, 30)).white() + } else { + cell.to_line().bg(Color::Rgb(50, 50, 50)).white() + }; + + let rect = Rect::new( + sheet_area.x + column * DEFAULT_COLUMN_WIDTH, + sheet_area.y + row, + (sheet_area.width - column * DEFAULT_COLUMN_WIDTH) + .min(DEFAULT_COLUMN_WIDTH), + 1, + ); + + line.render(rect, buf); + } + } + } + + self.block.render(block_area, buf); + + if let Some(textarea) = &mut self.luaeditor { + textarea.render(splits[1], buf) + } + + if let Some(error_msg) = &self.error_window { + let lines = error_msg.lines().collect::<Vec<_>>(); + let height = lines.len() as u16 + 2; + let width = lines.iter().map(|s| s.len()).max().unwrap_or(0) as u16 + 2; + + let centered = Rect::new( + (area.width - width) / 2, + (area.height - height) / 2, + width, + height, + ); + + let block = Block::bordered().red().on_black().bold().title(" Error "); + let inner_centered = block.inner(centered); + Clear::default().render(centered, buf); + block.render(centered, buf); + + for (i, line) in lines.iter().enumerate() { + let line = line.replace('\t', " ").red().on_black().bold(); + line.render(inner_centered.offset(Offset { x: 0, y: i as i32 }), buf); + } + } + + self.join_process_handle_on_finished(); + + if self.process_handle.is_some() { + let height = 3; + let width = area.width / 2; + + let centered = Rect::new( + (area.width - width) / 2, + (area.height - height) / 2, + width, + height, + ); + + let progress = *self.process_progress.lock().unwrap(); + let gauge = Gauge::default() + .block(Block::bordered().title("Progress").on_black()) + .gauge_style( + Style::default() + .fg(Color::White) + .bg(Color::Black) + .add_modifier(Modifier::ITALIC), + ) + .percent(progress as u16); + + Clear::default().render(centered, buf); + gauge.render(centered, buf); + } + } +} |