summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock666
-rw-r--r--Cargo.toml8
-rw-r--r--src/app.rs81
-rw-r--r--src/lua/iobuffer.rs38
-rw-r--r--src/lua/mod.rs80
-rw-r--r--src/main.rs20
-rw-r--r--src/sheet/cell.rs198
-rw-r--r--src/sheet/mod.rs98
-rw-r--r--src/sheet/register.rs60
-rw-r--r--src/tui.rs22
-rw-r--r--src/widgets/luaeditor/buffer.rs134
-rw-r--r--src/widgets/luaeditor/cursor.rs102
-rw-r--r--src/widgets/luaeditor/mod.rs132
-rw-r--r--src/widgets/mod.rs2
-rw-r--r--src/widgets/sheetview.rs360
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(&register[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);
+ }
+ }
+}