From 9d94cb9af43cca3a13526f8c418b86c8a14d75da Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 30 Dec 2023 09:29:33 -0500 Subject: [PATCH] Restart TUI --- Cargo.lock | 568 +++++++++++++++++++++++-- Cargo.toml | 15 +- assets/config.json5 | 14 + src/bin/tui/action.rs | 25 ++ src/bin/tui/app.rs | 196 ++++++--- src/bin/tui/cli.rs | 12 + src/bin/tui/components.rs | 126 ++++++ src/bin/tui/components/fps.rs | 90 ++++ src/bin/tui/components/home.rs | 99 +++++ src/bin/tui/components/list_display.rs | 5 + src/bin/tui/components/map_display.rs | 106 +++++ src/bin/tui/config.rs | 563 ++++++++++++++++++++++++ src/bin/tui/error.rs | 3 - src/bin/tui/interpreter_display.rs | 53 --- src/bin/tui/log.rs | 83 ---- src/bin/tui/main.rs | 85 ++-- src/bin/tui/mode.rs | 7 + src/bin/tui/terminal.rs | 229 ---------- src/bin/tui/{lib.rs => tui.rs} | 90 ++-- src/bin/tui/utils.rs | 169 ++++++++ src/bin/tui/value_display.rs | 52 --- src/built_in_functions/output.rs | 33 +- src/interpret.rs | 5 +- src/value/mod.rs | 6 +- tree-sitter-dust/highlights.scm | 1 + 25 files changed, 2015 insertions(+), 620 deletions(-) create mode 100644 assets/config.json5 create mode 100644 src/bin/tui/action.rs create mode 100644 src/bin/tui/cli.rs create mode 100644 src/bin/tui/components.rs create mode 100644 src/bin/tui/components/fps.rs create mode 100644 src/bin/tui/components/home.rs create mode 100644 src/bin/tui/components/list_display.rs create mode 100644 src/bin/tui/components/map_display.rs create mode 100644 src/bin/tui/config.rs delete mode 100644 src/bin/tui/error.rs delete mode 100644 src/bin/tui/interpreter_display.rs delete mode 100644 src/bin/tui/log.rs create mode 100644 src/bin/tui/mode.rs delete mode 100644 src/bin/tui/terminal.rs rename src/bin/tui/{lib.rs => tui.rs} (75%) create mode 100644 src/bin/tui/utils.rs delete mode 100644 src/bin/tui/value_display.rs diff --git a/Cargo.lock b/Cargo.lock index 0b724f5..c1a7ab7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.7" @@ -88,7 +99,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -98,7 +109,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-trait" +version = "0.1.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", ] [[package]] @@ -122,12 +144,28 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +[[package]] +name = "better-panic" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa9e1d11a268684cbd90ed36370d7577afb6c62d912ddff5c15fc34343e5036" +dependencies = [ + "backtrace", + "console", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -143,6 +181,15 @@ dependencies = [ "serde", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -197,6 +244,9 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", + "unicase", + "unicode-width", ] [[package]] @@ -273,6 +323,37 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "config" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml 0.5.11", + "yaml-rust", +] + +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -289,6 +370,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" version = "0.8.3" @@ -365,6 +455,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "csv" version = "1.3.0" @@ -386,6 +486,33 @@ dependencies = [ "memchr", ] +[[package]] +name = "derive_deref" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdbcee2d9941369faba772587a565f4f534e42cb8d17e5295871de730163b2b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "directories" version = "5.0.1" @@ -404,24 +531,38 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys", + "windows-sys 0.48.0", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "dust-lang" version = "0.4.0" dependencies = [ "ansi_term", + "better-panic", "cc", "clap", "color-eyre", "comfy-table", + "config", "crossterm 0.27.0", "csv", + "derive_deref", "directories", "futures", "git2", + "human-panic", + "json5", "lazy_static", + "libc", + "log", + "pretty_assertions", "rand", "ratatui", "rayon", @@ -429,15 +570,19 @@ dependencies = [ "rustyline", "serde", "serde_json", + "serial_int", "signal-hook", + "strip-ansi-escapes", + "strum 0.25.0", "tokio", "tokio-util", - "toml", + "toml 0.8.8", "tracing", "tracing-error", "tracing-subscriber", "tree-sitter", "tui-textarea", + "tui-tree-widget", ] [[package]] @@ -446,6 +591,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -475,7 +626,7 @@ checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" dependencies = [ "errno-dragonfly", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -522,7 +673,7 @@ checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" dependencies = [ "cfg-if", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -644,6 +795,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -700,6 +861,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.7", +] [[package]] name = "hashbrown" @@ -707,7 +871,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" dependencies = [ - "ahash", + "ahash 0.8.7", "allocator-api2", ] @@ -729,7 +893,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -766,6 +930,22 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "human-panic" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a67745be0cb8dd2771f03b24c2f25df98d5471fe7a595d668cfa2e6f843d" +dependencies = [ + "anstream", + "anstyle", + "backtrace", + "os_info", + "serde", + "serde_derive", + "toml 0.8.8", + "uuid", +] + [[package]] name = "hyper" version = "0.14.27" @@ -884,6 +1064,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -947,6 +1138,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.8" @@ -1008,6 +1205,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1026,7 +1229,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1067,6 +1270,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1152,6 +1365,27 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + +[[package]] +name = "os_info" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +dependencies = [ + "log", + "serde", + "winapi", +] + [[package]] name = "overload" version = "0.1.1" @@ -1184,7 +1418,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1193,12 +1427,63 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "percent-encoding" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "pest" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "pest_meta" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1223,6 +1508,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.67" @@ -1399,7 +1694,7 @@ version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ - "base64", + "base64 0.21.4", "bytes", "encoding_rs", "futures-core", @@ -1431,6 +1726,27 @@ dependencies = [ "winreg", ] +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1447,7 +1763,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1503,7 +1819,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1568,9 +1884,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] @@ -1587,6 +1903,23 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_int" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a6ace048d852c367559b6d6d7c809d1a1812e500537c9083287feecb3519" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1658,7 +1991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1677,6 +2010,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1777,7 +2119,17 @@ dependencies = [ "fastrand", "redox_syscall 0.3.5", "rustix", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", ] [[package]] @@ -1840,7 +2192,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1880,9 +2232,18 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.2" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ "serde", "serde_spanned", @@ -1892,18 +2253,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.20.2" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ "indexmap 2.0.2", "serde", @@ -2017,6 +2378,37 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "tui-tree-widget" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136011b328c4f392499a02c4b5b78d509fb297bf9c10f2bda5d11d65cb946e4c" +dependencies = [ + "ratatui", + "unicode-width", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2067,6 +2459,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom", +] + [[package]] name = "valuable" version = "0.1.0" @@ -2085,6 +2486,26 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "want" version = "0.3.1" @@ -2198,13 +2619,37 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -2213,51 +2658,93 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.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_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[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_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2280,9 +2767,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index 6186493..1ca530a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ version = "0.4.0" repository = "https://git.jeffa.io/jeff/dust.git" edition = "2021" license = "MIT" +authors = ["Jeff Anderson"] [[bin]] name = "dust" @@ -20,15 +21,23 @@ opt-level = 3 [dependencies] ansi_term = "0.12.1" -clap = { version = "4.4.4", features = ["derive"] } +better-panic = "0.3.0" +clap = { version = "4.4.4", features = ["derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] } color-eyre = "0.6.2" comfy-table = "7.0.1" +config = "0.13.3" crossterm = { version = "0.27.0", features = ["serde", "event-stream"] } csv = "1.2.2" +derive_deref = "1.1.1" directories = "5.0.1" futures = "0.3.30" git2 = "0.18.1" +human-panic = "1.2.0" +json5 = "0.4.1" lazy_static = "1.4.0" +libc = "0.2.148" +log = "0.4.20" +pretty_assertions = "1.4.0" rand = "0.8.5" ratatui = "0.25.0" rayon = "1.8.0" @@ -36,7 +45,10 @@ reqwest = { version = "0.11.20", features = ["blocking", "json"] } rustyline = { version = "12.0.0", features = ["derive", "with-file-history"] } serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" +serial_int = "2.0.0" signal-hook = "0.3.17" +strip-ansi-escapes = "0.2.0" +strum = { version = "0.25.0", features = ["derive"] } tokio = { version = "1.35.1", features = ["signal-hook-registry", "macros", "signal", "rt", "time", "rt-multi-thread"] } tokio-util = "0.7.10" toml = "0.8.1" @@ -45,6 +57,7 @@ tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tree-sitter = "0.20.10" tui-textarea = { version = "0.4.0", features = ["search"] } +tui-tree-widget = "0.16.0" [build-dependencies] cc = "1.0" diff --git a/assets/config.json5 b/assets/config.json5 new file mode 100644 index 0000000..2157fba --- /dev/null +++ b/assets/config.json5 @@ -0,0 +1,14 @@ +{ + "keybindings": { + "Home": { + "": "Quit", // Quit the application + "": "Quit", // Another way to quit + "": "Quit", // Yet another way to quit + "": "Suspend", // Suspend the application + "": "Up", + "": "Down", + "": "Left", + "": "Right", + }, + } +} diff --git a/src/bin/tui/action.rs b/src/bin/tui/action.rs new file mode 100644 index 0000000..051af6a --- /dev/null +++ b/src/bin/tui/action.rs @@ -0,0 +1,25 @@ +use std::{fmt, string::ToString}; + +use serde::{ + de::{self, Deserializer, Visitor}, + Deserialize, Serialize, +}; +use strum::Display; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Display, Deserialize)] +pub enum Action { + Tick, + Render, + ReadFile, + Resize(u16, u16), + Suspend, + Resume, + Quit, + Refresh, + Error(String), + Help, + Up, + Down, + Left, + Right, +} diff --git a/src/bin/tui/app.rs b/src/bin/tui/app.rs index d614ce1..36c3058 100644 --- a/src/bin/tui/app.rs +++ b/src/bin/tui/app.rs @@ -1,88 +1,152 @@ use std::path::PathBuf; -use crossterm::event::KeyCode; -use dust_lang::Map; -use ratatui::Frame; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use color_eyre::eyre::Result; +use crossterm::event::KeyEvent; +use ratatui::prelude::Rect; +use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc; -use crate::{interpreter_display::InterpreterDisplay, terminal::Terminal, Action, Elm, Result}; +use crate::{ + action::Action, + components::{fps::FpsCounter, home::Home, Component}, + config::Config, + mode::Mode, + tui, +}; pub struct App { - action_rx: UnboundedReceiver, - action_tx: UnboundedSender, - interpreter_display: InterpreterDisplay, - should_quit: bool, + pub config: Config, + pub components: Vec>, + pub should_quit: bool, + pub should_suspend: bool, + pub mode: Mode, + pub last_tick_key_events: Vec, } impl App { - pub fn new( - action_rx: UnboundedReceiver, - action_tx: UnboundedSender, - path: PathBuf, - ) -> Result { - let interpreter_display = InterpreterDisplay::new(Map::new(), path)?; - - Ok(App { - action_rx, - action_tx, - interpreter_display, + pub fn new(path: PathBuf) -> Result { + let home = Home::new(path)?; + let config = Config::new()?; + let mode = Mode::Home; + Ok(Self { + components: vec![Box::new(home)], should_quit: false, + should_suspend: false, + config, + mode, + last_tick_key_events: Vec::new(), }) } pub async fn run(&mut self) -> Result<()> { - let mut terminal = Terminal::new()?.tick_rate(4.0).frame_rate(30.0); + let (action_tx, mut action_rx) = mpsc::unbounded_channel(); - terminal.enter()?; + let mut tui = tui::Tui::new()?.mouse(true); - loop { - if self.should_quit { - break; - } + tui.enter()?; - if let Some(action) = terminal.next().await { - self.action_tx.send(action)?; - } else { - continue; - }; - - while let Ok(action) = self.action_rx.try_recv() { - if let Action::Render = action { - terminal.draw(|frame| { - self.view(frame); - })?; - } - - let next_action = self.update(action)?; - - if let Some(action) = next_action { - self.action_tx.send(action)?; - } - } + for component in self.components.iter_mut() { + component.register_action_handler(action_tx.clone())?; } - terminal.exit()?; + for component in self.components.iter_mut() { + component.register_config_handler(self.config.clone())?; + } + for component in self.components.iter_mut() { + component.init(tui.size()?)?; + } + + loop { + if let Some(e) = tui.next().await { + match e { + tui::Event::Quit => action_tx.send(Action::Quit)?, + tui::Event::Tick => action_tx.send(Action::Tick)?, + tui::Event::Render => action_tx.send(Action::Render)?, + tui::Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?, + tui::Event::Key(key) => { + if let Some(keymap) = self.config.keybindings.get(&self.mode) { + if let Some(action) = keymap.get(&vec![key]) { + log::info!("Got action: {action:?}"); + action_tx.send(action.clone())?; + } else { + // If the key was not handled as a single key action, + // then consider it for multi-key combinations. + self.last_tick_key_events.push(key); + + // Check for multi-key combinations + if let Some(action) = keymap.get(&self.last_tick_key_events) { + log::info!("Got action: {action:?}"); + action_tx.send(action.clone())?; + } + } + }; + } + _ => {} + } + for component in self.components.iter_mut() { + if let Some(action) = component.handle_events(Some(e.clone()))? { + action_tx.send(action)?; + } + } + } + + while let Ok(action) = action_rx.try_recv() { + if action != Action::Tick && action != Action::Render { + log::debug!("{action:?}"); + } + match action { + Action::Tick => { + self.last_tick_key_events.drain(..); + } + Action::Quit => self.should_quit = true, + Action::Suspend => self.should_suspend = true, + Action::Resume => self.should_suspend = false, + Action::Resize(w, h) => { + tui.resize(Rect::new(0, 0, w, h))?; + tui.draw(|f| { + for component in self.components.iter_mut() { + let r = component.draw(f, f.size()); + if let Err(e) = r { + action_tx + .send(Action::Error(format!("Failed to draw: {:?}", e))) + .unwrap(); + } + } + })?; + } + Action::Render => { + tui.draw(|f| { + for component in self.components.iter_mut() { + let r = component.draw(f, f.size()); + if let Err(e) = r { + action_tx + .send(Action::Error(format!("Failed to draw: {:?}", e))) + .unwrap(); + } + } + })?; + } + _ => {} + } + for component in self.components.iter_mut() { + if let Some(action) = component.update(action.clone())? { + action_tx.send(action)? + }; + } + } + if self.should_suspend { + tui.suspend()?; + action_tx.send(Action::Resume)?; + tui = tui::Tui::new()?; + // tui.mouse(true); + tui.enter()?; + } else if self.should_quit { + tui.stop()?; + break; + } + } + tui.exit()?; Ok(()) } } - -impl Elm for App { - fn update(&mut self, message: Action) -> Result> { - match message { - Action::Quit => self.should_quit = true, - Action::Key(key_event) => { - if let KeyCode::Esc = key_event.code { - return Ok(Some(Action::Quit)); - } - } - _ => {} - } - - self.interpreter_display.update(message) - } - - fn view(&self, frame: &mut Frame) { - self.interpreter_display.view(frame) - } -} diff --git a/src/bin/tui/cli.rs b/src/bin/tui/cli.rs new file mode 100644 index 0000000..1a54c29 --- /dev/null +++ b/src/bin/tui/cli.rs @@ -0,0 +1,12 @@ +use std::path::PathBuf; + +use clap::Parser; + +use crate::utils::version; + +#[derive(Parser, Debug)] +#[command(author, version = version(), about)] +pub struct Cli { + /// File that will be run and watched for changes. + pub path: PathBuf, +} diff --git a/src/bin/tui/components.rs b/src/bin/tui/components.rs new file mode 100644 index 0000000..2fbb868 --- /dev/null +++ b/src/bin/tui/components.rs @@ -0,0 +1,126 @@ +use color_eyre::eyre::Result; +use crossterm::event::{KeyEvent, MouseEvent}; +use ratatui::layout::Rect; +use tokio::sync::mpsc::UnboundedSender; + +use crate::{ + action::Action, + config::Config, + tui::{Event, Frame}, +}; + +pub mod fps; +pub mod home; +mod list_display; +mod map_display; + +/// `Component` is a trait that represents a visual and interactive element of the user interface. +/// Implementors of this trait can be registered with the main application loop and will be able to receive events, +/// update state, and be rendered on the screen. +pub trait Component { + /// Register an action handler that can send actions for processing if necessary. + /// + /// # Arguments + /// + /// * `tx` - An unbounded sender that can send actions. + /// + /// # Returns + /// + /// * `Result<()>` - An Ok result or an error. + #[allow(unused_variables)] + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + Ok(()) + } + /// Register a configuration handler that provides configuration settings if necessary. + /// + /// # Arguments + /// + /// * `config` - Configuration settings. + /// + /// # Returns + /// + /// * `Result<()>` - An Ok result or an error. + #[allow(unused_variables)] + fn register_config_handler(&mut self, config: Config) -> Result<()> { + Ok(()) + } + /// Initialize the component with a specified area if necessary. + /// + /// # Arguments + /// + /// * `area` - Rectangular area to initialize the component within. + /// + /// # Returns + /// + /// * `Result<()>` - An Ok result or an error. + fn init(&mut self, area: Rect) -> Result<()> { + Ok(()) + } + /// Handle incoming events and produce actions if necessary. + /// + /// # Arguments + /// + /// * `event` - An optional event to be processed. + /// + /// # Returns + /// + /// * `Result>` - An action to be processed or none. + fn handle_events(&mut self, event: Option) -> Result> { + let r = match event { + Some(Event::Key(key_event)) => self.handle_key_events(key_event)?, + Some(Event::Mouse(mouse_event)) => self.handle_mouse_events(mouse_event)?, + _ => None, + }; + Ok(r) + } + /// Handle key events and produce actions if necessary. + /// + /// # Arguments + /// + /// * `key` - A key event to be processed. + /// + /// # Returns + /// + /// * `Result>` - An action to be processed or none. + #[allow(unused_variables)] + fn handle_key_events(&mut self, key: KeyEvent) -> Result> { + Ok(None) + } + /// Handle mouse events and produce actions if necessary. + /// + /// # Arguments + /// + /// * `mouse` - A mouse event to be processed. + /// + /// # Returns + /// + /// * `Result>` - An action to be processed or none. + #[allow(unused_variables)] + fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result> { + Ok(None) + } + /// Update the state of the component based on a received action. (REQUIRED) + /// + /// # Arguments + /// + /// * `action` - An action that may modify the state of the component. + /// + /// # Returns + /// + /// * `Result>` - An action to be processed or none. + #[allow(unused_variables)] + fn update(&mut self, action: Action) -> Result> { + Ok(None) + } + /// Render the component on the screen. (REQUIRED) + /// + /// # Arguments + /// + /// * `f` - A frame used for rendering. + /// * `area` - The area in which the component should be drawn. + /// + /// # Returns + /// + /// * `Result<()>` - An Ok result or an error. + fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()>; +} diff --git a/src/bin/tui/components/fps.rs b/src/bin/tui/components/fps.rs new file mode 100644 index 0000000..7c79805 --- /dev/null +++ b/src/bin/tui/components/fps.rs @@ -0,0 +1,90 @@ +use std::time::Instant; + +use color_eyre::eyre::Result; +use ratatui::{prelude::*, widgets::*}; + +use super::Component; +use crate::{action::Action, tui::Frame}; + +#[derive(Debug, Clone, PartialEq)] +pub struct FpsCounter { + app_start_time: Instant, + app_frames: u32, + app_fps: f64, + + render_start_time: Instant, + render_frames: u32, + render_fps: f64, +} + +impl Default for FpsCounter { + fn default() -> Self { + Self::new() + } +} + +impl FpsCounter { + pub fn new() -> Self { + Self { + app_start_time: Instant::now(), + app_frames: 0, + app_fps: 0.0, + render_start_time: Instant::now(), + render_frames: 0, + render_fps: 0.0, + } + } + + fn app_tick(&mut self) -> Result<()> { + self.app_frames += 1; + let now = Instant::now(); + let elapsed = (now - self.app_start_time).as_secs_f64(); + if elapsed >= 1.0 { + self.app_fps = self.app_frames as f64 / elapsed; + self.app_start_time = now; + self.app_frames = 0; + } + Ok(()) + } + + fn render_tick(&mut self) -> Result<()> { + self.render_frames += 1; + let now = Instant::now(); + let elapsed = (now - self.render_start_time).as_secs_f64(); + if elapsed >= 1.0 { + self.render_fps = self.render_frames as f64 / elapsed; + self.render_start_time = now; + self.render_frames = 0; + } + Ok(()) + } +} + +impl Component for FpsCounter { + fn update(&mut self, action: Action) -> Result> { + if let Action::Tick = action { + self.app_tick()? + }; + if let Action::Render = action { + self.render_tick()? + }; + Ok(None) + } + + fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()> { + let rects = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Length(1), // first row + Constraint::Min(0), + ]) + .split(rect); + + let rect = rects[0]; + + let s = format!("{:.2} ticks per sec (app) {:.2} frames per sec (render)", self.app_fps, self.render_fps); + let block = Block::default().title(block::Title::from(s.dim()).alignment(Alignment::Right)); + f.render_widget(block, rect); + Ok(()) + } +} diff --git a/src/bin/tui/components/home.rs b/src/bin/tui/components/home.rs new file mode 100644 index 0000000..082fee7 --- /dev/null +++ b/src/bin/tui/components/home.rs @@ -0,0 +1,99 @@ +use std::{ + collections::HashMap, + fs::read_to_string, + path::PathBuf, + time::{Duration, SystemTime}, +}; + +use color_eyre::eyre::Result; +use crossterm::event::{KeyCode, KeyEvent}; +use dust_lang::{interpret, interpret_with_context, Map, Value}; +use ratatui::{prelude::*, widgets::*}; +use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc::UnboundedSender; +use tui_tree_widget::{Tree, TreeItem, TreeState}; + +use super::{map_display::MapDisplay, Component, Frame}; +use crate::{ + action::Action, + config::{Config, KeyBindings}, +}; + +pub struct Home<'a> { + command_tx: Option>, + config: Config, + path: PathBuf, + source: String, + context: Map, + context_tree_state: TreeState, + context_display: MapDisplay<'a>, + output: dust_lang::Result, + last_modified: SystemTime, +} + +impl<'a> Home<'a> { + pub fn new(path: PathBuf) -> Result { + let context = Map::new(); + let context_display = MapDisplay::new(context.clone())?; + + Ok(Home { + command_tx: None, + config: Config::default(), + path, + source: String::new(), + context, + context_display, + context_tree_state: TreeState::default(), + output: Ok(Value::default()), + last_modified: SystemTime::now(), + }) + } +} + +impl<'a> Component for Home<'a> { + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.command_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + self.config = config; + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::Tick => { + let modified = self.path.metadata()?.modified()?; + + if modified != self.last_modified { + self.source = read_to_string(&self.path)?; + self.last_modified = modified; + self.output = interpret_with_context(&self.source, self.context.clone()); + self.context_display = MapDisplay::new(self.context.clone())?; + } + } + _ => {} + } + + self.context_display.update(action) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(area); + + self.context_display.draw(frame, layout[0])?; + + let output_text = match &self.output { + Ok(value) => value.to_string(), + Err(error) => error.to_string(), + }; + + frame.render_widget(Paragraph::new(output_text), layout[1]); + + Ok(()) + } +} diff --git a/src/bin/tui/components/list_display.rs b/src/bin/tui/components/list_display.rs new file mode 100644 index 0000000..29c3592 --- /dev/null +++ b/src/bin/tui/components/list_display.rs @@ -0,0 +1,5 @@ +use dust_lang::List; + +pub struct ListDisplay { + list: List, +} diff --git a/src/bin/tui/components/map_display.rs b/src/bin/tui/components/map_display.rs new file mode 100644 index 0000000..8024438 --- /dev/null +++ b/src/bin/tui/components/map_display.rs @@ -0,0 +1,106 @@ +use color_eyre::Result; +use dust_lang::{Map, Type, Value}; +use lazy_static::lazy_static; +use ratatui::{ + prelude::Rect, + style::{Color, Modifier, Style, Stylize}, + widgets::{Block, Borders}, + Frame, +}; +use serial_int::SerialGenerator; +use std::{hash::Hash, sync::Mutex}; +use tui_tree_widget::{Tree, TreeItem, TreeState}; + +use crate::{action::Action, components::Component}; + +lazy_static! { + static ref ID_GENERATOR: Mutex> = Mutex::new(SerialGenerator::new()); +} + +fn create_tree_item<'a>(key: String, value: &Value) -> Result> { + let tree_item = match value { + Value::List(list) => { + let mut items = Vec::new(); + + for (index, value) in list.items().iter().enumerate() { + let item = create_tree_item(index.to_string(), value)?; + + items.push(item); + } + + TreeItem::new(ID_GENERATOR.lock().unwrap().generate(), key, items)? + } + Value::Map(map) => { + let mut items = Vec::new(); + + for (key, (value, _)) in map.variables()?.iter() { + let item = create_tree_item(key.to_string(), value)?; + + items.push(item); + } + + TreeItem::new(ID_GENERATOR.lock().unwrap().generate(), key, items)? + } + Value::Function(_) => todo!(), + Value::String(string) => TreeItem::new_leaf( + ID_GENERATOR.lock().unwrap().generate(), + format!("{key} = {value}"), + ), + Value::Float(float) => TreeItem::new_leaf( + ID_GENERATOR.lock().unwrap().generate(), + format!("{key} = {value}"), + ), + Value::Integer(integer) => TreeItem::new_leaf( + ID_GENERATOR.lock().unwrap().generate(), + format!("{key} = {value}"), + ), + Value::Boolean(_) => todo!(), + Value::Option(_) => todo!(), + }; + + Ok(tree_item) +} + +pub struct MapDisplay<'a> { + tree_state: TreeState, + items: Vec>, +} + +impl<'a> MapDisplay<'a> { + pub fn new(map: Map) -> Result { + let tree_state = TreeState::default(); + let mut items = Vec::new(); + + for (key, (value, _)) in map.variables()?.iter() { + let item = create_tree_item(key.to_string(), value)?; + + items.push(item); + } + + Ok(MapDisplay { tree_state, items }) + } +} + +impl<'a> Component for MapDisplay<'a> { + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let tree = Tree::new(self.items.clone())? + .block(Block::new().title("context").borders(Borders::ALL)) + .highlight_style(Style::new().add_modifier(Modifier::BOLD)); + + frame.render_stateful_widget(tree, area, &mut self.tree_state); + + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::Up => self.tree_state.key_up(self.items.as_slice()), + Action::Down => self.tree_state.key_down(&self.items), + Action::Left => self.tree_state.key_left(), + Action::Right => self.tree_state.key_right(), + _ => {} + } + + Ok(None) + } +} diff --git a/src/bin/tui/config.rs b/src/bin/tui/config.rs new file mode 100644 index 0000000..44d91d9 --- /dev/null +++ b/src/bin/tui/config.rs @@ -0,0 +1,563 @@ +use std::{collections::HashMap, fmt, path::PathBuf}; + +use color_eyre::eyre::Result; +use config::Value; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use derive_deref::{Deref, DerefMut}; +use ratatui::style::{Color, Modifier, Style}; +use serde::{ + de::{self, Deserializer, MapAccess, Visitor}, + Deserialize, Serialize, +}; +use serde_json::Value as JsonValue; + +use crate::{action::Action, mode::Mode}; + +const CONFIG: &str = include_str!("../../../assets/config.json5"); + +#[derive(Clone, Debug, Deserialize, Default)] +pub struct AppConfig { + #[serde(default)] + pub _data_dir: PathBuf, + #[serde(default)] + pub _config_dir: PathBuf, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct Config { + #[serde(default, flatten)] + pub config: AppConfig, + #[serde(default)] + pub keybindings: KeyBindings, + #[serde(default)] + pub styles: Styles, +} + +impl Config { + pub fn new() -> Result { + let default_config: Config = json5::from_str(CONFIG).unwrap(); + let data_dir = crate::utils::get_data_dir(); + let config_dir = crate::utils::get_config_dir(); + let mut builder = config::Config::builder() + .set_default("_data_dir", data_dir.to_str().unwrap())? + .set_default("_config_dir", config_dir.to_str().unwrap())?; + + let config_files = [ + ("config.json5", config::FileFormat::Json5), + ("config.json", config::FileFormat::Json), + ("config.yaml", config::FileFormat::Yaml), + ("config.toml", config::FileFormat::Toml), + ("config.ini", config::FileFormat::Ini), + ]; + let mut found_config = false; + for (file, format) in &config_files { + builder = builder.add_source( + config::File::from(config_dir.join(file)) + .format(*format) + .required(false), + ); + if config_dir.join(file).exists() { + found_config = true + } + } + if !found_config { + log::error!("No configuration file found. Application may not behave as expected"); + } + + let mut cfg: Self = builder.build()?.try_deserialize()?; + + for (mode, default_bindings) in default_config.keybindings.iter() { + let user_bindings = cfg.keybindings.entry(*mode).or_default(); + for (key, cmd) in default_bindings.iter() { + user_bindings + .entry(key.clone()) + .or_insert_with(|| cmd.clone()); + } + } + for (mode, default_styles) in default_config.styles.iter() { + let user_styles = cfg.styles.entry(*mode).or_default(); + for (style_key, style) in default_styles.iter() { + user_styles + .entry(style_key.clone()) + .or_insert_with(|| style.clone()); + } + } + + Ok(cfg) + } +} + +#[derive(Clone, Debug, Default, Deref, DerefMut)] +pub struct KeyBindings(pub HashMap, Action>>); + +impl<'de> Deserialize<'de> for KeyBindings { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let parsed_map = HashMap::>::deserialize(deserializer)?; + + let keybindings = parsed_map + .into_iter() + .map(|(mode, inner_map)| { + let converted_inner_map = inner_map + .into_iter() + .map(|(key_str, cmd)| (parse_key_sequence(&key_str).unwrap(), cmd)) + .collect(); + (mode, converted_inner_map) + }) + .collect(); + + Ok(KeyBindings(keybindings)) + } +} + +fn parse_key_event(raw: &str) -> Result { + let raw_lower = raw.to_ascii_lowercase(); + let (remaining, modifiers) = extract_modifiers(&raw_lower); + parse_key_code_with_modifiers(remaining, modifiers) +} + +fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) { + let mut modifiers = KeyModifiers::empty(); + let mut current = raw; + + loop { + match current { + rest if rest.starts_with("ctrl-") => { + modifiers.insert(KeyModifiers::CONTROL); + current = &rest[5..]; + } + rest if rest.starts_with("alt-") => { + modifiers.insert(KeyModifiers::ALT); + current = &rest[4..]; + } + rest if rest.starts_with("shift-") => { + modifiers.insert(KeyModifiers::SHIFT); + current = &rest[6..]; + } + _ => break, // break out of the loop if no known prefix is detected + }; + } + + (current, modifiers) +} + +fn parse_key_code_with_modifiers( + raw: &str, + mut modifiers: KeyModifiers, +) -> Result { + let c = match raw { + "esc" => KeyCode::Esc, + "enter" => KeyCode::Enter, + "left" => KeyCode::Left, + "right" => KeyCode::Right, + "up" => KeyCode::Up, + "down" => KeyCode::Down, + "home" => KeyCode::Home, + "end" => KeyCode::End, + "pageup" => KeyCode::PageUp, + "pagedown" => KeyCode::PageDown, + "backtab" => { + modifiers.insert(KeyModifiers::SHIFT); + KeyCode::BackTab + } + "backspace" => KeyCode::Backspace, + "delete" => KeyCode::Delete, + "insert" => KeyCode::Insert, + "f1" => KeyCode::F(1), + "f2" => KeyCode::F(2), + "f3" => KeyCode::F(3), + "f4" => KeyCode::F(4), + "f5" => KeyCode::F(5), + "f6" => KeyCode::F(6), + "f7" => KeyCode::F(7), + "f8" => KeyCode::F(8), + "f9" => KeyCode::F(9), + "f10" => KeyCode::F(10), + "f11" => KeyCode::F(11), + "f12" => KeyCode::F(12), + "space" => KeyCode::Char(' '), + "hyphen" => KeyCode::Char('-'), + "minus" => KeyCode::Char('-'), + "tab" => KeyCode::Tab, + c if c.len() == 1 => { + let mut c = c.chars().next().unwrap(); + if modifiers.contains(KeyModifiers::SHIFT) { + c = c.to_ascii_uppercase(); + } + KeyCode::Char(c) + } + _ => return Err(format!("Unable to parse {raw}")), + }; + Ok(KeyEvent::new(c, modifiers)) +} + +pub fn key_event_to_string(key_event: &KeyEvent) -> String { + let char; + let key_code = match key_event.code { + KeyCode::Backspace => "backspace", + KeyCode::Enter => "enter", + KeyCode::Left => "left", + KeyCode::Right => "right", + KeyCode::Up => "up", + KeyCode::Down => "down", + KeyCode::Home => "home", + KeyCode::End => "end", + KeyCode::PageUp => "pageup", + KeyCode::PageDown => "pagedown", + KeyCode::Tab => "tab", + KeyCode::BackTab => "backtab", + KeyCode::Delete => "delete", + KeyCode::Insert => "insert", + KeyCode::F(c) => { + char = format!("f({c})"); + &char + } + KeyCode::Char(c) if c == ' ' => "space", + KeyCode::Char(c) => { + char = c.to_string(); + &char + } + KeyCode::Esc => "esc", + KeyCode::Null => "", + KeyCode::CapsLock => "", + KeyCode::Menu => "", + KeyCode::ScrollLock => "", + KeyCode::Media(_) => "", + KeyCode::NumLock => "", + KeyCode::PrintScreen => "", + KeyCode::Pause => "", + KeyCode::KeypadBegin => "", + KeyCode::Modifier(_) => "", + }; + + let mut modifiers = Vec::with_capacity(3); + + if key_event.modifiers.intersects(KeyModifiers::CONTROL) { + modifiers.push("ctrl"); + } + + if key_event.modifiers.intersects(KeyModifiers::SHIFT) { + modifiers.push("shift"); + } + + if key_event.modifiers.intersects(KeyModifiers::ALT) { + modifiers.push("alt"); + } + + let mut key = modifiers.join("-"); + + if !key.is_empty() { + key.push('-'); + } + key.push_str(key_code); + + key +} + +pub fn parse_key_sequence(raw: &str) -> Result, String> { + if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() { + return Err(format!("Unable to parse `{}`", raw)); + } + let raw = if !raw.contains("><") { + let raw = raw.strip_prefix('<').unwrap_or(raw); + let raw = raw.strip_prefix('>').unwrap_or(raw); + raw + } else { + raw + }; + let sequences = raw + .split("><") + .map(|seq| { + if let Some(s) = seq.strip_prefix('<') { + s + } else if let Some(s) = seq.strip_suffix('>') { + s + } else { + seq + } + }) + .collect::>(); + + sequences.into_iter().map(parse_key_event).collect() +} + +#[derive(Clone, Debug, Default, Deref, DerefMut)] +pub struct Styles(pub HashMap>); + +impl<'de> Deserialize<'de> for Styles { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let parsed_map = HashMap::>::deserialize(deserializer)?; + + let styles = parsed_map + .into_iter() + .map(|(mode, inner_map)| { + let converted_inner_map = inner_map + .into_iter() + .map(|(str, style)| (str, parse_style(&style))) + .collect(); + (mode, converted_inner_map) + }) + .collect(); + + Ok(Styles(styles)) + } +} + +pub fn parse_style(line: &str) -> Style { + let (foreground, background) = + line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len())); + let foreground = process_color_string(foreground); + let background = process_color_string(&background.replace("on ", "")); + + let mut style = Style::default(); + if let Some(fg) = parse_color(&foreground.0) { + style = style.fg(fg); + } + if let Some(bg) = parse_color(&background.0) { + style = style.bg(bg); + } + style = style.add_modifier(foreground.1 | background.1); + style +} + +fn process_color_string(color_str: &str) -> (String, Modifier) { + let color = color_str + .replace("grey", "gray") + .replace("bright ", "") + .replace("bold ", "") + .replace("underline ", "") + .replace("inverse ", ""); + + let mut modifiers = Modifier::empty(); + if color_str.contains("underline") { + modifiers |= Modifier::UNDERLINED; + } + if color_str.contains("bold") { + modifiers |= Modifier::BOLD; + } + if color_str.contains("inverse") { + modifiers |= Modifier::REVERSED; + } + + (color, modifiers) +} + +fn parse_color(s: &str) -> Option { + let s = s.trim_start(); + let s = s.trim_end(); + if s.contains("bright color") { + let s = s.trim_start_matches("bright "); + let c = s + .trim_start_matches("color") + .parse::() + .unwrap_or_default(); + Some(Color::Indexed(c.wrapping_shl(8))) + } else if s.contains("color") { + let c = s + .trim_start_matches("color") + .parse::() + .unwrap_or_default(); + Some(Color::Indexed(c)) + } else if s.contains("gray") { + let c = 232 + + s.trim_start_matches("gray") + .parse::() + .unwrap_or_default(); + Some(Color::Indexed(c)) + } else if s.contains("rgb") { + let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8; + let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8; + let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8; + let c = 16 + red * 36 + green * 6 + blue; + Some(Color::Indexed(c)) + } else if s == "bold black" { + Some(Color::Indexed(8)) + } else if s == "bold red" { + Some(Color::Indexed(9)) + } else if s == "bold green" { + Some(Color::Indexed(10)) + } else if s == "bold yellow" { + Some(Color::Indexed(11)) + } else if s == "bold blue" { + Some(Color::Indexed(12)) + } else if s == "bold magenta" { + Some(Color::Indexed(13)) + } else if s == "bold cyan" { + Some(Color::Indexed(14)) + } else if s == "bold white" { + Some(Color::Indexed(15)) + } else if s == "black" { + Some(Color::Indexed(0)) + } else if s == "red" { + Some(Color::Indexed(1)) + } else if s == "green" { + Some(Color::Indexed(2)) + } else if s == "yellow" { + Some(Color::Indexed(3)) + } else if s == "blue" { + Some(Color::Indexed(4)) + } else if s == "magenta" { + Some(Color::Indexed(5)) + } else if s == "cyan" { + Some(Color::Indexed(6)) + } else if s == "white" { + Some(Color::Indexed(7)) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_parse_style_default() { + let style = parse_style(""); + assert_eq!(style, Style::default()); + } + + #[test] + fn test_parse_style_foreground() { + let style = parse_style("red"); + assert_eq!(style.fg, Some(Color::Indexed(1))); + } + + #[test] + fn test_parse_style_background() { + let style = parse_style("on blue"); + assert_eq!(style.bg, Some(Color::Indexed(4))); + } + + #[test] + fn test_parse_style_modifiers() { + let style = parse_style("underline red on blue"); + assert_eq!(style.fg, Some(Color::Indexed(1))); + assert_eq!(style.bg, Some(Color::Indexed(4))); + } + + #[test] + fn test_process_color_string() { + let (color, modifiers) = process_color_string("underline bold inverse gray"); + assert_eq!(color, "gray"); + assert!(modifiers.contains(Modifier::UNDERLINED)); + assert!(modifiers.contains(Modifier::BOLD)); + assert!(modifiers.contains(Modifier::REVERSED)); + } + + #[test] + fn test_parse_color_rgb() { + let color = parse_color("rgb123"); + let expected = 16 + 1 * 36 + 2 * 6 + 3; + assert_eq!(color, Some(Color::Indexed(expected))); + } + + #[test] + fn test_parse_color_unknown() { + let color = parse_color("unknown"); + assert_eq!(color, None); + } + + #[test] + fn test_config() -> Result<()> { + let c = Config::new()?; + assert_eq!( + c.keybindings + .get(&Mode::Home) + .unwrap() + .get(&parse_key_sequence("").unwrap_or_default()) + .unwrap(), + &Action::Quit + ); + Ok(()) + } + + #[test] + fn test_simple_keys() { + assert_eq!( + parse_key_event("a").unwrap(), + KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()) + ); + + assert_eq!( + parse_key_event("enter").unwrap(), + KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()) + ); + + assert_eq!( + parse_key_event("esc").unwrap(), + KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()) + ); + } + + #[test] + fn test_with_modifiers() { + assert_eq!( + parse_key_event("ctrl-a").unwrap(), + KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL) + ); + + assert_eq!( + parse_key_event("alt-enter").unwrap(), + KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT) + ); + + assert_eq!( + parse_key_event("shift-esc").unwrap(), + KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT) + ); + } + + #[test] + fn test_multiple_modifiers() { + assert_eq!( + parse_key_event("ctrl-alt-a").unwrap(), + KeyEvent::new( + KeyCode::Char('a'), + KeyModifiers::CONTROL | KeyModifiers::ALT + ) + ); + + assert_eq!( + parse_key_event("ctrl-shift-enter").unwrap(), + KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT) + ); + } + + #[test] + fn test_reverse_multiple_modifiers() { + assert_eq!( + key_event_to_string(&KeyEvent::new( + KeyCode::Char('a'), + KeyModifiers::CONTROL | KeyModifiers::ALT + )), + "ctrl-alt-a".to_string() + ); + } + + #[test] + fn test_invalid_keys() { + assert!(parse_key_event("invalid-key").is_err()); + assert!(parse_key_event("ctrl-invalid-key").is_err()); + } + + #[test] + fn test_case_insensitivity() { + assert_eq!( + parse_key_event("CTRL-a").unwrap(), + KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL) + ); + + assert_eq!( + parse_key_event("AlT-eNtEr").unwrap(), + KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT) + ); + } +} diff --git a/src/bin/tui/error.rs b/src/bin/tui/error.rs deleted file mode 100644 index f1dd913..0000000 --- a/src/bin/tui/error.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub type Result = std::result::Result; - -pub enum Error {} diff --git a/src/bin/tui/interpreter_display.rs b/src/bin/tui/interpreter_display.rs deleted file mode 100644 index 8ac54e5..0000000 --- a/src/bin/tui/interpreter_display.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::{fs::read_to_string, path::PathBuf, time::SystemTime}; - -use dust_lang::{Interpreter, Map, Value}; -use ratatui::Frame; - -use crate::{value_display::ValueDisplay, Action, Elm, Result}; - -pub struct InterpreterDisplay { - interpreter: Interpreter, - path: PathBuf, - value_display: ValueDisplay, - modified: SystemTime, -} - -impl InterpreterDisplay { - pub fn new(context: Map, path: PathBuf) -> Result { - let interpreter = Interpreter::new(context)?; - let value_display = ValueDisplay::new(Value::default()); - let modified = SystemTime::now(); - - Ok(Self { - interpreter, - path, - value_display, - modified, - }) - } -} - -impl Elm for InterpreterDisplay { - fn update(&mut self, message: Action) -> Result> { - match message { - Action::Tick => { - let last_modified = self.path.metadata()?.modified()?; - - if last_modified != self.modified { - let source = read_to_string(&self.path)?; - let value = self.interpreter.run(&source)?; - - self.value_display = ValueDisplay::new(value); - self.modified = last_modified; - } - } - _ => {} - } - - self.value_display.update(message) - } - - fn view(&self, frame: &mut Frame) { - self.value_display.view(frame) - } -} diff --git a/src/bin/tui/log.rs b/src/bin/tui/log.rs deleted file mode 100644 index 00bd511..0000000 --- a/src/bin/tui/log.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::path::PathBuf; - -use color_eyre::eyre::Result; -use directories::ProjectDirs; -use lazy_static::lazy_static; -pub use tracing::error; -use tracing_error::ErrorLayer; -use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, Layer}; - -lazy_static! { - pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); - pub static ref DATA_FOLDER: Option = - std::env::var(format!("{}_DATA", PROJECT_NAME.clone())) - .ok() - .map(PathBuf::from); - pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone()); - pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); -} - -pub fn project_directory() -> Option { - ProjectDirs::from("io", "jeffa", env!("CARGO_PKG_NAME")) -} - -pub fn get_data_dir() -> PathBuf { - if let Some(path) = DATA_FOLDER.clone() { - path - } else if let Some(proj_dirs) = project_directory() { - proj_dirs.data_local_dir().to_path_buf() - } else { - PathBuf::from(".").join(".data") - } -} - -pub fn initialize_logging() -> Result<()> { - let directory = get_data_dir(); - std::fs::create_dir_all(directory.clone())?; - let log_path = directory.join(LOG_FILE.clone()); - let log_file = std::fs::File::create(log_path)?; - std::env::set_var( - "RUST_LOG", - std::env::var("RUST_LOG") - .or_else(|_| std::env::var(LOG_ENV.clone())) - .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))), - ); - let file_subscriber = tracing_subscriber::fmt::layer() - .with_file(true) - .with_line_number(true) - .with_writer(log_file) - .with_target(false) - .with_ansi(false) - .with_filter(tracing_subscriber::filter::EnvFilter::from_default_env()); - tracing_subscriber::registry() - .with(file_subscriber) - .with(ErrorLayer::default()) - .init(); - Ok(()) -} - -/// Similar to the `std::dbg!` macro, but generates `tracing` events rather -/// than printing to stdout. -/// -/// By default, the verbosity level for the generated events is `DEBUG`, but -/// this can be customized. -#[macro_export] -macro_rules! trace_dbg { - (target: $target:expr, level: $level:expr, $ex:expr) => {{ - match $ex { - value => { - tracing::event!(target: $target, $level, ?value, stringify!($ex)); - value - } - } - }}; - (level: $level:expr, $ex:expr) => { - trace_dbg!(target: module_path!(), level: $level, $ex) - }; - (target: $target:expr, $ex:expr) => { - trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex) - }; - ($ex:expr) => { - trace_dbg!(level: tracing::Level::DEBUG, $ex) - }; -} diff --git a/src/bin/tui/main.rs b/src/bin/tui/main.rs index c8f8add..a611157 100644 --- a/src/bin/tui/main.rs +++ b/src/bin/tui/main.rs @@ -1,67 +1,46 @@ -pub mod app; -pub mod interpreter_display; -pub mod log; -pub mod terminal; -pub mod value_display; +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_variables)] -use std::path::PathBuf; +pub mod action; +pub mod app; +pub mod cli; +pub mod components; +pub mod config; +pub mod mode; +pub mod tui; +pub mod utils; + +use std::env; + +use clap::Parser; +use cli::Cli; +use color_eyre::eyre::Result; use crate::{ app::App, - log::{get_data_dir, initialize_logging}, + utils::{initialize_logging, initialize_panic_handler, version}, }; -use clap::Parser; -use color_eyre::Result; -use crossterm::event::{KeyEvent, MouseEvent}; -use dust_lang::Value; -use ratatui::Frame; -use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc; -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Cli { - /// File with source to be run and watched by the shell. - path: Option, -} +async fn tokio_main() -> Result<()> { + initialize_panic_handler()?; + initialize_logging()?; -pub trait Elm { - fn update(&mut self, message: Action) -> Result>; - fn view(&self, frame: &mut Frame); -} + env::set_var("DUST_OUTPUT_MODE", "SILENT"); -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum Action { - Init, - Quit, - Error, - Closed, - Tick, - Render, - FocusGained, - FocusLost, - Paste(String), - Key(KeyEvent), - Mouse(MouseEvent), - Resize(u16, u16), - UpdateValue(Value), + let args = Cli::parse(); + let mut app = App::new(args.path)?; + app.run().await?; + + Ok(()) } #[tokio::main] -async fn main() { - initialize_logging().unwrap(); - - let args = Cli::parse(); - let (action_tx, action_rx) = mpsc::unbounded_channel(); - let path = if let Some(path) = args.path { - PathBuf::from(path) +async fn main() -> Result<()> { + if let Err(e) = tokio_main().await { + eprintln!("{} error: Something went wrong", env!("CARGO_PKG_NAME")); + Err(e) } else { - PathBuf::from(format!("{}/scratch.ds", get_data_dir().to_string_lossy())) - }; - let mut app = App::new(action_rx, action_tx, path).unwrap(); - let run_result = app.run().await; - - if let Err(report) = run_result { - eprintln!("{report}") + Ok(()) } } diff --git a/src/bin/tui/mode.rs b/src/bin/tui/mode.rs new file mode 100644 index 0000000..1775f94 --- /dev/null +++ b/src/bin/tui/mode.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Mode { + #[default] + Home, +} diff --git a/src/bin/tui/terminal.rs b/src/bin/tui/terminal.rs deleted file mode 100644 index cdb943b..0000000 --- a/src/bin/tui/terminal.rs +++ /dev/null @@ -1,229 +0,0 @@ -use std::{ - io::{stderr, Stderr}, - ops::{Deref, DerefMut}, - time::Duration, -}; - -use color_eyre::eyre::Result; -use crossterm::{ - cursor, - event::{ - DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, - Event, KeyEventKind, - }, - terminal::{EnterAlternateScreen, LeaveAlternateScreen}, -}; -use futures::{FutureExt, StreamExt}; -use ratatui::prelude::CrosstermBackend; -use tokio::{ - sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, - task::JoinHandle, -}; -use tokio_util::sync::CancellationToken; - -use crate::{log, Action}; - -pub struct Terminal { - pub terminal: ratatui::Terminal>, - pub task: JoinHandle<()>, - pub cancellation_token: CancellationToken, - pub event_rx: UnboundedReceiver, - pub event_tx: UnboundedSender, - pub frame_rate: f64, - pub tick_rate: f64, - pub mouse: bool, - pub paste: bool, -} - -impl Terminal { - pub fn new() -> Result { - let tick_rate = 4.0; - let frame_rate = 60.0; - let terminal = ratatui::Terminal::new(CrosstermBackend::new(stderr()))?; - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let cancellation_token = CancellationToken::new(); - let task = tokio::spawn(async {}); - let mouse = false; - let paste = false; - Ok(Self { - terminal, - task, - cancellation_token, - event_rx, - event_tx, - frame_rate, - tick_rate, - mouse, - paste, - }) - } - - pub fn tick_rate(mut self, tick_rate: f64) -> Self { - self.tick_rate = tick_rate; - self - } - - pub fn frame_rate(mut self, frame_rate: f64) -> Self { - self.frame_rate = frame_rate; - self - } - - pub fn mouse(mut self, mouse: bool) -> Self { - self.mouse = mouse; - self - } - - pub fn paste(mut self, paste: bool) -> Self { - self.paste = paste; - self - } - - pub fn start(&mut self) { - let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate); - let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate); - self.cancel(); - self.cancellation_token = CancellationToken::new(); - let _cancellation_token = self.cancellation_token.clone(); - let _event_tx = self.event_tx.clone(); - self.task = tokio::spawn(async move { - let mut reader = crossterm::event::EventStream::new(); - let mut tick_interval = tokio::time::interval(tick_delay); - let mut render_interval = tokio::time::interval(render_delay); - _event_tx.send(Action::Init).unwrap(); - loop { - let tick_delay = tick_interval.tick(); - let render_delay = render_interval.tick(); - let crossterm_event = reader.next().fuse(); - tokio::select! { - _ = _cancellation_token.cancelled() => { - break; - } - maybe_event = crossterm_event => { - match maybe_event { - Some(Ok(evt)) => { - match evt { - Event::Key(key) => { - if key.kind == KeyEventKind::Press { - _event_tx.send(Action::Key(key)).unwrap(); - } - }, - Event::Mouse(mouse) => { - _event_tx.send(Action::Mouse(mouse)).unwrap(); - }, - Event::Resize(x, y) => { - _event_tx.send(Action::Resize(x, y)).unwrap(); - }, - Event::FocusLost => { - _event_tx.send(Action::FocusLost).unwrap(); - }, - Event::FocusGained => { - _event_tx.send(Action::FocusGained).unwrap(); - }, - Event::Paste(s) => { - _event_tx.send(Action::Paste(s)).unwrap(); - }, - } - } - Some(Err(_)) => { - _event_tx.send(Action::Error).unwrap(); - } - None => {}, - } - }, - _ = tick_delay => { - _event_tx.send(Action::Tick).unwrap(); - }, - _ = render_delay => { - _event_tx.send(Action::Render).unwrap(); - }, - } - } - }); - } - - pub fn stop(&self) -> Result<()> { - self.cancel(); - let mut counter = 0; - while !self.task.is_finished() { - std::thread::sleep(Duration::from_millis(1)); - counter += 1; - if counter > 50 { - self.task.abort(); - } - if counter > 100 { - log::error!("Failed to abort task in 100 milliseconds for unknown reason"); - break; - } - } - Ok(()) - } - - pub fn enter(&mut self) -> Result<()> { - crossterm::terminal::enable_raw_mode()?; - crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?; - if self.mouse { - crossterm::execute!(std::io::stderr(), EnableMouseCapture)?; - } - if self.paste { - crossterm::execute!(std::io::stderr(), EnableBracketedPaste)?; - } - self.start(); - Ok(()) - } - - pub fn exit(&mut self) -> Result<()> { - self.stop()?; - if crossterm::terminal::is_raw_mode_enabled()? { - self.flush()?; - if self.paste { - crossterm::execute!(std::io::stderr(), DisableBracketedPaste)?; - } - if self.mouse { - crossterm::execute!(std::io::stderr(), DisableMouseCapture)?; - } - crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?; - crossterm::terminal::disable_raw_mode()?; - } - Ok(()) - } - - pub fn cancel(&self) { - self.cancellation_token.cancel(); - } - - pub fn suspend(&mut self) -> Result<()> { - self.exit()?; - #[cfg(not(windows))] - signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; - Ok(()) - } - - pub fn resume(&mut self) -> Result<()> { - self.enter()?; - Ok(()) - } - - pub async fn next(&mut self) -> Option { - self.event_rx.recv().await - } -} - -impl Deref for Terminal { - type Target = ratatui::Terminal>; - - fn deref(&self) -> &Self::Target { - &self.terminal - } -} - -impl DerefMut for Terminal { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.terminal - } -} - -impl Drop for Terminal { - fn drop(&mut self) { - self.exit().unwrap(); - } -} diff --git a/src/bin/tui/lib.rs b/src/bin/tui/tui.rs similarity index 75% rename from src/bin/tui/lib.rs rename to src/bin/tui/tui.rs index 811d933..aa9783b 100644 --- a/src/bin/tui/lib.rs +++ b/src/bin/tui/tui.rs @@ -1,10 +1,3 @@ -pub mod app; -pub mod buffer; -pub mod error; -pub mod interpreter_display; -pub mod log; -pub mod value_display; - use std::{ ops::{Deref, DerefMut}, time::Duration, @@ -19,9 +12,8 @@ use crossterm::{ }, terminal::{EnterAlternateScreen, LeaveAlternateScreen}, }; -use dust_lang::Value; use futures::{FutureExt, StreamExt}; -use ratatui::{backend::CrosstermBackend as Backend, Frame}; +use ratatui::backend::CrosstermBackend as Backend; use serde::{Deserialize, Serialize}; use tokio::{ sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, @@ -29,13 +21,14 @@ use tokio::{ }; use tokio_util::sync::CancellationToken; -pub trait Elm { - fn update(&mut self, message: Action) -> Result>; - fn view(&self, frame: &mut Frame); +pub type IO = std::io::Stderr; +pub fn io() -> IO { + std::io::stderr() } +pub type Frame<'a> = ratatui::Frame<'a>; #[derive(Clone, Debug, Serialize, Deserialize)] -pub enum Action { +pub enum Event { Init, Quit, Error, @@ -48,15 +41,14 @@ pub enum Action { Key(KeyEvent), Mouse(MouseEvent), Resize(u16, u16), - UpdateValue(Value), } pub struct Tui { - pub terminal: ratatui::Terminal>, + pub terminal: ratatui::Terminal>, pub task: JoinHandle<()>, pub cancellation_token: CancellationToken, - pub event_rx: UnboundedReceiver, - pub event_tx: UnboundedSender, + pub event_rx: UnboundedReceiver, + pub event_tx: UnboundedSender, pub frame_rate: f64, pub tick_rate: f64, pub mouse: bool, @@ -65,14 +57,15 @@ pub struct Tui { impl Tui { pub fn new() -> Result { - let tick_rate = 4.0; - let frame_rate = 60.0; - let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?; + let tick_rate = 2.0; + let frame_rate = 30.0; + let terminal = ratatui::Terminal::new(Backend::new(io()))?; let (event_tx, event_rx) = mpsc::unbounded_channel(); let cancellation_token = CancellationToken::new(); let task = tokio::spawn(async {}); let mouse = false; let paste = false; + Ok(Self { terminal, task, @@ -109,19 +102,25 @@ impl Tui { pub fn start(&mut self) { let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate); let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate); + self.cancel(); self.cancellation_token = CancellationToken::new(); + let _cancellation_token = self.cancellation_token.clone(); let _event_tx = self.event_tx.clone(); + self.task = tokio::spawn(async move { let mut reader = crossterm::event::EventStream::new(); let mut tick_interval = tokio::time::interval(tick_delay); let mut render_interval = tokio::time::interval(render_delay); - _event_tx.send(Action::Init).unwrap(); + + _event_tx.send(Event::Init).unwrap(); + loop { let tick_delay = tick_interval.tick(); let render_delay = render_interval.tick(); let crossterm_event = reader.next().fuse(); + tokio::select! { _ = _cancellation_token.cancelled() => { break; @@ -132,37 +131,37 @@ impl Tui { match evt { CrosstermEvent::Key(key) => { if key.kind == KeyEventKind::Press { - _event_tx.send(Action::Key(key)).unwrap(); + _event_tx.send(Event::Key(key)).unwrap(); } }, CrosstermEvent::Mouse(mouse) => { - _event_tx.send(Action::Mouse(mouse)).unwrap(); + _event_tx.send(Event::Mouse(mouse)).unwrap(); }, CrosstermEvent::Resize(x, y) => { - _event_tx.send(Action::Resize(x, y)).unwrap(); + _event_tx.send(Event::Resize(x, y)).unwrap(); }, CrosstermEvent::FocusLost => { - _event_tx.send(Action::FocusLost).unwrap(); + _event_tx.send(Event::FocusLost).unwrap(); }, CrosstermEvent::FocusGained => { - _event_tx.send(Action::FocusGained).unwrap(); + _event_tx.send(Event::FocusGained).unwrap(); }, CrosstermEvent::Paste(s) => { - _event_tx.send(Action::Paste(s)).unwrap(); + _event_tx.send(Event::Paste(s)).unwrap(); }, } } Some(Err(_)) => { - _event_tx.send(Action::Error).unwrap(); + _event_tx.send(Event::Error).unwrap(); } None => {}, } }, _ = tick_delay => { - _event_tx.send(Action::Tick).unwrap(); + _event_tx.send(Event::Tick).unwrap(); }, _ = render_delay => { - _event_tx.send(Action::Render).unwrap(); + _event_tx.send(Event::Render).unwrap(); }, } } @@ -171,47 +170,62 @@ impl Tui { pub fn stop(&self) -> Result<()> { self.cancel(); + let mut counter = 0; + while !self.task.is_finished() { std::thread::sleep(Duration::from_millis(1)); + counter += 1; + if counter > 50 { self.task.abort(); } + if counter > 100 { log::error!("Failed to abort task in 100 milliseconds for unknown reason"); break; } } + Ok(()) } pub fn enter(&mut self) -> Result<()> { crossterm::terminal::enable_raw_mode()?; - crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?; + crossterm::execute!(io(), EnterAlternateScreen, cursor::Hide)?; + if self.mouse { - crossterm::execute!(std::io::stderr(), EnableMouseCapture)?; + crossterm::execute!(io(), EnableMouseCapture)?; } + if self.paste { - crossterm::execute!(std::io::stderr(), EnableBracketedPaste)?; + crossterm::execute!(io(), EnableBracketedPaste)?; } + self.start(); + Ok(()) } pub fn exit(&mut self) -> Result<()> { self.stop()?; + if crossterm::terminal::is_raw_mode_enabled()? { self.flush()?; + if self.paste { - crossterm::execute!(std::io::stderr(), DisableBracketedPaste)?; + crossterm::execute!(io(), DisableBracketedPaste)?; } + if self.mouse { - crossterm::execute!(std::io::stderr(), DisableMouseCapture)?; + crossterm::execute!(io(), DisableMouseCapture)?; } - crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?; + + crossterm::execute!(io(), LeaveAlternateScreen, cursor::Show)?; crossterm::terminal::disable_raw_mode()?; } + Ok(()) } @@ -231,13 +245,13 @@ impl Tui { Ok(()) } - pub async fn next(&mut self) -> Option { + pub async fn next(&mut self) -> Option { self.event_rx.recv().await } } impl Deref for Tui { - type Target = ratatui::Terminal>; + type Target = ratatui::Terminal>; fn deref(&self) -> &Self::Target { &self.terminal diff --git a/src/bin/tui/utils.rs b/src/bin/tui/utils.rs new file mode 100644 index 0000000..f00e99e --- /dev/null +++ b/src/bin/tui/utils.rs @@ -0,0 +1,169 @@ +use std::path::PathBuf; + +use color_eyre::eyre::Result; +use directories::ProjectDirs; +use lazy_static::lazy_static; +use tracing::error; +use tracing_error::ErrorLayer; +use tracing_subscriber::{ + self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer, +}; + +lazy_static! { + pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); + pub static ref DATA_FOLDER: Option = + std::env::var(format!("{}_DATA", PROJECT_NAME.clone())) + .ok() + .map(PathBuf::from); + pub static ref CONFIG_FOLDER: Option = + std::env::var(format!("{}_CONFIG", PROJECT_NAME.clone())) + .ok() + .map(PathBuf::from); + pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone()); + pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); +} + +fn project_directory() -> Option { + ProjectDirs::from("com", "kdheepak", env!("CARGO_PKG_NAME")) +} + +pub fn initialize_panic_handler() -> Result<()> { + let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() + .panic_section(format!( + "This is a bug. Consider reporting it at {}", + env!("CARGO_PKG_REPOSITORY") + )) + .capture_span_trace_by_default(false) + .display_location_section(false) + .display_env_section(false) + .into_hooks(); + eyre_hook.install()?; + std::panic::set_hook(Box::new(move |panic_info| { + if let Ok(mut t) = crate::tui::Tui::new() { + if let Err(r) = t.exit() { + error!("Unable to exit Terminal: {:?}", r); + } + } + + #[cfg(not(debug_assertions))] + { + use human_panic::{handle_dump, print_msg, Metadata}; + let meta = Metadata { + version: env!("CARGO_PKG_VERSION").into(), + name: env!("CARGO_PKG_NAME").into(), + authors: env!("CARGO_PKG_AUTHORS").replace(':', ", ").into(), + homepage: env!("CARGO_PKG_HOMEPAGE").into(), + }; + + let file_path = handle_dump(&meta, panic_info); + // prints human-panic message + print_msg(file_path, &meta) + .expect("human-panic: printing error message to console failed"); + eprintln!("{}", panic_hook.panic_report(panic_info)); // prints color-eyre stack trace to stderr + } + let msg = format!("{}", panic_hook.panic_report(panic_info)); + log::error!("Error: {}", strip_ansi_escapes::strip_str(msg)); + + #[cfg(debug_assertions)] + { + // Better Panic stacktrace that is only enabled when debugging. + better_panic::Settings::auto() + .most_recent_first(false) + .lineno_suffix(true) + .verbosity(better_panic::Verbosity::Full) + .create_panic_handler()(panic_info); + } + + std::process::exit(libc::EXIT_FAILURE); + })); + Ok(()) +} + +pub fn get_data_dir() -> PathBuf { + let directory = if let Some(s) = DATA_FOLDER.clone() { + s + } else if let Some(proj_dirs) = project_directory() { + proj_dirs.data_local_dir().to_path_buf() + } else { + PathBuf::from(".").join(".data") + }; + directory +} + +pub fn get_config_dir() -> PathBuf { + let directory = if let Some(s) = CONFIG_FOLDER.clone() { + s + } else if let Some(proj_dirs) = project_directory() { + proj_dirs.config_local_dir().to_path_buf() + } else { + PathBuf::from(".").join(".config") + }; + directory +} + +pub fn initialize_logging() -> Result<()> { + let directory = get_data_dir(); + std::fs::create_dir_all(directory.clone())?; + let log_path = directory.join(LOG_FILE.clone()); + let log_file = std::fs::File::create(log_path)?; + std::env::set_var( + "RUST_LOG", + std::env::var("RUST_LOG") + .or_else(|_| std::env::var(LOG_ENV.clone())) + .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))), + ); + let file_subscriber = tracing_subscriber::fmt::layer() + .with_file(true) + .with_line_number(true) + .with_writer(log_file) + .with_target(false) + .with_ansi(false) + .with_filter(tracing_subscriber::filter::EnvFilter::from_default_env()); + tracing_subscriber::registry() + .with(file_subscriber) + .with(ErrorLayer::default()) + .init(); + Ok(()) +} + +/// Similar to the `std::dbg!` macro, but generates `tracing` events rather +/// than printing to stdout. +/// +/// By default, the verbosity level for the generated events is `DEBUG`, but +/// this can be customized. +#[macro_export] +macro_rules! trace_dbg { + (target: $target:expr, level: $level:expr, $ex:expr) => {{ + match $ex { + value => { + tracing::event!(target: $target, $level, ?value, stringify!($ex)); + value + } + } + }}; + (level: $level:expr, $ex:expr) => { + trace_dbg!(target: module_path!(), level: $level, $ex) + }; + (target: $target:expr, $ex:expr) => { + trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex) + }; + ($ex:expr) => { + trace_dbg!(level: tracing::Level::DEBUG, $ex) + }; +} + +pub fn version() -> String { + let author = clap::crate_authors!(); + + // let current_exe_path = PathBuf::from(clap::crate_name!()).display().to_string(); + let config_dir_path = get_config_dir().display().to_string(); + let data_dir_path = get_data_dir().display().to_string(); + + format!( + "\ +authors: {author} + +Config directory: {config_dir_path} +Data directory: {data_dir_path}" + ) +} diff --git a/src/bin/tui/value_display.rs b/src/bin/tui/value_display.rs deleted file mode 100644 index 2124f85..0000000 --- a/src/bin/tui/value_display.rs +++ /dev/null @@ -1,52 +0,0 @@ -use dust_lang::Value; -use ratatui::{ - style::{Color, Style}, - widgets::{Block, Borders, List, Paragraph}, - Frame, -}; - -use crate::{Action, Elm, Result}; - -pub struct ValueDisplay { - value: Value, -} - -impl ValueDisplay { - pub fn new(value: Value) -> Self { - ValueDisplay { value } - } -} - -impl Elm for ValueDisplay { - fn update(&mut self, _message: Action) -> Result> { - Ok(None) - } - - fn view(&self, frame: &mut Frame) { - match &self.value { - Value::List(list) => { - let widget = List::new(list.items().iter().map(|value| value.to_string())) - .block(Block::default().title("list").borders(Borders::all())); - - frame.render_widget(widget, frame.size()); - } - Value::Map(_) => todo!(), - Value::Function(_) => todo!(), - Value::String(string) => { - let widget = - Paragraph::new(string.as_str()).style(Style::default().fg(Color::Green)); - - frame.render_widget(widget, frame.size()); - } - Value::Float(_) => todo!(), - Value::Integer(integer) => { - let widget = - Paragraph::new(integer.to_string()).style(Style::default().fg(Color::Red)); - - frame.render_widget(widget, frame.size()); - } - Value::Boolean(_) => todo!(), - Value::Option(_) => todo!(), - } - } -} diff --git a/src/built_in_functions/output.rs b/src/built_in_functions/output.rs index 6735410..ced0c19 100644 --- a/src/built_in_functions/output.rs +++ b/src/built_in_functions/output.rs @@ -1,4 +1,25 @@ -use crate::{BuiltInFunction, Map, Result, Type, Value}; +use lazy_static::lazy_static; + +use crate::{BuiltInFunction, Error, Map, Result, Type, Value}; + +lazy_static! { + static ref OUTPUT_MODE: OutputMode = { + if let Ok(variable) = std::env::var("DUST_OUTPUT_MODE") { + if variable == "SILENT" { + OutputMode::Silent + } else { + OutputMode::Normal + } + } else { + OutputMode::Normal + } + }; +} + +pub enum OutputMode { + Normal, + Silent, +} pub struct Output; @@ -8,11 +29,15 @@ impl BuiltInFunction for Output { } fn run(&self, arguments: &[Value], _context: &Map) -> Result { - for argument in arguments { - println!("{argument}"); + Error::expect_argument_amount(self, 1, arguments.len())?; + + let value = arguments.first().unwrap(); + + if let OutputMode::Normal = *OUTPUT_MODE { + println!("{value}"); } - Ok(Value::Option(None)) + Ok(Value::default()) } fn r#type(&self) -> Type { diff --git a/src/interpret.rs b/src/interpret.rs index 0ebc40c..f8223c3 100644 --- a/src/interpret.rs +++ b/src/interpret.rs @@ -41,9 +41,6 @@ pub fn interpret(source: &str) -> Result { /// ); /// ``` pub fn interpret_with_context(source: &str, context: Map) -> Result { - let mut parser = Parser::new(); - parser.set_language(language())?; - let mut interpreter = Interpreter::new(context)?; let value = interpreter.run(source)?; @@ -91,7 +88,7 @@ impl Interpreter { if let Some(abstract_tree) = &self.abstract_tree { abstract_tree.run(source, &self.context) } else { - Ok(Value::Option(None)) + Ok(Value::none()) } } diff --git a/src/value/mod.rs b/src/value/mod.rs index 69d42f3..de253ca 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -41,7 +41,7 @@ pub enum Value { impl Default for Value { fn default() -> Self { - Value::Option(None) + Value::none() } } @@ -87,6 +87,10 @@ impl Value { r#type } + pub fn none() -> Self { + Value::Option(None) + } + pub fn is_string(&self) -> bool { matches!(self, Value::String(_)) } diff --git a/tree-sitter-dust/highlights.scm b/tree-sitter-dust/highlights.scm index 59a3259..913f3a8 100644 --- a/tree-sitter-dust/highlights.scm +++ b/tree-sitter-dust/highlights.scm @@ -37,6 +37,7 @@ "any" "async" "else" + "else if" "false" "float" "for"