commit 47b3413d7de1ba061462c858f6f97b56bb3b598d Author: unspeaker Date: Sun Mar 2 14:31:04 2025 +0200 tabula rasa diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8899d91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +cov +*.profraw diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..540bdd5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1281 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic_float" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[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 = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[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 = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clojure-reader" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4774a73e49ad34674c398196f8bee1aed6187eaa17ce57c17e7703cfd2c05ec6" +dependencies = [ + "ordered-float", +] + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "const_panic" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "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 = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "instability" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "konst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.170" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[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.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", + "rand", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] + +[[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", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quanta" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd1fe6824cea6538803de3ff1bc0cf3949024db3d43c9643024bfb33a807c0e" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "raw-cpuid" +version = "11.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529468c1335c1c03919960dfefdb1b3648858c20d7ec2d0663e728e4a717efbc" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tek_edn" +version = "0.1.0" +dependencies = [ + "clojure-reader", + "itertools 0.14.0", + "konst", + "proptest", + "tek_tui", + "thiserror", +] + +[[package]] +name = "tek_input" +version = "0.2.0" +dependencies = [ + "tek_edn", + "tek_tui", +] + +[[package]] +name = "tek_output" +version = "0.2.0" +dependencies = [ + "proptest", + "proptest-derive", + "tek_edn", + "tek_tui", +] + +[[package]] +name = "tek_tui" +version = "0.2.0" +dependencies = [ + "atomic_float", + "better-panic", + "crossterm", + "konst", + "palette", + "quanta", + "rand", + "ratatui", + "tek_edn", + "tek_input", + "tek_output", +] + +[[package]] +name = "tempfile" +version = "3.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typewit" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb77c29baba9e4d3a6182d51fa75e3215c7fd1dab8f4ea9d107c716878e55fc0" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "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..251c6e8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +resolver = "2" +members = [ + "./input", + "./output", + "./tui", + "./edn" +] + +[profile.release] +lto = true + +[profile.coverage] +inherits = "test" +lto = false diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..cf9d1e9 --- /dev/null +++ b/Justfile @@ -0,0 +1,10 @@ +covfig := "CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' RUSTDOCFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cov/cargo-test-%p-%m.profraw'" +grcov-binary := "--binary-path ./target/coverage/deps/" +grcov-ignore := "--ignore-not-existing --ignore '../*' --ignore \"/*\" --ignore 'target/*'" +cov: + {{covfig}} time cargo test -j4 --workspace --exclude jack --profile coverage + rm -rf target/coverage/html || true + {{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t html -o target/coverage/html +cov-md: + {{covfig}} time cargo test -j4 --workspace --exclude jack --profile coverage + {{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t markdown | sort diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/edn/Cargo.lock b/edn/Cargo.lock new file mode 100644 index 0000000..7305975 --- /dev/null +++ b/edn/Cargo.lock @@ -0,0 +1,980 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[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 = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[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 = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clojure-reader" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edf141eea627c101a97509266bc9f6ba8cd408618f5e2ac4a0cb6b64b1d4ea8" +dependencies = [ + "ordered-float", +] + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "const_panic" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53857514f72ee4a2b583de67401e3ff63a5472ca4acf289d09a9ea7636dfec17" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "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 = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "instability" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "894813a444908c0c8c0e221b041771d107c4a21de1d317dc49bcc66e3c9e5b3f" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "konst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", + "rand", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] + +[[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", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[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 = "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tek_edn" +version = "0.1.0" +dependencies = [ + "clojure-reader", + "itertools 0.14.0", + "konst", + "tek_tui", +] + +[[package]] +name = "tek_input" +version = "0.2.0" + +[[package]] +name = "tek_output" +version = "0.2.0" +dependencies = [ + "tek_edn", +] + +[[package]] +name = "tek_tui" +version = "0.2.0" +dependencies = [ + "better-panic", + "crossterm", + "palette", + "rand", + "ratatui", + "tek_edn", + "tek_input", + "tek_output", +] + +[[package]] +name = "typewit" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb77c29baba9e4d3a6182d51fa75e3215c7fd1dab8f4ea9d107c716878e55fc0" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[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 = [ + "byteorder", + "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/edn/Cargo.toml b/edn/Cargo.toml new file mode 100644 index 0000000..c064cfb --- /dev/null +++ b/edn/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tek_edn" +edition = "2021" +version = "0.1.0" + +[dependencies] +clojure-reader = "0.3.0" +konst = { version = "0.3.16", features = [ "rust_1_83" ] } +itertools = "0.14.0" +thiserror = "2.0" + +[features] +default = [] + +[dev-dependencies] +tek_tui = { path = "../tui" } +proptest = "^1.6.0" diff --git a/edn/README.md b/edn/README.md new file mode 100644 index 0000000..bffd5f5 --- /dev/null +++ b/edn/README.md @@ -0,0 +1,94 @@ +# `ket` + +**ket** is the configuration language of **tek**. +it's based on [edn](https://en.wikipedia.org/wiki/Clojure#Extensible_Data_Notation) +but without all the features. + +## usage + +### with `tek_output` + +this is a `tek_output` view layout defined using ket: + +```edn +(bsp/s (fixed/y 2 :toolbar) + (fill/x (align/c (bsp/w :pool + (bsp/s :outputs (bsp/s :inputs (bsp/s :tracks :scenes))))))) +``` + +### with `tek_input` + +this is a `tek_input` keymap defined using ket: + +```edn +(@u undo 1) +(@shift-u redo 1) +(@e editor show :pool-clip) +(@ctrl-a scene add) +(@ctrl-t track add) +(@tab pool toggle) +``` + +## tokens + +ket has 4 "types", represented by variants of the `Value` enum: + +* `Num` - numeric literal +* `Sym` - textual symbol +* `Key` - textual key +* `Exp` - parenthesized group of tokens + +### numeric literal + +numbers are passed through as is. only non-negative integers are supported. + +```edn +0 +123456 +``` + +### keys + +keys are the names of available operations. they look like this: + +```edn +simple-key +multi-part/key +``` + +keys are implemented by the underlying subsystem: + +* in `tek_output`, keys are names of layout primitives +* in `tek_input`, keys are names of commands + +### symbols + +symbols that start with `:` represent the names of variables +provided by the `Context` trait - such as command parameters, +or entire layout components: + +```edn +:symbol-name +``` + +symbols that start with `@` represent keybindings. +they are parsed in `tek_tui` and look like this: + +```edn +@ctrl-alt-shift-space +``` + +### parenthesized groups + +parenthesized groups represent things like expressions +or configuration statements, and look like this: + +```edn +(some-key :symbol (some/other-key @another-symbol 123) 456) +``` + +## goals + +* [ ] const parse +* [ ] live reload +* [ ] serialize modified code back to original indentation diff --git a/edn/proptest-regressions/iter.txt b/edn/proptest-regressions/iter.txt new file mode 100644 index 0000000..b4d8405 --- /dev/null +++ b/edn/proptest-regressions/iter.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc bbb90b16e6106f17dbb5a4f57594f451360a2ea7e3e20c28adeb8babc98d39df # shrinks to source = "(𰀀" diff --git a/edn/proptest-regressions/token.txt b/edn/proptest-regressions/token.txt new file mode 100644 index 0000000..cc03cf2 --- /dev/null +++ b/edn/proptest-regressions/token.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 5fb814b74ae035bdecb536817090cfb473f0a874e9acf9aaa136a4794cdb367f # shrinks to source = "", start = 10336420442936153584, length = 8110323630773398032 diff --git a/edn/src/context.rs b/edn/src/context.rs new file mode 100644 index 0000000..d7adc78 --- /dev/null +++ b/edn/src/context.rs @@ -0,0 +1,130 @@ +use crate::*; +pub trait TryFromAtom<'a, T>: Sized { + fn try_from_expr (_state: &'a T, _iter: TokenIter<'a>) -> Option { None } + fn try_from_atom (state: &'a T, value: Value<'a>) -> Option { + if let Exp(0, iter) = value { return Self::try_from_expr(state, iter) } + None + } +} +pub trait TryIntoAtom: Sized { + fn try_into_atom (&self) -> Option; +} +/// Map EDN tokens to parameters of a given type for a given context +pub trait Context: Sized { + fn get (&self, _atom: &Value) -> Option { + None + } + fn get_or_fail (&self, atom: &Value) -> U { + self.get(atom).expect("no value") + } +} +impl, U> Context for &T { + fn get (&self, atom: &Value) -> Option { + (*self).get(atom) + } + fn get_or_fail (&self, atom: &Value) -> U { + (*self).get_or_fail(atom) + } +} +impl, U> Context for Option { + fn get (&self, atom: &Value) -> Option { + self.as_ref().map(|s|s.get(atom)).flatten() + } + fn get_or_fail (&self, atom: &Value) -> U { + self.as_ref().map(|s|s.get_or_fail(atom)).expect("no provider") + } +} +/// Implement `Context` for a context and type. +#[macro_export] macro_rules! provide { + // Provide a value to the EDN template + ($type:ty:|$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => { + impl Context<$type> for $State { + #[allow(unreachable_code)] + fn get (&$self, atom: &Value) -> Option<$type> { + use Value::*; + Some(match atom { $(Sym($pat) => $expr,)* _ => return None }) + } + } + }; + // Provide a value more generically + ($lt:lifetime: $type:ty:|$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => { + impl<$lt> Context<$lt, $type> for $State { + #[allow(unreachable_code)] + fn get (&$lt $self, atom: &Value) -> Option<$type> { + use Value::*; + Some(match atom { $(Sym($pat) => $expr,)* _ => return None }) + } + } + }; +} +/// Implement `Context` for a context and numeric type. +/// +/// This enables support for numeric literals. +#[macro_export] macro_rules! provide_num { + // Provide a value that may also be a numeric literal in the EDN, to a generic implementation. + ($type:ty:|$self:ident:<$T:ident:$Trait:path>|{ $($pat:pat => $expr:expr),* $(,)? }) => { + impl<$T: $Trait> Context<$type> for $T { + fn get (&$self, atom: &Value) -> Option<$type> { + use Value::*; + Some(match atom { $(Sym($pat) => $expr,)* Num(n) => *n as $type, _ => return None }) + } + } + }; + // Provide a value that may also be a numeric literal in the EDN, to a concrete implementation. + ($type:ty:|$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => { + impl Context<$type> for $State { + fn get (&$self, atom: &Value) -> Option<$type> { + use Value::*; + Some(match atom { $(Sym($pat) => $expr,)* Num(n) => *n as $type, _ => return None }) + } + } + }; +} +/// Implement `Context` for a context and the boolean type. +/// +/// This enables support for boolean literals. +#[macro_export] macro_rules! provide_bool { + // Provide a value that may also be a numeric literal in the EDN, to a generic implementation. + ($type:ty:|$self:ident:<$T:ident:$Trait:path>|{ $($pat:pat => $expr:expr),* $(,)? }) => { + impl<$T: $Trait> Context<$type> for $T { + fn get (&$self, atom: &Value) -> Option<$type> { + use Value::*; + Some(match atom { + Num(n) => match *n { 0 => false, _ => true }, + Sym(":false") | Sym(":f") => false, + Sym(":true") | Sym(":t") => true, + $(Sym($pat) => $expr,)* + _ => return Context::get(self, atom) + }) + } + } + }; + // Provide a value that may also be a numeric literal in the EDN, to a concrete implementation. + ($type:ty:|$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => { + impl Context<$type> for $State { + fn get (&$self, atom: &Value) -> Option<$type> { + use Value::*; + Some(match atom { + Num(n) => match *n { 0 => false, _ => true }, + Sym(":false") | Sym(":f") => false, + Sym(":true") | Sym(":t") => true, + $(Sym($pat) => $expr,)* + _ => return None + }) + } + } + }; +} +#[cfg(test)] #[test] fn test_edn_context () { + struct Test; + provide_bool!(bool: |self: Test|{ + ":provide-bool" => true + }); + let test = Test; + assert_eq!(test.get(&Value::Sym(":false")), Some(false)); + assert_eq!(test.get(&Value::Sym(":true")), Some(true)); + assert_eq!(test.get(&Value::Sym(":provide-bool")), Some(true)); + assert_eq!(test.get(&Value::Sym(":missing-bool")), None); + assert_eq!(test.get(&Value::Num(0)), Some(false)); + assert_eq!(test.get(&Value::Num(1)), Some(true)); +} diff --git a/edn/src/error.rs b/edn/src/error.rs new file mode 100644 index 0000000..40b687d --- /dev/null +++ b/edn/src/error.rs @@ -0,0 +1,15 @@ +use crate::*; +use thiserror::Error; +pub type ParseResult = Result; +#[derive(Error, Debug, Copy, Clone, PartialEq)] pub enum ParseError { + #[error("parse failed: not implemented")] + Unimplemented, + #[error("parse failed: empty")] + Empty, + #[error("parse failed: incomplete")] + Incomplete, + #[error("parse failed: unexpected character '{0}'")] + Unexpected(char), + #[error("parse failed: error #{0}")] + Code(u8), +} diff --git a/edn/src/iter.rs b/edn/src/iter.rs new file mode 100644 index 0000000..02ff043 --- /dev/null +++ b/edn/src/iter.rs @@ -0,0 +1,144 @@ +//! The token iterator [TokenIter] allows you to get the +//! general-purpose syntactic [Token]s represented by the source text. +//! +//! Both iterators are `peek`able: +//! +//! ``` +//! let src = include_str!("../test.edn"); +//! let mut view = tek_edn::TokenIter::new(src); +//! assert_eq!(view.0.0, src); +//! assert_eq!(view.peek(), view.0.peek()) +//! ``` +use crate::*; +/// Provides a native [Iterator] API over the [ConstIntoIter] [SourceIter] +/// [TokenIter::next] returns just the [Token] and mutates `self`, +/// instead of returning an updated version of the struct as [SourceIter::next] does. +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct TokenIter<'a>(pub SourceIter<'a>); +impl<'a> TokenIter<'a> { + pub const fn new (source: &'a str) -> Self { Self(SourceIter::new(source)) } + pub const fn peek (&self) -> Option> { self.0.peek() } +} +impl<'a> Iterator for TokenIter<'a> { + type Item = Token<'a>; + fn next (&mut self) -> Option> { + self.0.next().map(|(item, rest)|{self.0 = rest; item}) + } +} +/// Owns a reference to the source text. +/// [SourceIter::next] emits subsequent pairs of: +/// * a [Token] and +/// * the source text remaining +/// * [ ] TODO: maybe [SourceIter::next] should wrap the remaining source in `Self` ? +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct SourceIter<'a>(pub &'a str); +const_iter!(<'a>|self: SourceIter<'a>| => Token<'a> => self.next_mut().map(|(result, _)|result)); +impl<'a> From<&'a str> for SourceIter<'a> {fn from (source: &'a str) -> Self{Self::new(source)}} +impl<'a> SourceIter<'a> { + pub const fn new (source: &'a str) -> Self { Self(source) } + pub const fn chomp (&self, index: usize) -> Self { Self(split_at(self.0, index).1) } + pub const fn next (mut self) -> Option<(Token<'a>, Self)> { Self::next_mut(&mut self) } + pub const fn peek (&self) -> Option> { peek_src(self.0) } + pub const fn next_mut (&mut self) -> Option<(Token<'a>, Self)> { + match self.peek() { + Some(token) => Some((token, self.chomp(token.end()))), + None => None + } + } +} +pub const fn peek_src <'a> (source: &'a str) -> Option> { + let mut token: Token<'a> = Token::new(source, 0, 0, Nil); + iterate!(char_indices(source) => (start, c) => token = match token.value() { + Err(_) => return Some(token), + Nil => match c { + ' '|'\n'|'\r'|'\t' => + token.grow(), + '(' => + Token::new(source, start, 1, Exp(1, TokenIter::new(str_range(source, start, start + 1)))), + ':'|'@' => + Token::new(source, start, 1, Sym(str_range(source, start, start + 1))), + '/'|'a'..='z' => + Token::new(source, start, 1, Key(str_range(source, start, start + 1))), + '0'..='9' => + Token::new(source, start, 1, match to_digit(c) { + Ok(c) => Value::Num(c), + Result::Err(e) => Value::Err(e) + }), + _ => token.error(Unexpected(c)) + }, + Num(n) => match c { + '0'..='9' => token.grow_num(n, c), + ' '|'\n'|'\r'|'\t'|')' => return Some(token), + _ => token.error(Unexpected(c)) + }, + Sym(_) => match c { + 'a'..='z'|'A'..='Z'|'0'..='9'|'-' => token.grow_sym(), + ' '|'\n'|'\r'|'\t'|')' => return Some(token), + _ => token.error(Unexpected(c)) + }, + Key(_) => match c { + 'a'..='z'|'0'..='9'|'-'|'/' => token.grow_key(), + ' '|'\n'|'\r'|'\t'|')' => return Some(token), + _ => token.error(Unexpected(c)) + }, + Exp(depth, _) => match depth { + 0 => return Some(token.grow_exp()), + _ => match c { + ')' => token.grow_out(), + '(' => token.grow_in(), + _ => token.grow_exp(), + } + }, + }); + match token.value() { + Nil => None, + _ => Some(token), + } +} +pub const fn to_number (digits: &str) -> Result { + let mut value = 0; + iterate!(char_indices(digits) => (_, c) => match to_digit(c) { + Ok(digit) => value = 10 * value + digit, + Result::Err(e) => return Result::Err(e) + }); + Ok(value) +} +pub const fn to_digit (c: char) -> Result { + Ok(match c { + '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, + '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, + _ => return Result::Err(Unexpected(c)) + }) +} +#[cfg(test)] mod test_token_iter { + use super::*; + //use proptest::prelude::*; + #[test] fn test_iters () { + let mut iter = crate::SourceIter::new(&":foo :bar"); + let _ = iter.next(); + let mut iter = crate::TokenIter::new(&":foo :bar"); + let _ = iter.next(); + } + #[test] const fn test_const_iters () { + let mut iter = crate::SourceIter::new(&":foo :bar"); + let _ = iter.next(); + } + #[test] fn test_num () { + let digit = to_digit('0'); + let digit = to_digit('x'); + let number = to_number(&"123"); + let number = to_number(&"12asdf3"); + } + //proptest! { + //#[test] fn proptest_source_iter ( + //source in "\\PC*" + //) { + //let mut iter = crate::SourceIter::new(&source); + ////let _ = iter.next(); + //} + //#[test] fn proptest_token_iter ( + //source in "\\PC*" + //) { + //let mut iter = crate::TokenIter::new(&source); + ////let _ = iter.next(); + //} + //} +} diff --git a/edn/src/lib.rs b/edn/src/lib.rs new file mode 100644 index 0000000..10d60bb --- /dev/null +++ b/edn/src/lib.rs @@ -0,0 +1,61 @@ +#![feature(adt_const_params)] +#![feature(type_alias_impl_trait)] +#![feature(impl_trait_in_fn_trait_return)] +mod error; pub use self::error::*; +mod token; pub use self::token::*; +mod iter; pub use self::iter::*; +mod context; pub use self::context::*; +pub(crate) use self::Value::*; +pub(crate) use self::ParseError::*; +pub(crate) use konst::iter::{ConstIntoIter, IsIteratorKind}; +pub(crate) use konst::string::{split_at, str_range, char_indices}; +pub(crate) use std::error::Error; +pub(crate) use std::fmt::{Debug, Display, Formatter, Result as FormatResult}; +/// Static iteration helper. +#[macro_export] macro_rules! iterate { + ($expr:expr => $arg: pat => $body:expr) => { + let mut iter = $expr; + while let Some(($arg, next)) = iter.next() { + $body; + iter = next; + } + } +} +/// Implement the const iterator pattern. +#[macro_export] macro_rules! const_iter { + ($(<$l:lifetime>)?|$self:ident: $Struct:ty| => $Item:ty => $expr:expr) => { + impl$(<$l>)? Iterator for $Struct { + type Item = $Item; + fn next (&mut $self) -> Option<$Item> { $expr } + } + impl$(<$l>)? ConstIntoIter for $Struct { + type Kind = IsIteratorKind; + type Item = $Item; + type IntoIter = Self; + } + } +} +//#[cfg(test)] #[test] fn test_examples () -> Result<(), ParseError> { + //// Let's pretend to render some view. + //let source = include_str!("../../tek/src/view_arranger.edn"); + //// The token iterator allows you to get the tokens represented by the source text. + //let mut view = TokenIter(source); + //// The token iterator wraps a const token+source iterator. + //assert_eq!(view.0.0, source); + //let mut expr = view.peek(); + //assert_eq!(view.0.0, source); + //assert_eq!(expr, Some(Token { + //source, start: 0, length: source.len() - 1, value: Exp(0, SourceIter(&source[1..])) + //})); + ////panic!("{view:?}"); + ////panic!("{:#?}", expr); + ////for example in [ + ////include_str!("../../tui/examples/edn01.edn"), + ////include_str!("../../tui/examples/edn02.edn"), + ////] { + //////let items = Atom::read_all(example)?; + //////panic!("{layout:?}"); + //////let content = >::from(&layout); + ////} + //Ok(()) +//} diff --git a/edn/src/token.rs b/edn/src/token.rs new file mode 100644 index 0000000..6a5b3e6 --- /dev/null +++ b/edn/src/token.rs @@ -0,0 +1,169 @@ +//! [Token]s are parsed substrings with an associated [Value]. +//! +//! * [ ] FIXME: Value may be [Err] which may shadow [Result::Err] +//! * [Value::Exp] wraps an expression depth and a [SourceIter] +//! with the remaining part of the expression. +//! * expression depth other that 0 mean unclosed parenthesis. +//! * closing and unopened parenthesis panics during reading. +//! * [ ] TODO: signed depth might be interesting +//! * [Value::Sym] and [Value::Key] are stringish literals +//! with slightly different parsing rules. +//! * [Value::Num] is an unsigned integer literal. +//!``` +//! use tek_edn::{*, Value::*}; +//! let source = include_str!("../test.edn"); +//! let mut view = TokenIter::new(source); +//! assert_eq!(view.peek(), Some(Token { +//! source, +//! start: 0, +//! length: source.len(), +//! value: Exp(0, TokenIter::new(&source[1..])) +//! })); +//!``` +use crate::*; +use self::Value::*; +#[derive(Debug, Copy, Clone, Default, PartialEq)] pub struct Token<'a> { + pub source: &'a str, + pub start: usize, + pub length: usize, + pub value: Value<'a>, +} +#[derive(Debug, Copy, Clone, Default, PartialEq)] pub enum Value<'a> { + #[default] Nil, + Err(ParseError), + Num(usize), + Sym(&'a str), + Key(&'a str), + Exp(usize, TokenIter<'a>), +} +impl<'a> Token<'a> { + pub const fn new (source: &'a str, start: usize, length: usize, value: Value<'a>) -> Self { + Self { source, start, length, value } + } + pub const fn end (&self) -> usize { + self.start.saturating_add(self.length) + } + pub const fn slice (&'a self) -> &'a str { + self.slice_source(self.source) + //str_range(self.source, self.start, self.end()) + } + pub const fn slice_source <'b> (&'a self, source: &'b str) -> &'b str { + str_range(source, self.start, self.end()) + } + pub const fn slice_source_exp <'b> (&'a self, source: &'b str) -> &'b str { + str_range(source, self.start.saturating_add(1), self.end()) + } + pub const fn value (&self) -> Value { + self.value + } + pub const fn error (self, error: ParseError) -> Self { + Self { value: Value::Err(error), ..self } + } + pub const fn grow (self) -> Self { + Self { length: self.length.saturating_add(1), ..self } + } + pub const fn grow_num (self, m: usize, c: char) -> Self { + match to_digit(c) { + Ok(n) => Self { value: Num(10*m+n), ..self.grow() }, + Result::Err(e) => Self { value: Err(e), ..self.grow() }, + } + } + pub const fn grow_key (self) -> Self { + let mut token = self.grow(); + token.value = Key(token.slice_source(self.source)); + token + } + pub const fn grow_sym (self) -> Self { + let mut token = self.grow(); + token.value = Sym(token.slice_source(self.source)); + token + } + pub const fn grow_exp (self) -> Self { + let mut token = self.grow(); + if let Exp(depth, _) = token.value { + token.value = Exp(depth, TokenIter::new(token.slice_source_exp(self.source))); + } else { + unreachable!() + } + token + } + pub const fn grow_in (self) -> Self { + let mut token = self.grow_exp(); + if let Value::Exp(depth, source) = token.value { + token.value = Value::Exp(depth.saturating_add(1), source) + } else { + unreachable!() + } + token + } + pub const fn grow_out (self) -> Self { + let mut token = self.grow_exp(); + if let Value::Exp(depth, source) = token.value { + if depth > 0 { + token.value = Value::Exp(depth - 1, source) + } else { + return self.error(Unexpected(')')) + } + } else { + unreachable!() + } + token + } +} +#[cfg(test)] mod test_token_prop { + use proptest::prelude::*; + proptest! { + #[test] fn test_token_prop ( + source in "\\PC*", + start in usize::MIN..usize::MAX, + length in usize::MIN..usize::MAX, + ) { + let token = crate::Token { + source: &source, + start, + length, + value: crate::Value::Nil + }; + let _ = token.slice(); + } + } +} +#[cfg(test)] #[test] fn test_token () -> Result<(), Box> { + let source = ":f00"; + let mut token = Token { source, start: 0, length: 1, value: Sym(":") }; + token = token.grow_sym(); + assert_eq!(token, Token { source, start: 0, length: 2, value: Sym(":f") }); + token = token.grow_sym(); + assert_eq!(token, Token { source, start: 0, length: 3, value: Sym(":f0") }); + token = token.grow_sym(); + assert_eq!(token, Token { source, start: 0, length: 4, value: Sym(":f00") }); + + let src = ""; + assert_eq!(None, SourceIter(src).next()); + + let src = " \n \r \t "; + assert_eq!(None, SourceIter(src).next()); + + let src = "7"; + assert_eq!(Num(7), SourceIter(src).next().unwrap().0.value); + + let src = " 100 "; + assert_eq!(Num(100), SourceIter(src).next().unwrap().0.value); + + let src = " 9a "; + assert_eq!(Err(Unexpected('a')), SourceIter(src).next().unwrap().0.value); + + let src = " :123foo "; + assert_eq!(Sym(":123foo"), SourceIter(src).next().unwrap().0.value); + + let src = " \r\r\r\n\n\n@bar456\t\t\t\t\t\t"; + assert_eq!(Sym("@bar456"), SourceIter(src).next().unwrap().0.value); + + let src = "foo123"; + assert_eq!(Key("foo123"), SourceIter(src).next().unwrap().0.value); + + let src = "foo/bar"; + assert_eq!(Key("foo/bar"), SourceIter(src).next().unwrap().0.value); + + Ok(()) +} diff --git a/edn/test.edn b/edn/test.edn new file mode 100644 index 0000000..7e79d1a --- /dev/null +++ b/edn/test.edn @@ -0,0 +1 @@ +(bsp/n (fixed/y 2 :transport) (bsp/s (fixed/y 2 :status) (fill/xy (bsp/a (fill/xy (align/e :pool)) :arranger)))) diff --git a/input/Cargo.lock b/input/Cargo.lock new file mode 100644 index 0000000..f9911b0 --- /dev/null +++ b/input/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "tek_engine" +version = "0.2.0" diff --git a/input/Cargo.toml b/input/Cargo.toml new file mode 100644 index 0000000..8022543 --- /dev/null +++ b/input/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "tek_input" +edition = "2021" +version = "0.2.0" + +[dependencies] +tek_edn = { path = "../edn" } + +[dev-dependencies] +tek_tui = { path = "../tui" } diff --git a/input/README.md b/input/README.md new file mode 100644 index 0000000..6187e5a --- /dev/null +++ b/input/README.md @@ -0,0 +1,16 @@ +# `tek_engine` + +## rendering + +## input handling + +the **input thread** polls for keyboard events +and passes them onto the application's `Handle::handle` method. + +thus, for a type to be a valid application for engine `E`, +it must implement the trait `Handle`, which allows it +to respond to user input. + +this thread has write access to the application state, +and is responsible for mutating it in response to +user activity. diff --git a/input/src/command.rs b/input/src/command.rs new file mode 100644 index 0000000..23b2464 --- /dev/null +++ b/input/src/command.rs @@ -0,0 +1,28 @@ +use crate::*; +#[macro_export] macro_rules! command { + ($(<$($l:lifetime),+>)?|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => { + impl$(<$($l),+>)? Command<$State> for $Command { + fn execute ($self, $state: &mut $State) -> Perhaps { + Ok($handler) + } + } + }; +} +pub trait Command: Send + Sync + Sized { + fn execute (self, state: &mut S) -> Perhaps; + fn delegate (self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps + where Self: Sized + { + Ok(self.execute(state)?.map(wrap)) + } +} +impl> Command for Option { + fn execute (self, _: &mut S) -> Perhaps { + Ok(None) + } + fn delegate (self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps + where Self: Sized + { + Ok(None) + } +} diff --git a/input/src/event_map.rs b/input/src/event_map.rs new file mode 100644 index 0000000..4a7e260 --- /dev/null +++ b/input/src/event_map.rs @@ -0,0 +1,73 @@ +use crate::*; + +pub struct EventMap<'a, S, I: PartialEq, C> { + pub bindings: &'a [(I, &'a dyn Fn(&S) -> Option)], + pub fallback: Option<&'a dyn Fn(&S, &I) -> Option> +} + +impl<'a, S, I: PartialEq, C> EventMap<'a, S, I, C> { + pub fn handle (&self, state: &S, input: &I) -> Option { + for (binding, handler) in self.bindings.iter() { + if input == binding { + return handler(state) + } + } + if let Some(fallback) = self.fallback { + fallback(state, input) + } else { + None + } + } +} + +#[macro_export] macro_rules! keymap { + ( + $(<$lt:lifetime>)? $KEYS:ident = |$state:ident: $State:ty, $input:ident: $Input:ty| $Command:ty + { $($key:expr => $handler:expr),* $(,)? } $(,)? + ) => { + pub const $KEYS: EventMap<'static, $State, $Input, $Command> = EventMap { + fallback: None, + bindings: &[ $(($key, &|$state|Some($handler)),)* ] + }; + input_to_command!($(<$lt>)? $Command: |$state: $State, input: $Input|$KEYS.handle($state, input)?); + }; + ( + $(<$lt:lifetime>)? $KEYS:ident = |$state:ident: $State:ty, $input:ident: $Input:ty| $Command:ty + { $($key:expr => $handler:expr),* $(,)? }, $default:expr + ) => { + pub const $KEYS: EventMap<'static, $State, $Input, $Command> = EventMap { + fallback: Some(&|$state, $input|Some($default)), + bindings: &[ $(($key, &|$state|Some($handler)),)* ] + }; + input_to_command!($(<$lt>)? $Command: |$state: $State, input: $Input|$KEYS.handle($state, input)?); + }; +} +#[macro_export] macro_rules! input_to_command { + (<$($l:lifetime),+> $Command:ty: |$state:ident:$State:ty, $input:ident:$Input:ty| $handler:expr) => { + impl<$($l),+> InputToCommand<$Input, $State> for $Command { + fn input_to_command ($state: &$State, $input: &$Input) -> Option { + Some($handler) + } + } + }; + ($Command:ty: |$state:ident:$State:ty, $input:ident:$Input:ty| $handler:expr) => { + impl InputToCommand<$Input, $State> for $Command { + fn input_to_command ($state: &$State, $input: &$Input) -> Option { + Some($handler) + } + } + } +} + +pub trait InputToCommand: Command + Sized { + fn input_to_command (state: &S, input: &I) -> Option; + fn execute_with_state (state: &mut S, input: &I) -> Perhaps { + Ok(if let Some(command) = Self::input_to_command(state, input) { + let _undo = command.execute(state)?; + Some(true) + } else { + None + }) + } +} + diff --git a/input/src/input.rs b/input/src/input.rs new file mode 100644 index 0000000..c8b59e3 --- /dev/null +++ b/input/src/input.rs @@ -0,0 +1,75 @@ +use crate::*; +use std::sync::{Mutex, Arc, RwLock}; + +/// Event source +pub trait Input: Send + Sync + Sized { + /// Type of input event + type Event; + /// Result of handling input + type Handled; // TODO: make this an Option> containing the undo + /// Currently handled event + fn event (&self) -> &Self::Event; + /// Whether component should exit + fn is_done (&self) -> bool; + /// Mark component as done + fn done (&self); +} + +/// Implement the [Handle] trait. +#[macro_export] macro_rules! handle { + (|$self:ident:$Struct:ty,$input:ident|$handler:expr) => { + impl Handle for $Struct { + fn handle (&mut $self, $input: &E) -> Perhaps { + $handler + } + } + }; + ($E:ty: |$self:ident:$Struct:ty,$input:ident|$handler:expr) => { + impl Handle<$E> for $Struct { + fn handle (&mut $self, $input: &$E) -> Perhaps<<$E as Input>::Handled> { + $handler + } + } + } +} + +/// Handle input +pub trait Handle: Send + Sync { + fn handle (&mut self, _input: &E) -> Perhaps { + Ok(None) + } +} +impl> Handle for &mut H { + fn handle (&mut self, context: &E) -> Perhaps { + (*self).handle(context) + } +} +impl> Handle for Option { + fn handle (&mut self, context: &E) -> Perhaps { + if let Some(ref mut handle) = self { + handle.handle(context) + } else { + Ok(None) + } + } +} +impl Handle for Mutex where H: Handle { + fn handle (&mut self, context: &E) -> Perhaps { + self.get_mut().unwrap().handle(context) + } +} +impl Handle for Arc> where H: Handle { + fn handle (&mut self, context: &E) -> Perhaps { + self.lock().unwrap().handle(context) + } +} +impl Handle for RwLock where H: Handle { + fn handle (&mut self, context: &E) -> Perhaps { + self.write().unwrap().handle(context) + } +} +impl Handle for Arc> where H: Handle { + fn handle (&mut self, context: &E) -> Perhaps { + self.write().unwrap().handle(context) + } +} diff --git a/input/src/keymap.rs b/input/src/keymap.rs new file mode 100644 index 0000000..73b063e --- /dev/null +++ b/input/src/keymap.rs @@ -0,0 +1,227 @@ +use crate::*; +/// [Input] state that can be matched against a [Value]. +pub trait AtomInput: Input { + fn matches_atom (&self, token: &str) -> bool; +} +pub trait KeyMap<'a> { + /// Try to find a command that matches the current input event. + fn command , I: AtomInput> (&'a self, state: &'a S, input: &'a I) + -> Option; +} +impl<'a> KeyMap<'a> for SourceIter<'a> { + fn command , I: AtomInput> (&'a self, state: &'a S, input: &'a I) + -> Option + { + let mut iter = self.clone(); + while let Some((token, rest)) = iter.next() { + iter = rest; + match token { + Token { value: Value::Exp(0, exp_iter), .. } => { + let mut exp_iter = exp_iter.clone(); + match exp_iter.next() { + Some(Token { value: Value::Sym(binding), .. }) => { + if input.matches_atom(binding) { + if let Some(command) = C::try_from_expr(state, exp_iter.clone()) { + return Some(command) + } + } + }, + _ => panic!("invalid config (expected symbol)") + } + }, + _ => panic!("invalid config (expected expression)") + } + } + None + } +} +impl<'a> KeyMap<'a> for TokenIter<'a> { + fn command , I: AtomInput> (&'a self, state: &'a S, input: &'a I) + -> Option + { + let mut iter = self.clone(); + while let Some(next) = iter.next() { + match next { + Token { value: Value::Exp(0, exp_iter), .. } => { + let mut exp_iter = exp_iter.clone(); + match exp_iter.next() { + Some(Token { value: Value::Sym(binding), .. }) => { + if input.matches_atom(binding) { + if let Some(command) = C::try_from_expr(state, exp_iter.clone()) { + return Some(command) + } + } + }, + _ => panic!("invalid config (expected symbol)") + } + }, + _ => panic!("invalid config (expected expression)") + } + } + None + } +} +/// A [Command] that can be constructed from a [Token]. +pub trait AtomCommand<'a, C>: TryFromAtom<'a, C> + Command {} +impl<'a, C, T: TryFromAtom<'a, C> + Command> AtomCommand<'a, C> for T {} +/** Implement `AtomCommand` for given `State` and `Command` */ +#[macro_export] macro_rules! atom_command { + ($Command:ty : |$state:ident:<$State:ident: $Trait:path>| { $(( + // identifier + $key:literal [ + // named parameters + $( + // argument name + $arg:ident + // if type is not provided defaults to Atom + $( + // type:name separator + : + // argument type + $type:ty + )? + ),* + // rest of parameters + $(, ..$rest:ident)? + ] + // bound command: + $command:expr + ))* }) => { + impl<'a, $State: $Trait> TryFromAtom<'a, $State> for $Command { + fn try_from_expr ($state: &$State, iter: TokenIter) -> Option { + let iter = iter.clone(); + match iter.next() { + $(Some(Token { value: Value::Key($key), .. }) => { + let iter = iter.clone(); + $( + let next = iter.next(); + if next.is_none() { panic!("no argument: {}", stringify!($arg)); } + let $arg = next.unwrap(); + $(let $arg: Option<$type> = Context::<$type>::get($state, &$arg.value);)? + )* + $(let $rest = iter.clone();)? + return $command + },)* + _ => None + } + None + } + } + }; + ($Command:ty : |$state:ident:$State:ty| { $(( + // identifier + $key:literal [ + // named parameters + $( + // argument name + $arg:ident + // if type is not provided defaults to Atom + $( + // type:name separator + : + // argument type + $type:ty + )? + ),* + // rest of parameters + $(, ..$rest:ident)? + ] + // bound command: + $command:expr + ))* }) => { + impl<'a> TryFromAtom<'a, $State> for $Command { + fn try_from_expr ($state: &$State, iter: TokenIter) -> Option { + let mut iter = iter.clone(); + match iter.next() { + $(Some(Token { value: Value::Key($key), .. }) => { + let mut iter = iter.clone(); + $( + let next = iter.next(); + if next.is_none() { panic!("no argument: {}", stringify!($arg)); } + let $arg = next.unwrap(); + $(let $arg: Option<$type> = Context::<$type>::get($state, &$arg.value);)? + )* + $(let $rest = iter.clone();)? + return $command + }),* + _ => None + } + } + } + }; + (@bind $state:ident =>$arg:ident ? : $type:ty) => { + let $arg: Option<$type> = Context::<$type>::get($state, $arg); + }; + (@bind $state:ident => $arg:ident : $type:ty) => { + let $arg: $type = Context::<$type>::get_or_fail($state, $arg); + }; +} +//pub struct SourceKeyMap<'a>(&'a str); +//impl<'a> KeyMap for SourceKeyMap<'a> { + //fn command > (&self, state: &S, input: &AtomInput) -> Option { + //todo!(); + //None + //} +//} +//pub struct ParsedKeyMap<'a>(TokensIterator<'a>); +//impl<'a> KeyMap for ParsedKeyMap<'a> { + //fn command > (&self, state: &S, input: &AtomInput) -> Option { + //todo!(); + //None + //} +//} +//pub struct RefKeyMap<'a>(TokensIterator<'a>); +//impl<'a> KeyMap for RefKeyMap<'a> { + //fn command > (&self, state: &S, input: &AtomInput) -> Option { + //todo!(); + ////for token in self.0 { + ////match token?.kind() { + ////TokenKind::Exp => match atoms.as_slice() { + ////[key, command, args @ ..] => match (key.kind(), key.text()) { + ////(TokenKind::Sym, key) => { + ////if input.matches_atom(key) { + ////let command = C::from_atom(state, command, args); + ////if command.is_some() { + ////return command + ////} + ////} + ////}, + ////_ => panic!("invalid config: {item}") + ////}, + ////_ => panic!("invalid config: {item}") + ////} + ////_ => panic!("invalid config: {item}") + ////} + ////} + //None + //} +//} +//pub struct ArcKeyMap(Vec); +//impl KeyMap for ArcKeyMap { + //fn command > (&self, state: &S, input: &AtomInput) -> Option { + //for atom in self.0.iter() { + //match atom { + //ArcAtom::Exp(atoms) => match atoms.as_slice() { + //[key, command, args @ ..] => match (key.kind(), key.text()) { + //(TokenKind::Sym, key) => { + //if input.matches_atom(key) { + //let command = C::from_atom(state, command, args); + //if command.is_some() { + //return command + //} + //} + //}, + //_ => panic!("invalid config: {atom}") + //}, + //_ => panic!("invalid config: {atom}") + //} + //_ => panic!("invalid config: {atom}") + //} + //} + //None + //} +//} +#[cfg(test)] #[test] fn test_atom_keymap () -> Usually<()> { + let keymap = SourceIter::new(""); + Ok(()) +} diff --git a/input/src/lib.rs b/input/src/lib.rs new file mode 100644 index 0000000..c6a0838 --- /dev/null +++ b/input/src/lib.rs @@ -0,0 +1,32 @@ +#![feature(associated_type_defaults)] +mod input; pub use self::input::*; +mod command; pub use self::command::*; +mod keymap; pub use self::keymap::*; +//mod event_map; pub use self::event_map::*; +pub(crate) use ::tek_edn::*; +/// Standard error trait. +pub(crate) use std::error::Error; +/// Standard result type. +#[cfg(test)] pub(crate) type Usually = Result>; +/// Standard optional result type. +pub(crate) type Perhaps = Result, Box>; +#[cfg(test)] #[test] fn test_stub_input () -> Usually<()> { + use crate::*; + struct TestInput(bool); + enum TestEvent { Test1 } + impl Input for TestInput { + type Event = TestEvent; + type Handled = (); + fn event (&self) -> &Self::Event { + &TestEvent::Test1 + } + fn is_done (&self) -> bool { + self.0 + } + fn done (&self) {} + } + let _ = TestInput(true).event(); + assert!(TestInput(true).is_done()); + assert!(!TestInput(false).is_done()); + Ok(()) +} diff --git a/output/Cargo.lock b/output/Cargo.lock new file mode 100644 index 0000000..b6f132d --- /dev/null +++ b/output/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "tek_engine" +version = "0.2.0" + +[[package]] +name = "tek_layout" +version = "0.2.0" +dependencies = [ + "tek_engine", +] diff --git a/output/Cargo.toml b/output/Cargo.toml new file mode 100644 index 0000000..3a708ad --- /dev/null +++ b/output/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tek_output" +edition = "2021" +version = "0.2.0" + +[dependencies] +tek_edn = { path = "../edn" } + +[dev-dependencies] +tek_tui = { path = "../tui" } +proptest = "^1" +proptest-derive = "^0.5.1" diff --git a/output/README.md b/output/README.md new file mode 100644 index 0000000..4f575b6 --- /dev/null +++ b/output/README.md @@ -0,0 +1,76 @@ +# `tek_output` + +## free floating layout primitives + +this crate exposes several layout operators +which work entirely in unsigned coordinates +and are generic over `tek_engine::Engine` +and `tek_engine::Content`. chiefly, they +are not dependent on rendering framework. + +|operator|description| +|-|-| +|**`When(x, a)`**|render `a` only when `x == true`| +|**`Either(x, a, b)`**|render `a` when `x == true`, otherwise render `b`| +|**`Map(get_iterator, callback)`**|transform items in uniform way| +|**`Bsp`**|concatenative layout| +|...|...| +|**`Align`**|pin content along axis| +|...|...| +|**`Fill`**|**make content's dimension equal to container's:**| +|`Fill::x(a)`|use container's width for content| +|`Fill::y(a)`|use container's height for content| +|`Fill::xy(a)`|use container's width and height for content| +|**`Fixed`**|**assign fixed dimension to content:**| +|`Fixed::x(w, a)`|use width `w` for content| +|`Fixed::y(w, a)`|use height `w` for content| +|`Fixed::xy(w, h, a)`|use width `w` and height `h` for content| +|**`Expand`/`Shrink`**|**change dimension of content:**| +|`Expand::x(n, a)`/`Shrink::x(n, a)`|increment/decrement width of content area by `n`| +|`Expand::y(n, a)`/`Shrink::y(n, a)`|increment/decrement height of content area by `m`| +|`Expand::xy(n, m, a)`/`Shrink::xy(n, m, a)`|increment/decrement width of content area by `n`, height by `m`| +|**`Min`/`Max`**|**constrain dimension of content:**| +|`Min::x(w, a)`/`Max::x(w, a)`|enforce minimum/maximum width `w` for content| +|`Min::y(h, a)`/`Max::y(h, a)`|enforce minimum/maximum height `h` for content| +|`Min::xy(w, h, a)`/`Max::xy(w, h, a)`|enforce minimum/maximum width `w` and height `h` for content| +|**`Push`/`Pull`**|**move content along axis:**| +|`Push::x(n, a)`/`Pull::x(n, a)`|increment/decrement `x` of content area| +|`Push::y(n, a)`/`Pull::y(n, a)`|increment/decrement `y` of content area| +|`Push::xy(n, m, a)`/`Pull::xy(n, m, a)`|increment/decrement `x` and `y` of content area| + +**todo:** +* sensible `Margin`/`Padding` +* `Reduce` + +## example rendering loop + +the **render thread** continually invokes the +`Content::render` method of the application +to redraw the display. it does this efficiently +by using ratatui's double buffering. + +thus, for a type to be a valid application for engine `E`, +it must implement the trait `Content`, which allows +it to display content to the engine's output. + +the most important thing about the `Content` trait is that +it composes: +* you can implement `Content::content` to build + `Content`s out of other `Content`s +* and/or `Content::area` for custom positioning and sizing, +* and/or `Content::render` for custom rendering + within the given `Content`'s area. + +the manner of output is determined by the +`Engine::Output` type, a mutable pointer to which +is passed to the render method, e.g. in the case of +the `Tui` engine: `fn render(&self, output: &mut TuiOut)` + +you can use `TuiOut::blit` and `TuiOut::place` +to draw at specified coordinates of the display, and/or +directly modify the underlying `ratatui::Buffer` at +`output.buffer` + +rendering is intended to work with read-only access +to the application state. if you really need to update +values during rendering, use interior mutability. diff --git a/output/proptest-regressions/area.txt b/output/proptest-regressions/area.txt new file mode 100644 index 0000000..1c957a5 --- /dev/null +++ b/output/proptest-regressions/area.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc d2cd65ec39a1bf43c14bb2d3196c7e84ba854411360e570f06dd7ede62b0fd61 # shrinks to x = 0, y = 43998, w = 0, h = 43076, a = 0, b = 0 diff --git a/output/proptest-regressions/direction.txt b/output/proptest-regressions/direction.txt new file mode 100644 index 0000000..76b3fac --- /dev/null +++ b/output/proptest-regressions/direction.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 5b236150b286e479089d5bf6accc8ffbc3c0b0a1f955682af1987f342930d31e # shrinks to x = 0, y = 0, w = 0, h = 0, a = 1 diff --git a/output/proptest-regressions/op_transform.txt b/output/proptest-regressions/op_transform.txt new file mode 100644 index 0000000..5981536 --- /dev/null +++ b/output/proptest-regressions/op_transform.txt @@ -0,0 +1,10 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc b05b448ca4eb29304cae506927639494cae99a9e1ab40c58ac9dcb70d1ea1298 # shrinks to op_x = Some(0), op_y = None, content = "", x = 0, y = 46377, w = 0, h = 38318 +cc efdb7136c68396fa7c632cc6d3b304545ada1ba134269278f890639559a17575 # shrinks to op_x = Some(0), op_y = Some(32768), content = "", x = 0, y = 0, w = 0, h = 0 +cc f6d43c39db04f4c0112fe998ef68cff0a4454cd9791775a3014cc81997fbadf4 # shrinks to op_x = Some(10076), op_y = None, content = "", x = 60498, y = 0, w = 0, h = 0 +cc 3cabc97f3fa3a83fd5f8cf2c619ed213c2be5e9b1cb13e5178bde87dd838e2f4 # shrinks to op_x = Some(3924), op_y = None, content = "", x = 63574, y = 0, w = 0, h = 0 diff --git a/output/src/area.rs b/output/src/area.rs new file mode 100644 index 0000000..261ba54 --- /dev/null +++ b/output/src/area.rs @@ -0,0 +1,137 @@ +use crate::*; +use std::fmt::Debug; + +pub trait Area: From<[N;4]> + Debug + Copy { + fn x (&self) -> N; + fn y (&self) -> N; + fn w (&self) -> N; + fn h (&self) -> N; + #[inline] fn zero () -> [N;4] { + [N::zero(), N::zero(), N::zero(), N::zero()] + } + #[inline] fn from_position (pos: impl Size) -> [N;4] { + let [x, y] = pos.wh(); + [x, y, 0.into(), 0.into()] + } + #[inline] fn from_size (size: impl Size) -> [N;4] { + let [w, h] = size.wh(); + [0.into(), 0.into(), w, h] + } + #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { + Err(format!("min {w}x{h}").into()) + } else { + Ok(self) + } + } + #[inline] fn xy (&self) -> [N;2] { + [self.x(), self.y()] + } + #[inline] fn wh (&self) -> [N;2] { + [self.w(), self.h()] + } + #[inline] fn xywh (&self) -> [N;4] { + [self.x(), self.y(), self.w(), self.h()] + } + #[inline] fn clip_h (&self, h: N) -> [N;4] { + [self.x(), self.y(), self.w(), self.h().min(h)] + } + #[inline] fn clip_w (&self, w: N) -> [N;4] { + [self.x(), self.y(), self.w().min(w), self.h()] + } + #[inline] fn clip (&self, wh: impl Size) -> [N;4] { + [self.x(), self.y(), wh.w(), wh.h()] + } + #[inline] fn set_w (&self, w: N) -> [N;4] { + [self.x(), self.y(), w, self.h()] + } + #[inline] fn set_h (&self, h: N) -> [N;4] { + [self.x(), self.y(), self.w(), h] + } + #[inline] fn x2 (&self) -> N { + self.x().plus(self.w()) + } + #[inline] fn y2 (&self) -> N { + self.y().plus(self.h()) + } + #[inline] fn lrtb (&self) -> [N;4] { + [self.x(), self.x2(), self.y(), self.y2()] + } + #[inline] fn center (&self) -> [N;2] { + [self.x().plus(self.w()/2.into()), self.y().plus(self.h()/2.into())] + } + #[inline] fn center_x (&self, n: N) -> [N;4] { + let [x, y, w, h] = self.xywh(); + [(x.plus(w / 2.into())).minus(n / 2.into()), y.plus(h / 2.into()), n, 1.into()] + } + #[inline] fn center_y (&self, n: N) -> [N;4] { + let [x, y, w, h] = self.xywh(); + [x.plus(w / 2.into()), (y.plus(h / 2.into())).minus(n / 2.into()), 1.into(), n] + } + #[inline] fn center_xy (&self, [n, m]: [N;2]) -> [N;4] { + let [x, y, w, h] = self.xywh(); + [(x.plus(w / 2.into())).minus(n / 2.into()), (y.plus(h / 2.into())).minus(m / 2.into()), n, m] + } + + #[inline] fn centered (&self) -> [N;2] { + [self.x().minus(self.w()/2.into()), self.y().minus(self.h()/2.into())] + } + + fn iter_x (&self) -> impl Iterator where N: std::iter::Step { + self.x()..(self.x()+self.w()) + } + fn iter_y (&self) -> impl Iterator where N: std::iter::Step { + self.y()..(self.y()+self.h()) + } +} + +impl Area for (N, N, N, N) { + #[inline] fn x (&self) -> N { self.0 } + #[inline] fn y (&self) -> N { self.1 } + #[inline] fn w (&self) -> N { self.2 } + #[inline] fn h (&self) -> N { self.3 } +} + +impl Area for [N;4] { + #[inline] fn x (&self) -> N { self[0] } + #[inline] fn y (&self) -> N { self[1] } + #[inline] fn w (&self) -> N { self[2] } + #[inline] fn h (&self) -> N { self[3] } +} + +#[cfg(test)] mod test_area { + use super::*; + use proptest::prelude::*; + proptest! { + #[test] fn test_area_prop ( + x in u16::MIN..u16::MAX, + y in u16::MIN..u16::MAX, + w in u16::MIN..u16::MAX, + h in u16::MIN..u16::MAX, + a in u16::MIN..u16::MAX, + b in u16::MIN..u16::MAX, + ) { + let _: [u16;4] = <[u16;4] as Area>::zero(); + let _: [u16;4] = <[u16;4] as Area>::from_position([a, b]); + let _: [u16;4] = <[u16;4] as Area>::from_size([a, b]); + let area: [u16;4] = [x, y, w, h]; + let _ = area.expect_min(a, b); + let _ = area.xy(); + let _ = area.wh(); + let _ = area.xywh(); + let _ = area.clip_h(a); + let _ = area.clip_w(b); + let _ = area.clip([a, b]); + let _ = area.set_w(a); + let _ = area.set_h(b); + let _ = area.x2(); + let _ = area.y2(); + let _ = area.lrtb(); + let _ = area.center(); + let _ = area.center_x(a); + let _ = area.center_y(b); + let _ = area.center_xy([a, b]); + let _ = area.centered(); + } + } +} diff --git a/output/src/collection.rs b/output/src/collection.rs new file mode 100644 index 0000000..5559afe --- /dev/null +++ b/output/src/collection.rs @@ -0,0 +1,23 @@ +//! Groupings of elements. +use crate::*; + +/// A function or closure that emits renderables. +pub trait Collector: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)) {} + +/// Any function or closure that emits renderables for the given engine matches [CollectCallback]. +impl Collector for F +where E: Engine, F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)) {} + +pub trait Render { + fn area (&self, to: E::Area) -> E::Area; + fn render (&self, to: &mut E::Output); +} + +impl> Render for C { + fn area (&self, to: E::Area) -> E::Area { + Content::area(self, to) + } + fn render (&self, to: &mut E::Output) { + Content::render(self, to) + } +} diff --git a/output/src/coordinate.rs b/output/src/coordinate.rs new file mode 100644 index 0000000..9523fbc --- /dev/null +++ b/output/src/coordinate.rs @@ -0,0 +1,31 @@ +use std::fmt::{Debug, Display}; +use std::ops::{Add, Sub, Mul, Div}; + +impl Coordinate for u16 { + #[inline] fn plus (self, other: Self) -> Self { + self.saturating_add(other) + } +} + +/// A linear coordinate. +pub trait Coordinate: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into +{ + #[inline] fn zero () -> Self { 0.into() } + #[inline] fn minus (self, other: Self) -> Self { + if self >= other { + self - other + } else { + 0.into() + } + } + fn plus (self, other: Self) -> Self; +} diff --git a/output/src/direction.rs b/output/src/direction.rs new file mode 100644 index 0000000..afa2139 --- /dev/null +++ b/output/src/direction.rs @@ -0,0 +1,38 @@ +use crate::*; +#[cfg(test)] use proptest_derive::Arbitrary; +/// A cardinal direction. +#[derive(Copy, Clone, PartialEq, Debug)] +#[cfg_attr(test, derive(Arbitrary))] +pub enum Direction { North, South, East, West, Above, Below } +impl Direction { + pub fn split_fixed (self, area: impl Area, a: N) -> ([N;4],[N;4]) { + let [x, y, w, h] = area.xywh(); + match self { + North => ([x, y.plus(h).minus(a), w, a], [x, y, w, h.minus(a)]), + South => ([x, y, w, a], [x, y.plus(a), w, h.minus(a)]), + East => ([x, y, a, h], [x.plus(a), y, w.minus(a), h]), + West => ([x.plus(w).minus(a), y, a, h], [x, y, w.minus(a), h]), + Above | Below => (area.xywh(), area.xywh()) + } + } +} +#[cfg(test)] mod test { + use super::*; + use proptest::prelude::*; + proptest! { + #[test] fn proptest_direction ( + d in prop_oneof![ + Just(North), Just(South), + Just(East), Just(West), + Just(Above), Just(Below) + ], + x in u16::MIN..u16::MAX, + y in u16::MIN..u16::MAX, + w in u16::MIN..u16::MAX, + h in u16::MIN..u16::MAX, + a in u16::MIN..u16::MAX, + ) { + let _ = d.split_fixed([x, y, w, h], a); + } + } +} diff --git a/output/src/lib.rs b/output/src/lib.rs new file mode 100644 index 0000000..f6353d1 --- /dev/null +++ b/output/src/lib.rs @@ -0,0 +1,55 @@ +//#![feature(lazy_type_alias)] +#![feature(step_trait)] +#![feature(type_alias_impl_trait)] +#![feature(impl_trait_in_assoc_type)] +mod direction; pub use self::direction::*; +mod coordinate; pub use self::coordinate::*; +mod size; pub use self::size::*; +mod area; pub use self::area::*; +mod output; pub use self::output::*; +mod measure; pub use self::measure::*; +mod thunk; pub use self::thunk::*; +mod op_cond; pub use self::op_cond::*; +mod op_iter; pub use self::op_iter::*; +mod op_align; pub use self::op_align::*; +mod op_bsp; pub use self::op_bsp::*; +mod op_transform; pub use self::op_transform::*; +mod view; pub use self::view::*; +pub(crate) use std::marker::PhantomData; +pub(crate) use std::error::Error; +pub(crate) use ::tek_edn::*; +/// Standard result type. +pub type Usually = Result>; +/// Standard optional result type. +pub type Perhaps = Result, Box>; +#[cfg(test)] #[test] fn test_stub_output () -> Usually<()> { + use crate::*; + struct TestOutput([u16;4]); + impl Output for TestOutput { + type Unit = u16; + type Size = [u16;2]; + type Area = [u16;4]; + fn area (&self) -> [u16;4] { + self.0 + } + fn area_mut (&mut self) -> &mut [u16;4] { + &mut self.0 + } + fn place (&mut self, _: [u16;4], _: &impl Render) { + () + } + } + impl Content for String { + fn render (&self, to: &mut TestOutput) { + to.area_mut().set_w(self.len() as u16); + } + } + Ok(()) +} +#[cfg(test)] #[test] fn test_dimensions () { + use crate::*; + assert_eq!(Area::center(&[10u16, 10, 20, 20]), [20, 20]); +} +#[cfg(test)] #[test] fn test_layout () -> Usually<()> { + Ok(()) +} diff --git a/output/src/measure.rs b/output/src/measure.rs new file mode 100644 index 0000000..784f0a2 --- /dev/null +++ b/output/src/measure.rs @@ -0,0 +1,144 @@ +use crate::*; +use std::sync::{Arc, atomic::{AtomicUsize, Ordering::Relaxed}}; +//use ratatui::prelude::{Style, Color}; +// TODO: 🡘 🡙 ←🡙→ indicator to expand window when too small +pub trait HasSize { + fn size (&self) -> &Measure; +} +#[macro_export] macro_rules! has_size { + (<$E:ty>|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasSize<$E> for $Struct $(<$($L),*$($T),*>)? { + fn size (&$self) -> &Measure<$E> { $cb } + } + } +} +/// A widget that tracks its render width and height +#[derive(Default)] +pub struct Measure { + _engine: PhantomData, + pub x: Arc, + pub y: Arc, +} +impl Content for Measure { + fn render (&self, to: &mut E) { + self.x.store(to.area().w().into(), Relaxed); + self.y.store(to.area().h().into(), Relaxed); + } +} +impl Clone for Measure { + fn clone (&self) -> Self { + Self { + _engine: Default::default(), + x: self.x.clone(), + y: self.y.clone(), + } + } +} +impl std::fmt::Debug for Measure { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Measure") + .field("width", &self.x) + .field("height", &self.y) + .finish() + } +} +impl Measure { + pub fn new () -> Self { + Self { + _engine: PhantomData::default(), + x: Arc::new(0.into()), + y: Arc::new(0.into()), + } + } + pub fn set_w (&self, w: impl Into) -> &Self { + self.x.store(w.into(), Relaxed); + self + } + pub fn set_h (&self, h: impl Into) -> &Self { + self.y.store(h.into(), Relaxed); + self + } + pub fn set_wh (&self, w: impl Into, h: impl Into) -> &Self { + self.set_w(w); + self.set_h(h); + self + } + pub fn w (&self) -> usize { + self.x.load(Relaxed) + } + pub fn h (&self) -> usize { + self.y.load(Relaxed) + } + pub fn wh (&self) -> [usize;2] { + [self.w(), self.h()] + } + pub fn format (&self) -> Arc { + format!("{}x{}", self.w(), self.h()).into() + } + pub fn of > (&self, item: T) -> Bsp, T> { + Bsp::b(Fill::xy(self), item) + } +} +//#[cfg(test)] #[test] fn test_measure () { + //use tek_tui::*; + //let size: Measure = Measure::default().set_w(1usize).set_h(1usize).clone(); + //let size: Measure = (&Measure::new().set_wh(2usize, 1usize)).clone(); + //let _ = format!("{:?}", &size); + //let _ = size.wh(); + //let _ = size.format(); + //let _ = size.of(()); +//} + +///// A scrollable area. +//pub struct Scroll(pub F, pub Direction, pub u64, PhantomData) +//where + //E: Output, + //F: Send + Sync + Fn(&mut dyn FnMut(&dyn Content)->Usually<()>)->Usually<()>; + +//pub trait ContentDebug { + //fn debug > (other: W) -> DebugOverlay { + //DebugOverlay(Default::default(), other) + //} +//} + +//impl ContentDebug for E {} + +//impl Render for Measure { + //fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { + //Ok(Some([0u16.into(), 0u16.into()].into())) + //} + //fn render (&self, to: &mut TuiOut) -> Usually<()> { + //self.set_w(to.area().w()); + //self.set_h(to.area().h()); + //Ok(()) + //} +//} + +//impl Measure { + //pub fn debug (&self) -> ShowMeasure { + //ShowMeasure(&self) + //} +//} + +//render!(Tui: |self: ShowMeasure<'a>|render(|to: &mut TuiOut|Ok({ + //let w = self.0.w(); + //let h = self.0.h(); + //to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some( + //Style::default().bold().italic().bg(Color::Rgb(255, 0, 255)).fg(Color::Rgb(0,0,0)) + //)) +//}))); + +//pub struct ShowMeasure<'a>(&'a Measure); + +//pub struct DebugOverlay>(PhantomData, pub W); + +//impl> Render for DebugOverlay { + //fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + //self.1.min_size(to) + //} + //fn render (&self, to: &mut TuiOut) -> Usually<()> { + //let [x, y, w, h] = to.area(); + //self.1.render(to)?; + //Ok(to.blit(&format!("{w}x{h}+{x}+{y}"), x, y, Some(Style::default().green()))) + //} +//} diff --git a/output/src/op_align.rs b/output/src/op_align.rs new file mode 100644 index 0000000..8ce3695 --- /dev/null +++ b/output/src/op_align.rs @@ -0,0 +1,102 @@ +//! Aligns things to the container. Comes with caveats. +//! ``` +//! use ::tek_tui::{*, tek_output::*}; +//! let area: [u16;4] = [10, 10, 20, 20]; +//! fn test (area: [u16;4], item: &impl Content, expected: [u16;4]) { +//! assert_eq!(Content::layout(item, area), expected); +//! assert_eq!(Render::layout(item, area), expected); +//! }; +//! +//! let four = ||Fixed::xy(4, 4, ""); +//! test(area, &Align::nw(four()), [10, 10, 4, 4]); +//! test(area, &Align::n(four()), [18, 10, 4, 4]); +//! test(area, &Align::ne(four()), [26, 10, 4, 4]); +//! test(area, &Align::e(four()), [26, 18, 4, 4]); +//! test(area, &Align::se(four()), [26, 26, 4, 4]); +//! test(area, &Align::s(four()), [18, 26, 4, 4]); +//! test(area, &Align::sw(four()), [10, 26, 4, 4]); +//! test(area, &Align::w(four()), [10, 18, 4, 4]); +//! +//! let two_by_four = ||Fixed::xy(4, 2, ""); +//! test(area, &Align::nw(two_by_four()), [10, 10, 4, 2]); +//! test(area, &Align::n(two_by_four()), [18, 10, 4, 2]); +//! test(area, &Align::ne(two_by_four()), [26, 10, 4, 2]); +//! test(area, &Align::e(two_by_four()), [26, 19, 4, 2]); +//! test(area, &Align::se(two_by_four()), [26, 28, 4, 2]); +//! test(area, &Align::s(two_by_four()), [18, 28, 4, 2]); +//! test(area, &Align::sw(two_by_four()), [10, 28, 4, 2]); +//! test(area, &Align::w(two_by_four()), [10, 19, 4, 2]); +//! ``` +use crate::*; +#[derive(Debug, Copy, Clone, Default)] pub enum Alignment { #[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W } +pub struct Align(Alignment, A); +try_from_expr!(<'a, E>: Align>: |state, iter| + if let Some(Token { value: Value::Key(key), .. }) = iter.peek() { + match key { + "align/c"|"align/x"|"align/y"| + "align/n"|"align/s"|"align/e"|"align/w"| + "align/nw"|"align/sw"|"align/ne"|"align/se" => { + let _ = iter.next().unwrap(); + let c = iter.next().expect("no content specified"); + let c = state.get_content(&c.value).expect("no content provided"); + return Some(match key { + "align/c" => Self::c(c), + "align/x" => Self::x(c), + "align/y" => Self::y(c), + "align/n" => Self::n(c), + "align/s" => Self::s(c), + "align/e" => Self::e(c), + "align/w" => Self::w(c), + "align/nw" => Self::nw(c), + "align/ne" => Self::ne(c), + "align/sw" => Self::sw(c), + "align/se" => Self::se(c), + _ => unreachable!() + }) + }, + _ => return None + } + }); +impl Align { + #[inline] pub fn c (a: A) -> Self { Self(Alignment::Center, a) } + #[inline] pub fn x (a: A) -> Self { Self(Alignment::X, a) } + #[inline] pub fn y (a: A) -> Self { Self(Alignment::Y, a) } + #[inline] pub fn n (a: A) -> Self { Self(Alignment::N, a) } + #[inline] pub fn s (a: A) -> Self { Self(Alignment::S, a) } + #[inline] pub fn e (a: A) -> Self { Self(Alignment::E, a) } + #[inline] pub fn w (a: A) -> Self { Self(Alignment::W, a) } + #[inline] pub fn nw (a: A) -> Self { Self(Alignment::NW, a) } + #[inline] pub fn sw (a: A) -> Self { Self(Alignment::SW, a) } + #[inline] pub fn ne (a: A) -> Self { Self(Alignment::NE, a) } + #[inline] pub fn se (a: A) -> Self { Self(Alignment::SE, a) } +} +impl> Content for Align { + fn content (&self) -> impl Render { + &self.1 + } + fn layout (&self, on: E::Area) -> E::Area { + use Alignment::*; + let it = Render::layout(&self.content(), on).xywh(); + let cx = on.x()+(on.w().minus(it.w())/2.into()); + let cy = on.y()+(on.h().minus(it.h())/2.into()); + let fx = (on.x()+on.w()).minus(it.w()); + let fy = (on.y()+on.h()).minus(it.h()); + let [x, y] = match self.0 { + Center => [cx, cy], + X => [cx, it.y()], + Y => [it.x(), cy], + NW => [on.x(), on.y()], + N => [cx, on.y()], + NE => [fx, on.y()], + W => [on.x(), cy], + E => [fx, cy], + SW => [on.x(), fy], + S => [cx, fy], + SE => [fx, fy], + }.into(); + [x, y, it.w(), it.h()].into() + } + fn render (&self, to: &mut E) { + to.place(Content::layout(self, to.area()), &self.content()) + } +} diff --git a/output/src/op_bsp.rs b/output/src/op_bsp.rs new file mode 100644 index 0000000..5846750 --- /dev/null +++ b/output/src/op_bsp.rs @@ -0,0 +1,143 @@ +use crate::*; +pub use Direction::*; +/// A split or layer. +pub struct Bsp(Direction, X, Y); +impl, B: Content> Content for Bsp { + fn layout (&self, outer: E::Area) -> E::Area { + let [_, _, c] = self.areas(outer); + c + } + fn render (&self, to: &mut E) { + let [area_a, area_b, _] = self.areas(to.area()); + let (a, b) = self.contents(); + match self.0 { + Below => { to.place(area_a, a); to.place(area_b, b); }, + _ => { to.place(area_b, b); to.place(area_a, a); } + } + } +} +try_from_expr!(<'a, E>: Bsp, RenderBox<'a, E>>: |state, iter| { + if let Some(Token { value: Value::Key(key), .. }) = iter.peek() { + match key { + "bsp/n"|"bsp/s"|"bsp/e"|"bsp/w"|"bsp/a"|"bsp/b" => { + let _ = iter.next().unwrap(); + let c1 = iter.next().expect("no content1 specified"); + let c2 = iter.next().expect("no content2 specified"); + let c1 = state.get_content(&c1.value).expect("no content1 provided"); + let c2 = state.get_content(&c2.value).expect("no content2 provided"); + return Some(match key { + "bsp/n" => Self::n(c1, c2), + "bsp/s" => Self::s(c1, c2), + "bsp/e" => Self::e(c1, c2), + "bsp/w" => Self::w(c1, c2), + "bsp/a" => Self::a(c1, c2), + "bsp/b" => Self::b(c1, c2), + _ => unreachable!(), + }) + }, + _ => return None + } + } +}); +impl Bsp { + #[inline] pub fn n (a: A, b: B) -> Self { Self(North, a, b) } + #[inline] pub fn s (a: A, b: B) -> Self { Self(South, a, b) } + #[inline] pub fn e (a: A, b: B) -> Self { Self(East, a, b) } + #[inline] pub fn w (a: A, b: B) -> Self { Self(West, a, b) } + #[inline] pub fn a (a: A, b: B) -> Self { Self(Above, a, b) } + #[inline] pub fn b (a: A, b: B) -> Self { Self(Below, a, b) } +} +pub trait BspAreas, B: Content> { + fn direction (&self) -> Direction; + fn contents (&self) -> (&A, &B); + fn areas (&self, outer: E::Area) -> [E::Area;3] { + let direction = self.direction(); + let [x, y, w, h] = outer.xywh(); + let (a, b) = self.contents(); + let [aw, ah] = a.layout(outer).wh(); + let [bw, bh] = b.layout(match direction { + Above | Below => outer, + South => [x, y + ah, w, h.minus(ah)].into(), + North => [x, y, w, h.minus(ah)].into(), + East => [x + aw, y, w.minus(aw), h].into(), + West => [x, y, w.minus(aw), h].into(), + }).wh(); + match direction { + Above | Below => { + let [x, y, w, h] = outer.center_xy([aw.max(bw), ah.max(bh)]); + let a = [(x + w/2.into()).minus(aw/2.into()), (y + h/2.into()).minus(ah/2.into()), aw, ah]; + let b = [(x + w/2.into()).minus(bw/2.into()), (y + h/2.into()).minus(bh/2.into()), bw, bh]; + [a.into(), b.into(), [x, y, w, h].into()] + }, + South => { + let [x, y, w, h] = outer.center_xy([aw.max(bw), ah + bh]); + let a = [(x + w/2.into()).minus(aw/2.into()), y, aw, ah]; + let b = [(x + w/2.into()).minus(bw/2.into()), y + ah, bw, bh]; + [a.into(), b.into(), [x, y, w, h].into()] + }, + North => { + let [x, y, w, h] = outer.center_xy([aw.max(bw), ah + bh]); + let a = [(x + (w/2.into())).minus(aw/2.into()), y + bh, aw, ah]; + let b = [(x + (w/2.into())).minus(bw/2.into()), y, bw, bh]; + [a.into(), b.into(), [x, y, w, h].into()] + }, + East => { + let [x, y, w, h] = outer.center_xy([aw + bw, ah.max(bh)]); + let a = [x, (y + h/2.into()).minus(ah/2.into()), aw, ah]; + let b = [x + aw, (y + h/2.into()).minus(bh/2.into()), bw, bh]; + [a.into(), b.into(), [x, y, w, h].into()] + }, + West => { + let [x, y, w, h] = outer.center_xy([aw + bw, ah.max(bh)]); + let a = [x + bw, (y + h/2.into()).minus(ah/2.into()), aw, ah]; + let b = [x, (y + h/2.into()).minus(bh/2.into()), bw, bh]; + [a.into(), b.into(), [x, y, w, h].into()] + }, + } + } +} +impl, B: Content> BspAreas for Bsp { + fn direction (&self) -> Direction { self.0 } + fn contents (&self) -> (&A, &B) { (&self.1, &self.2) } +} +/// Renders multiple things on top of each other, +#[macro_export] macro_rules! lay { + ($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)*; bsp }} +} +/// Stack southward. +#[macro_export] macro_rules! col { + ($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)*; bsp }}; +} +/// Stack northward. +#[macro_export] macro_rules! col_up { + ($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)*; bsp }} +} +/// Stack eastward. +#[macro_export] macro_rules! row { + ($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)*; bsp }}; +} +#[cfg(test)] mod test { + use super::*; + use proptest::prelude::*; + proptest! { + #[test] fn proptest_op_bsp ( + d in prop_oneof![ + Just(North), Just(South), + Just(East), Just(West), + Just(Above), Just(Below) + ], + a in "\\PC*", + b in "\\PC*", + x in u16::MIN..u16::MAX, + y in u16::MIN..u16::MAX, + w in u16::MIN..u16::MAX, + h in u16::MIN..u16::MAX, + ) { + let bsp = Bsp(d, a, b); + assert_eq!( + Content::layout(&bsp, [x, y, w, h]), + Render::layout(&bsp, [x, y, w, h]), + ); + } + } +} diff --git a/output/src/op_cond.rs b/output/src/op_cond.rs new file mode 100644 index 0000000..7f5ad97 --- /dev/null +++ b/output/src/op_cond.rs @@ -0,0 +1,57 @@ +use crate::*; +/// Show an item only when a condition is true. +pub struct When(pub bool, pub A); +impl When { #[inline] pub fn new (c: bool, a: A) -> Self { Self(c, a) } } +/// Show one item if a condition is true and another if the condition is false +pub struct Either(pub bool, pub A, pub B); +impl Either { #[inline] pub fn new (c: bool, a: A, b: B) -> Self { Self(c, a, b) } } +try_from_expr!(<'a, E>: When>: |state, iter| { + if let Some(Token { value: Value::Key("when"), .. }) = iter.peek() { + let _ = iter.next().unwrap(); + let condition = iter.next().expect("no condition specified"); + let content = iter.next().expect("no content specified"); + let condition = state.get(&condition.value).expect("no condition provided"); + let content = state.get_content(&content.value).expect("no content provided"); + return Some(Self(condition, content)) + } +}); +try_from_expr!(<'a, E>: Either, RenderBox<'a, E>>: |state, iter| { + if let Some(Token { value: Value::Key("either"), .. }) = iter.peek() { + let _ = iter.next().unwrap(); + let condition = iter.next().expect("no condition specified"); + let content = iter.next().expect("no content specified"); + let alternate = iter.next().expect("no alternate specified"); + let condition = state.get(&condition.value).expect("no condition provided"); + let content = state.get_content(&content.value).expect("no content provided"); + let alternate = state.get_content(&alternate.value).expect("no alternate provided"); + return Some(Self(condition, content, alternate)) + } +}); +impl> Content for When { + fn layout (&self, to: E::Area) -> E::Area { + let Self(cond, item) = self; + let mut area = E::Area::zero(); + if *cond { + let item_area = item.layout(to); + area[0] = item_area.x(); + area[1] = item_area.y(); + area[2] = item_area.w(); + area[3] = item_area.h(); + } + area.into() + } + fn render (&self, to: &mut E) { + let Self(cond, item) = self; + if *cond { item.render(to) } + } +} +impl, B: Render> Content for Either { + fn layout (&self, to: E::Area) -> E::Area { + let Self(cond, a, b) = self; + if *cond { a.layout(to) } else { b.layout(to) } + } + fn render (&self, to: &mut E) { + let Self(cond, a, b) = self; + if *cond { a.render(to) } else { b.render(to) } + } +} diff --git a/output/src/op_iter.rs b/output/src/op_iter.rs new file mode 100644 index 0000000..f793a62 --- /dev/null +++ b/output/src/op_iter.rs @@ -0,0 +1,77 @@ +use crate::*; +#[inline] pub fn map_south( + item_offset: O::Unit, + item_height: O::Unit, + item: impl Content +) -> impl Content { + Push::y(item_offset, Fixed::y(item_height, Fill::x(item))) +} + +#[inline] pub fn map_south_west( + item_offset: O::Unit, + item_height: O::Unit, + item: impl Content +) -> impl Content { + Push::y(item_offset, Align::nw(Fixed::y(item_height, Fill::x(item)))) +} + +#[inline] pub fn map_east( + item_offset: O::Unit, + item_width: O::Unit, + item: impl Content +) -> impl Content { + Push::x(item_offset, Align::w(Fixed::x(item_width, Fill::y(item)))) +} + +pub struct Map<'a, A, B, I, F, G>(pub PhantomData<&'a()>, pub F, pub G) where + I: Iterator + Send + Sync, + F: Fn() -> I + Send + Sync + 'a, + G: Fn(A, usize)->B + Send + Sync; + +impl<'a, A, B, I, F, G> Map<'a, A, B, I, F, G> where + I: Iterator + Send + Sync, + F: Fn() -> I + Send + Sync + 'a, + G: Fn(A, usize)->B + Send + Sync +{ + pub fn new (f: F, g: G) -> Self { + Self(Default::default(), f, g) + } +} + +impl<'a, E, A, B, I, F, G> Content for Map<'a, A, B, I, F, G> where + E: Output, + B: Render, + I: Iterator + Send + Sync, + F: Fn() -> I + Send + Sync + 'a, + G: Fn(A, usize)->B + Send + Sync +{ + fn layout (&self, area: E::Area) -> E::Area { + let Self(_, get_iterator, callback) = self; + let mut index = 0; + let [mut min_x, mut min_y] = area.center(); + let [mut max_x, mut max_y] = area.center(); + for item in get_iterator() { + let [x,y,w,h] = callback(item, index).layout(area).xywh(); + min_x = min_x.min(x.into()); + min_y = min_y.min(y.into()); + max_x = max_x.max((x + w).into()); + max_y = max_y.max((y + h).into()); + index += 1; + } + let w = max_x - min_x; + let h = max_y - min_y; + //[min_x.into(), min_y.into(), w.into(), h.into()].into() + area.center_xy([w.into(), h.into()].into()).into() + } + fn render (&self, to: &mut E) { + let Self(_, get_iterator, callback) = self; + let mut index = 0; + let area = Content::layout(self, to.area()); + for item in get_iterator() { + let item = callback(item, index); + //to.place(area.into(), &item); + to.place(item.layout(area), &item); + index += 1; + } + } +} diff --git a/output/src/op_transform.rs b/output/src/op_transform.rs new file mode 100644 index 0000000..5900d8c --- /dev/null +++ b/output/src/op_transform.rs @@ -0,0 +1,232 @@ +//! [Content] items that modify the inherent +//! dimensions of their inner [Render]ables. +//! +//! Transform may also react to the [Area] provided. +//! ``` +//! use ::tek_tui::{*, tek_output::*}; +//! let area: [u16;4] = [10, 10, 20, 20]; +//! fn test (area: [u16;4], item: &impl Content, expected: [u16;4]) { +//! assert_eq!(Content::layout(item, area), expected); +//! assert_eq!(Render::layout(item, area), expected); +//! }; +//! test(area, &(), [20, 20, 0, 0]); +//! +//! test(area, &Fill::xy(()), area); +//! test(area, &Fill::x(()), [10, 20, 20, 0]); +//! test(area, &Fill::y(()), [20, 10, 0, 20]); +//! +//! //FIXME:test(area, &Fixed::x(4, ()), [18, 20, 4, 0]); +//! //FIXME:test(area, &Fixed::y(4, ()), [20, 18, 0, 4]); +//! //FIXME:test(area, &Fixed::xy(4, 4, unit), [18, 18, 4, 4]); +//! ``` +use crate::*; +/// Defines an enum that transforms its content +/// along either the X axis, the Y axis, or both. +macro_rules! transform_xy { + ($x:literal $y:literal $xy:literal |$self:ident : $Enum:ident, $to:ident|$area:expr) => { + pub enum $Enum { X(T), Y(T), XY(T) } + impl $Enum { + #[inline] pub fn x (item: T) -> Self { Self::X(item) } + #[inline] pub fn y (item: T) -> Self { Self::Y(item) } + #[inline] pub fn xy (item: T) -> Self { Self::XY(item) } + } + impl<'a, E: Output + 'a, T: ViewContext<'a, E>> TryFromAtom<'a, T> + for $Enum> { + fn try_from_expr (state: &'a T, iter: TokenIter<'a>) -> Option { + let mut iter = iter.clone(); + if let Some(Token { value: Value::Key(k), .. }) = iter.peek() { + if k == $x || k == $y || k == $xy { + let _ = iter.next().unwrap(); + let token = iter.next().expect("no content specified"); + let content = state.get_content(&token.value).expect("no content provided"); + return Some(match k { + $x => Self::x(content), + $y => Self::y(content), + $xy => Self::xy(content), + _ => unreachable!() + }) + } + } + None + } + } + impl> Content for $Enum { + fn content (&self) -> impl Render { + match self { + Self::X(item) => item, + Self::Y(item) => item, + Self::XY(item) => item, + } + } + fn layout (&$self, $to: ::Area) -> ::Area { + use $Enum::*; + $area + } + } + } +} + +/// Defines an enum that parametrically transforms its content +/// along either the X axis, the Y axis, or both. +macro_rules! transform_xy_unit { + ($x:literal $y:literal $xy:literal |$self:ident : $Enum:ident, $to:ident|$layout:expr) => { + pub enum $Enum { X(U, T), Y(U, T), XY(U, U, T), } + impl $Enum { + #[inline] pub fn x (x: U, item: T) -> Self { Self::X(x, item) } + #[inline] pub fn y (y: U, item: T) -> Self { Self::Y(y, item) } + #[inline] pub fn xy (x: U, y: U, item: T) -> Self { Self::XY(x, y, item) } + } + impl<'a, E: Output + 'a, T: ViewContext<'a, E>> TryFromAtom<'a, T> + for $Enum> { + fn try_from_expr (state: &'a T, iter: TokenIter<'a>) -> Option { + let mut iter = iter.clone(); + if let Some(Token { value: Value::Key(k), .. }) = iter.peek() { + if k == $x || k == $y { + let _ = iter.next().unwrap(); + let u = iter.next().expect("no unit specified"); + let c = iter.next().expect("no content specified"); + let u = state.get(&u.value).expect("no unit provided"); + let c = state.get_content(&c.value).expect("no content provided"); + return Some(match k { + $x => Self::x(u, c), + $y => Self::y(u, c), + _ => unreachable!(), + }) + } else if k == $xy { + let _ = iter.next().unwrap(); + let u = iter.next().expect("no unit specified"); + let v = iter.next().expect("no unit specified"); + let c = iter.next().expect("no content specified"); + let u = state.get(&u.value).expect("no unit provided"); + let v = state.get(&v.value).expect("no unit provided"); + let c = state.get_content(&c.value).expect("no content provided"); + return Some(Self::xy(u, v, c)) + } + } + None + } + } + impl> Content for $Enum { + fn content (&self) -> impl Render { + Some(match self { + Self::X(_, content) => content, + Self::Y(_, content) => content, + Self::XY(_, _, content) => content, + }) + } + fn layout (&$self, $to: E::Area) -> E::Area { + $layout.into() + } + } + impl $Enum { + #[inline] pub fn dx (&self) -> U { + match self { + Self::X(x, _) => *x, Self::Y(_, _) => 0.into(), Self::XY(x, _, _) => *x, + } + } + #[inline] pub fn dy (&self) -> U { + match self { + Self::X(_, _) => 0.into(), Self::Y(y, _) => *y, Self::XY(_, y, _) => *y, + } + } + } + } +} +transform_xy!("fill/x" "fill/y" "fill/xy" |self: Fill, to|{ + let [x0, y0, wmax, hmax] = to.xywh(); + let [x, y, w, h] = self.content().layout(to).xywh(); + match self { + X(_) => [x0, y, wmax, h], + Y(_) => [x, y0, w, hmax], + XY(_) => [x0, y0, wmax, hmax], + }.into() }); +transform_xy_unit!("fixed/x" "fixed/y" "fixed/xy"|self: Fixed, area|{ + let [x, y, w, h] = area.xywh(); + let fixed_area = match self { + Self::X(fw, _) => [x, y, *fw, h], + Self::Y(fh, _) => [x, y, w, *fh], + Self::XY(fw, fh, _) => [x, y, *fw, *fh], + }; + let [x, y, w, h] = Render::layout(&self.content(), fixed_area.into()).xywh(); + let fixed_area = match self { + Self::X(fw, _) => [x, y, *fw, h], + Self::Y(fh, _) => [x, y, w, *fh], + Self::XY(fw, fh, _) => [x, y, *fw, *fh], + }; + fixed_area }); +transform_xy_unit!("min/x" "min/y" "min/xy"|self: Min, area|{ + let area = Render::layout(&self.content(), area); + match self { + Self::X(mw, _) => [area.x(), area.y(), area.w().max(*mw), area.h()], + Self::Y(mh, _) => [area.x(), area.y(), area.w(), area.h().max(*mh)], + Self::XY(mw, mh, _) => [area.x(), area.y(), area.w().max(*mw), area.h().max(*mh)], + }}); +transform_xy_unit!("max/x" "max/y" "max/xy"|self: Max, area|{ + let [x, y, w, h] = area.xywh(); + Render::layout(&self.content(), match self { + Self::X(fw, _) => [x, y, *fw, h], + Self::Y(fh, _) => [x, y, w, *fh], + Self::XY(fw, fh, _) => [x, y, *fw, *fh], + }.into())}); +transform_xy_unit!("shrink/x" "shrink/y" "shrink/xy"|self: Shrink, area|Render::layout( + &self.content(), + [area.x(), area.y(), area.w().minus(self.dx()), area.h().minus(self.dy())].into())); +transform_xy_unit!("expand/x" "expand/y" "expand/xy"|self: Expand, area|Render::layout( + &self.content(), + [area.x(), area.y(), area.w().plus(self.dx()), area.h().plus(self.dy())].into())); +transform_xy_unit!("push/x" "push/y" "push/xy"|self: Push, area|{ + let area = Render::layout(&self.content(), area); + [area.x().plus(self.dx()), area.y().plus(self.dy()), area.w(), area.h()] }); +transform_xy_unit!("pull/x" "pull/y" "pull/xy"|self: Pull, area|{ + let area = Render::layout(&self.content(), area); + [area.x().minus(self.dx()), area.y().minus(self.dy()), area.w(), area.h()] }); +transform_xy_unit!("margin/x" "margin/y" "margin/xy"|self: Margin, area|{ + let area = Render::layout(&self.content(), area); + let dx = self.dx(); + let dy = self.dy(); + [area.x().minus(dx), area.y().minus(dy), area.w().plus(dy.plus(dy)), area.h().plus(dy.plus(dy))] }); +transform_xy_unit!("padding/x" "padding/y" "padding/xy"|self: Padding, area|{ + let area = Render::layout(&self.content(), area); + let dx = self.dx(); + let dy = self.dy(); + [area.x().plus(dx), area.y().plus(dy), area.w().minus(dy.plus(dy)), area.h().minus(dy.plus(dy)), ] }); + +#[cfg(test)] mod test_op_transform { + use super::*; + use proptest::prelude::*; + use proptest::option::of; + macro_rules! test_op_transform { + ($fn:ident, $Op:ident) => { + proptest! { + #[test] fn $fn ( + op_x in of(u16::MIN..u16::MAX), + op_y in of(u16::MIN..u16::MAX), + content in "\\PC*", + x in u16::MIN..u16::MAX, + y in u16::MIN..u16::MAX, + w in u16::MIN..u16::MAX, + h in u16::MIN..u16::MAX, + ) { + if let Some(op) = match (op_x, op_y) { + (Some(x), Some(y)) => Some($Op::xy(x, y, content)), + (Some(x), None) => Some($Op::x(x, content)), + (Some(y), None) => Some($Op::y(y, content)), + _ => None + } { + assert_eq!(Content::layout(&op, [x, y, w, h]), + Render::layout(&op, [x, y, w, h])); + } + } + } + } + } + test_op_transform!(test_op_fixed, Fixed); + test_op_transform!(test_op_min, Min); + test_op_transform!(test_op_max, Max); + test_op_transform!(test_op_push, Push); + test_op_transform!(test_op_pull, Pull); + test_op_transform!(test_op_shrink, Shrink); + test_op_transform!(test_op_expand, Expand); + test_op_transform!(test_op_margin, Margin); + test_op_transform!(test_op_padding, Padding); +} diff --git a/output/src/output.rs b/output/src/output.rs new file mode 100644 index 0000000..e993606 --- /dev/null +++ b/output/src/output.rs @@ -0,0 +1,132 @@ +use crate::*; +use std::ops::Deref; +/// Render target. +pub trait Output: Send + Sync + Sized { + /// Unit of length + type Unit: Coordinate; + /// Rectangle without offset + type Size: Size; + /// Rectangle with offset + type Area: Area; + /// Current output area + fn area (&self) -> Self::Area; + /// Mutable pointer to area + fn area_mut (&mut self) -> &mut Self::Area; + /// Render widget in area + fn place (&mut self, area: Self::Area, content: &impl Render); + #[inline] fn x (&self) -> Self::Unit { self.area().x() } + #[inline] fn y (&self) -> Self::Unit { self.area().y() } + #[inline] fn w (&self) -> Self::Unit { self.area().w() } + #[inline] fn h (&self) -> Self::Unit { self.area().h() } + #[inline] fn wh (&self) -> Self::Size { self.area().wh().into() } +} +/// Renderable with dynamic dispatch. +pub trait Render { + /// Compute layout. + fn layout (&self, area: E::Area) -> E::Area; + /// Write data to display. + fn render (&self, output: &mut E); + /// Perform type erasure, turning `self` into an opaque [RenderBox]. + fn boxed <'a> (self) -> RenderBox<'a, E> where Self: Send + Sync + Sized + 'a { + Box::new(self) as RenderBox<'a, E> + } +} +/// Most importantly, every [Content] is also a [Render]. +/// +/// However, the converse does not hold true. +/// Instead, the [Content::content] method returns an +/// opaque [Render] pointer. +impl> Render for C { + fn layout (&self, area: E::Area) -> E::Area { Content::layout(self, area) } + fn render (&self, output: &mut E) { Content::render(self, output) } +} +/// Opaque pointer to a renderable living on the heap. +/// +/// Return this from [Content::content] to use dynamic dispatch. +pub type RenderBox<'a, E> = Box>; +/// You can render from a box. +impl<'a, E: Output> Content for RenderBox<'a, E> { + fn content (&self) -> impl Render { self.deref() } + //fn boxed <'b> (self) -> RenderBox<'b, E> where Self: Sized + 'b { self } +} +/// Opaque pointer to a renderable. +pub type RenderDyn<'a, E> = dyn Render + Send + Sync + 'a; +/// You can render from an opaque pointer. +impl<'a, E: Output> Content for &RenderDyn<'a, E> where Self: Sized { + fn content (&self) -> impl Render { self.deref() } + fn layout (&self, area: E::Area) -> E::Area { Render::layout(self.deref(), area) } + fn render (&self, output: &mut E) { Render::render(self.deref(), output) } +} +/// Composable renderable with static dispatch. +pub trait Content { + /// Return a [Render]able of a specific type. + fn content (&self) -> impl Render { () } + /// Perform layout. By default, delegates to [Self::content]. + fn layout (&self, area: E::Area) -> E::Area { self.content().layout(area) } + /// Draw to output. By default, delegates to [Self::content]. + fn render (&self, output: &mut E) { self.content().render(output) } +} +/// Every pointer to [Content] is a [Content]. +impl> Content for &C { + fn content (&self) -> impl Render { (*self).content() } + fn layout (&self, area: E::Area) -> E::Area { (*self).layout(area) } + fn render (&self, output: &mut E) { (*self).render(output) } +} +/// The platonic ideal unit of [Content]: total emptiness at dead center (e=1vg^sqrt(-1)) +impl Content for () { + fn layout (&self, area: E::Area) -> E::Area { area.center().to_area_pos().into() } + fn render (&self, _: &mut E) {} +} +impl> Content for Option { + fn content (&self) -> impl Render { + self.as_ref() + } + fn layout (&self, area: E::Area) -> E::Area { + self.as_ref() + .map(|content|content.layout(area)) + .unwrap_or([0.into(), 0.into(), 0.into(), 0.into(),].into()) + } + fn render (&self, output: &mut E) { + self.as_ref() + .map(|content|content.render(output)); + } +} +/// Implement [Content] with composable content for a struct. +#[macro_export] macro_rules! content { + // Implement for all [Output]s. + (|$self:ident:$Struct:ty| $content:expr) => { + impl Content for $Struct { + fn content (&$self) -> impl Render { Some($content) } + } + }; + // Implement for specific [Output]. + ($Output:ty:| + $self:ident: + $Struct:ident$(<$($($L:lifetime)? $($T:ident)? $(:$Trait:path)?),+>)? + |$content:expr) => { + impl $(<$($($L)? $($T)? $(:$Trait)?),+>)? Content<$Output> + for $Struct $(<$($($L)? $($T)?),+>)? { + fn content (&$self) -> impl Render<$Output> { $content } + } + }; +} +/// Implement [Content] with custom rendering for a struct. +#[macro_export] macro_rules! render { + (|$self:ident:$Struct:ident $(< + $($L:lifetime),* $($T:ident $(:$Trait:path)?),* + >)?, $to:ident | $render:expr) => { + impl <$($($L),*)? E: Output, $($T$(:$Trait)?),*> Content + for $Struct $(<$($L),* $($T),*>>)? { + fn render (&$self, $to: &mut E) { $render } + } + }; + ($Output:ty:| + $self:ident: + $Struct:ident $(<$($($L:lifetime)? $($T:ident)? $(:$Trait:path)?),+>)?, $to:ident + |$render:expr) => { + impl $(<$($($L)? $($T)? $(:$Trait)?),+>)? Content<$Output> + for $Struct $(<$($($L)? $($T)?),+>)? { + fn render (&$self, $to: &mut $Output) { $render } + } + }; +} diff --git a/output/src/reduce.rs b/output/src/reduce.rs new file mode 100644 index 0000000..55021ef --- /dev/null +++ b/output/src/reduce.rs @@ -0,0 +1,93 @@ +use crate::*; + +pub struct Reduce(pub PhantomData, pub F, pub G) where + A: Send + Sync, B: Send + Sync, + I: Iterator + Send + Sync, + F: Fn() -> I + Send + Sync, + G: Fn(A, B, usize)->A + Send + Sync; + +impl Reduce where + A: Send + Sync, B: Send + Sync, + I: Iterator + Send + Sync, + F: Fn() -> I + Send + Sync, + G: Fn(A, B, usize)->A + Send + Sync +{ + pub fn new (f: F, g: G) -> Self { Self(Default::default(), f, g) } +} + +impl Content for Reduce where + A: Send + Sync, B: Send + Sync, + I: Iterator + Send + Sync, + F: Fn() -> I + Send + Sync, + G: Fn(A, B, usize)->A + Send + Sync +{ + fn content (&self) -> impl Render { + } +} + +/* + + //pub fn reduce (iterator: I, callback: F) -> Reduce where + //E: Output, + //I: Iterator + Send + Sync, + //R: Render, + //F: Fn(R, T, usize) -> R + Send + Sync + //{ + //Reduce(Default::default(), iterator, callback) + //} +pub struct Reduce(PhantomData<(E, R)>, I, F) where + E: Output, + I: Iterator + Send + Sync, + R: Render, + F: Fn(R, T, usize) -> R + Send + Sync; +impl Content for Reduce where + E: Output, + I: Iterator + Send + Sync, + R: Render, + F: Fn(R, T, usize) -> R + Send + Sync +{ + fn render (&self, to: &mut E) { + todo!() + } +} +*/ + +//macro_rules! define_ops { + //($Trait:ident<$E:ident:$Output:path> { $( + //$(#[$attr:meta $($attr_args:tt)*])* + //( + //$fn:ident + //$(<$($G:ident$(:$Gen:path)?, )+>)? + //$Op:ident + //($($arg:ident:$Arg:ty),*) + //) + //)* }) => { + //impl<$E: $Output> $Trait for E {} + //pub trait $Trait<$E: $Output> { + //$( + //$(#[$attr $($attr_args)*])* + //fn $fn $(<$($G),+>)? + //($($arg:$Arg),*)-> $Op<$($(, $G)+)?> + //$(where $($G: $($Gen + Send + Sync)?),+)? + //{ $Op($($arg),*) } + //)* + //} + //} +//} + +//define_ops! { + //Layout { + //(when ,> + //When(cond: bool, item: A)) + ///// When `cond` is `true`, render `a`, otherwise render `b`. + //(either , B: Render,> + //Either(cond: bool, a: A, b: B)) + ///// If `opt` is `Some(T)` renders `cb(t)`, otherwise nothing. + //(opt B, B: Render,> + //Opt(option: Option, cb: F)) + ///// Maps items of iterator through callback. + //(map , I: Iterator, F: Fn() -> I, G: Fn(A, usize)->B,> + //Map(get_iterator: F, callback: G)) + //} +//} + diff --git a/output/src/size.rs b/output/src/size.rs new file mode 100644 index 0000000..c0518d8 --- /dev/null +++ b/output/src/size.rs @@ -0,0 +1,63 @@ +use crate::*; +use std::fmt::Debug; + +pub trait Size: From<[N;2]> + Debug + Copy { + fn x (&self) -> N; + fn y (&self) -> N; + #[inline] fn w (&self) -> N { self.x() } + #[inline] fn h (&self) -> N { self.y() } + #[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] } + #[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w), self.h()] } + #[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h)] } + #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { + Err(format!("min {w}x{h}").into()) + } else { + Ok(self) + } + } + #[inline] fn zero () -> [N;2] { + [N::zero(), N::zero()] + } + #[inline] fn to_area_pos (&self) -> [N;4] { + let [x, y] = self.wh(); + [x, y, 0.into(), 0.into()] + } + #[inline] fn to_area_size (&self) -> [N;4] { + let [w, h] = self.wh(); + [0.into(), 0.into(), w, h] + } +} + +impl Size for (N, N) { + #[inline] fn x (&self) -> N { self.0 } + #[inline] fn y (&self) -> N { self.1 } +} + +impl Size for [N;2] { + #[inline] fn x (&self) -> N { self[0] } + #[inline] fn y (&self) -> N { self[1] } +} + +#[cfg(test)] mod test_size { + use super::*; + use proptest::prelude::*; + proptest! { + #[test] fn test_size ( + x in u16::MIN..u16::MAX, + y in u16::MIN..u16::MAX, + a in u16::MIN..u16::MAX, + b in u16::MIN..u16::MAX, + ) { + let size = [x, y]; + let _ = size.w(); + let _ = size.h(); + let _ = size.wh(); + let _ = size.clip_w(a); + let _ = size.clip_h(b); + let _ = size.expect_min(a, b); + let _ = size.to_area_pos(); + let _ = size.to_area_size(); + } + } +} diff --git a/output/src/thunk.rs b/output/src/thunk.rs new file mode 100644 index 0000000..8bcbcaa --- /dev/null +++ b/output/src/thunk.rs @@ -0,0 +1,50 @@ +use crate::*; +use std::marker::PhantomData; + +/// Lazily-evaluated [Render]able. +pub struct Thunk, F: Fn()->T + Send + Sync>(PhantomData, F); +impl, F: Fn()->T + Send + Sync> Thunk { + pub fn new (thunk: F) -> Self { + Self(Default::default(), thunk) + } +} +impl, F: Fn()->T + Send + Sync> Content for Thunk { + fn content (&self) -> impl Render { (self.1)() } +} + +pub struct ThunkBox<'a, E: Output>(PhantomData, BoxRenderBox<'a, E> + Send + Sync + 'a>); +impl<'a, E: Output> ThunkBox<'a, E> { + pub fn new (thunk: BoxRenderBox<'a, E> + Send + Sync + 'a>) -> Self { + Self(Default::default(), thunk) + } +} +impl<'a, E: Output> Content for ThunkBox<'a, E> { + fn content (&self) -> impl Render { (self.1)() } +} +impl<'a, E: Output, F: Fn()->T + Send + Sync + 'a, T: Render + Send + Sync + 'a> From for ThunkBox<'a, E> { + fn from (f: F) -> Self { + Self(Default::default(), Box::new(move||f().boxed())) + } +} +//impl<'a, E: Output, F: Fn()->Box + 'a> + Send + Sync + 'a> From for ThunkBox<'a, E> { + //fn from (f: F) -> Self { + //Self(Default::default(), Box::new(f)) + //} +//} + +pub struct ThunkRender(PhantomData, F); +impl ThunkRender { + pub fn new (render: F) -> Self { Self(Default::default(), render) } +} +impl Content for ThunkRender { + fn render (&self, to: &mut E) { (self.1)(to) } +} + +pub struct ThunkLayoutE::Area + Send + Sync, F2: Fn(&mut E) + Send + Sync>(PhantomData, F1, F2); +implE::Area + Send + Sync, F2: Fn(&mut E) + Send + Sync> ThunkLayout { + pub fn new (layout: F1, render: F2) -> Self { Self(Default::default(), layout, render) } +} +implE::Area + Send + Sync, F2: Fn(&mut E) + Send + Sync> Content for ThunkLayout { + fn layout (&self, to: E::Area) -> E::Area { (self.1)(to) } + fn render (&self, to: &mut E) { (self.2)(to) } +} diff --git a/output/src/view.rs b/output/src/view.rs new file mode 100644 index 0000000..c199258 --- /dev/null +++ b/output/src/view.rs @@ -0,0 +1,87 @@ +use crate::*; +#[macro_export] macro_rules! view { + ($Output:ty: |$self:ident: $State:ty| $expr:expr; { + $($sym:literal => $body:expr),* $(,)? + }) => { + impl Content<$Output> for $State { + fn content (&$self) -> impl Render<$Output> { $expr } + } + impl<'a> ViewContext<'a, $Output> for $State { + fn get_content_sym (&'a $self, value: &Value<'a>) -> Option> { + if let Value::Sym(s) = value { + match *s { + $($sym => Some($body),)* + _ => None + } + } else { + panic!("expected content, got: {value:?}") + } + } + } + } +} + +// An ephemeral wrapper around view state and view description, +// that is meant to be constructed and returned from [Content::content]. +pub struct View<'a, T>(pub &'a T, pub SourceIter<'a>); +impl<'a, O: Output + 'a, T: ViewContext<'a, O>> Content for View<'a, T> { + fn content (&self) -> impl Render { + let iter = self.1.clone(); + while let Some((Token { value, .. }, _)) = iter.next() { + if let Some(content) = self.0.get_content(&value) { + return Some(content) + } + } + return None + } +} +// Provides components to the view. +pub trait ViewContext<'a, E: Output + 'a>: Send + Sync + + Context + + Context + + Context +{ + fn get_content (&'a self, value: &Value<'a>) -> Option> { + match value { + Value::Sym(_) => self.get_content_sym(value), + Value::Exp(_, _) => self.get_content_exp(value), + _ => panic!("only :symbols and (expressions) accepted here") + } + } + fn get_content_sym (&'a self, value: &Value<'a>) -> Option>; + fn get_content_exp (&'a self, value: &Value<'a>) -> Option> { + try_delegate!(self, *value, When::>); + try_delegate!(self, *value, Either::, RenderBox<'a, E>>); + try_delegate!(self, *value, Align::>); + try_delegate!(self, *value, Bsp::, RenderBox<'a, E>>); + try_delegate!(self, *value, Fill::>); + try_delegate!(self, *value, Fixed::<_, RenderBox<'a, E>>); + try_delegate!(self, *value, Min::<_, RenderBox<'a, E>>); + try_delegate!(self, *value, Max::<_, RenderBox<'a, E>>); + try_delegate!(self, *value, Shrink::<_, RenderBox<'a, E>>); + try_delegate!(self, *value, Expand::<_, RenderBox<'a, E>>); + try_delegate!(self, *value, Push::<_, RenderBox<'a, E>>); + try_delegate!(self, *value, Pull::<_, RenderBox<'a, E>>); + try_delegate!(self, *value, Margin::<_, RenderBox<'a, E>>); + try_delegate!(self, *value, Padding::<_, RenderBox<'a, E>>); + None + } +} +#[macro_export] macro_rules! try_delegate { + ($s:ident, $atom:expr, $T:ty) => { + if let Some(value) = <$T>::try_from_atom($s, $atom) { + return Some(value.boxed()) + } + } +} +#[macro_export] macro_rules! try_from_expr { + (<$l:lifetime, $E:ident>: $Struct:ty: |$state:ident, $iter:ident|$body:expr) => { + impl<$l, $E: Output + $l, T: ViewContext<$l, $E>> TryFromAtom<$l, T> for $Struct { + fn try_from_expr ($state: &$l T, $iter: TokenIter<'a>) -> Option { + let mut $iter = $iter.clone(); + $body; + None + } + } + } +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..4087b75 --- /dev/null +++ b/shell.nix @@ -0,0 +1,13 @@ +#!/usr/bin/env nix-shell +{pkgs?import{}}:let + stdenv = pkgs.clang19Stdenv; + name = "tengri"; + nativeBuildInputs = with pkgs; [ pkg-config libclang ]; + buildInputs = with pkgs; [ libclang ]; + LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (with pkgs; []); +in pkgs.mkShell.override { + inherit stdenv; +} { + inherit name nativeBuildInputs buildInputs VST3_SDK_DIR LIBCLANG_PATH LD_LIBRARY_PATH; +} diff --git a/tui/Cargo.toml b/tui/Cargo.toml new file mode 100644 index 0000000..14e754b --- /dev/null +++ b/tui/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "tek_tui" +edition = "2021" +version = "0.2.0" + +[dependencies] +palette = { version = "0.7.6", features = [ "random" ] } +rand = "0.8.5" +crossterm = "0.28.1" +ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] } +better-panic = "0.3.0" +konst = { version = "0.3.16", features = [ "rust_1_83" ] } +atomic_float = "1" +quanta = "0.12.3" + +tek_edn = { path = "../edn" } +tek_input = { path = "../input" } +tek_output = { path = "../output" } +#tek_time = { path = "../time" } diff --git a/tui/README.md b/tui/README.md new file mode 100644 index 0000000..36606d5 --- /dev/null +++ b/tui/README.md @@ -0,0 +1,15 @@ +# `tek_tui` + +the `Tui` struct (the *engine*) implements the +`tek_input::Input` and `tek_output::Output` traits. + +at launch, the `Tui` engine spawns two threads, +a **render thread** and an **input thread**. (the +application may spawn further threads, such as a +**jack thread**.) + +all threads communicate using shared ownership, +`Arc` and `Arc`. the engine and +application instances are expected to be wrapped +in `Arc`; internally, those synchronization +mechanisms may be used liberally. diff --git a/tui/examples/demo.rs.old b/tui/examples/demo.rs.old new file mode 100644 index 0000000..a68d26d --- /dev/null +++ b/tui/examples/demo.rs.old @@ -0,0 +1,144 @@ +use tek::*; + +fn main () -> Usually<()> { + Tui::run(Arc::new(RwLock::new(Demo::new())))?; + Ok(()) +} + +pub struct Demo { + index: usize, + items: Vec>> +} + +impl Demo { + fn new () -> Self { + Self { + index: 0, + items: vec![ + //Box::new(tek_sequencer::TransportPlayPauseButton { + //_engine: Default::default(), + //transport: None, + //value: Some(TransportState::Stopped), + //focused: true + //}), + //Box::new(tek_sequencer::TransportPlayPauseButton { + //_engine: Default::default(), + //transport: None, + //value: Some(TransportState::Rolling), + //focused: false + //}), + ] + } + } +} + +impl Content for Demo { + type Engine = Tui; + fn content (&self) -> dyn Render { + let border_style = Style::default().fg(Color::Rgb(0,0,0)); + Align::Center(Layers::new(move|add|{ + + add(&Background(Color::Rgb(0,128,128)))?; + + add(&Margin::XY(1, 1, Stack::down(|add|{ + + add(&Layers::new(|add|{ + add(&Background(Color::Rgb(128,96,0)))?; + add(&Border(Square(border_style)))?; + add(&Margin::XY(2, 1, "..."))?; + Ok(()) + }).debug())?; + + add(&Layers::new(|add|{ + add(&Background(Color::Rgb(128,64,0)))?; + add(&Border(Lozenge(border_style)))?; + add(&Margin::XY(4, 2, "---"))?; + Ok(()) + }).debug())?; + + add(&Layers::new(|add|{ + add(&Background(Color::Rgb(96,64,0)))?; + add(&Border(SquareBold(border_style)))?; + add(&Margin::XY(6, 3, "~~~"))?; + Ok(()) + }).debug())?; + + Ok(()) + })).debug())?; + + Ok(()) + + })) + //Align::Center(Margin::X(1, Layers::new(|add|{ + //add(&Background(Color::Rgb(128,0,0)))?; + //add(&Stack::down(|add|{ + //add(&Margin::Y(1, Layers::new(|add|{ + //add(&Background(Color::Rgb(0,128,0)))?; + //add(&Align::Center("12345"))?; + //add(&Align::Center("FOO")) + //})))?; + //add(&Margin::XY(1, 1, Layers::new(|add|{ + //add(&Align::Center("1234567"))?; + //add(&Align::Center("BAR"))?; + //add(&Background(Color::Rgb(0,0,128))) + //}))) + //})) + //}))) + + //Align::Y(Layers::new(|add|{ + //add(&Background(Color::Rgb(128,0,0)))?; + //add(&Margin::X(1, Align::Center(Stack::down(|add|{ + //add(&Align::X(Margin::Y(1, Layers::new(|add|{ + //add(&Background(Color::Rgb(0,128,0)))?; + //add(&Align::Center("12345"))?; + //add(&Align::Center("FOO")) + //})))?; + //add(&Margin::XY(1, 1, Layers::new(|add|{ + //add(&Align::Center("1234567"))?; + //add(&Align::Center("BAR"))?; + //add(&Background(Color::Rgb(0,0,128))) + //})))?; + //Ok(()) + //}))))) + //})) + } +} + +impl Handle for Demo { + fn handle (&mut self, from: &TuiIn) -> Perhaps { + use KeyCode::{PageUp, PageDown}; + match from.event() { + kexp!(PageUp) => { + self.index = (self.index + 1) % self.items.len(); + }, + kexp!(PageDown) => { + self.index = if self.index > 1 { + self.index - 1 + } else { + self.items.len() - 1 + }; + }, + _ => return Ok(None) + } + Ok(Some(true)) + } +} + +//lisp!(CONTENT Demo (LET + //(BORDER-STYLE (STYLE (FG (RGB 0 0 0)))) + //(BG-COLOR-0 (RGB 0 128 128)) + //(BG-COLOR-1 (RGB 128 96 0)) + //(BG-COLOR-2 (RGB 128 64 0)) + //(BG-COLOR-3 (RGB 96 64 0)) + //(CENTER (LAYERS + //(BACKGROUND BG-COLOR-0) + //(OUTSET-XY 1 1 (SPLIT-DOWN + //(LAYERS (BACKGROUND BG-COLOR-1) + //(BORDER SQUARE BORDER-STYLE) + //(OUTSET-XY 2 1 "...")) + //(LAYERS (BACKGROUND BG-COLOR-2) + //(BORDER LOZENGE BORDER-STYLE) + //(OUTSET-XY 4 2 "---")) + //(LAYERS (BACKGROUND BG-COLOR-3) + //(BORDER SQUARE-BOLD BORDER-STYLE) + //(OUTSET-XY 2 1 "~~~")))))))) diff --git a/tui/examples/edn01.edn b/tui/examples/edn01.edn new file mode 100644 index 0000000..7ff93d1 --- /dev/null +++ b/tui/examples/edn01.edn @@ -0,0 +1 @@ +:hello-world diff --git a/tui/examples/edn02.edn b/tui/examples/edn02.edn new file mode 100644 index 0000000..4f352a6 --- /dev/null +++ b/tui/examples/edn02.edn @@ -0,0 +1 @@ +(bsp/s :hello :world) diff --git a/tui/examples/edn03.edn b/tui/examples/edn03.edn new file mode 100644 index 0000000..1622275 --- /dev/null +++ b/tui/examples/edn03.edn @@ -0,0 +1 @@ +(fill/xy :hello-world) diff --git a/tui/examples/edn04.edn b/tui/examples/edn04.edn new file mode 100644 index 0000000..9393669 --- /dev/null +++ b/tui/examples/edn04.edn @@ -0,0 +1 @@ +(fixed/xy 20 10 :hello-world) diff --git a/tui/examples/edn05.edn b/tui/examples/edn05.edn new file mode 100644 index 0000000..90313ad --- /dev/null +++ b/tui/examples/edn05.edn @@ -0,0 +1 @@ +(bsp/s (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn06.edn b/tui/examples/edn06.edn new file mode 100644 index 0000000..e35abf0 --- /dev/null +++ b/tui/examples/edn06.edn @@ -0,0 +1 @@ +(bsp/e (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn07.edn b/tui/examples/edn07.edn new file mode 100644 index 0000000..7370353 --- /dev/null +++ b/tui/examples/edn07.edn @@ -0,0 +1 @@ +(bsp/n (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn08.edn b/tui/examples/edn08.edn new file mode 100644 index 0000000..afcde46 --- /dev/null +++ b/tui/examples/edn08.edn @@ -0,0 +1 @@ +(bsp/w (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn09.edn b/tui/examples/edn09.edn new file mode 100644 index 0000000..3ced769 --- /dev/null +++ b/tui/examples/edn09.edn @@ -0,0 +1 @@ +(bsp/a (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn10.edn b/tui/examples/edn10.edn new file mode 100644 index 0000000..07af31e --- /dev/null +++ b/tui/examples/edn10.edn @@ -0,0 +1 @@ +(bsp/b (fixed/xy 5 6 :hello) (fixed/xy 7 8 :world)) diff --git a/tui/examples/edn11.edn b/tui/examples/edn11.edn new file mode 100644 index 0000000..1c47cb5 --- /dev/null +++ b/tui/examples/edn11.edn @@ -0,0 +1,11 @@ +(bsp/s + (bsp/e (align/nw (fixed/xy 5 3 :hello)) + (bsp/e (align/n (fixed/xy 5 3 :hello)) + (align/ne (fixed/xy 5 3 :hello)))) + (bsp/s + (bsp/e (align/w (fixed/xy 5 3 :hello)) + (bsp/e (align/c (fixed/xy 5 3 :hello)) + (align/e (fixed/xy 5 3 :hello)))) + (bsp/e (align/sw (fixed/xy 5 3 :hello)) + (bsp/e (align/s (fixed/xy 5 3 :hello)) + (align/se (fixed/xy 5 3 :hello)))))) diff --git a/tui/examples/edn12.edn b/tui/examples/edn12.edn new file mode 100644 index 0000000..26e3f5f --- /dev/null +++ b/tui/examples/edn12.edn @@ -0,0 +1,11 @@ +(bsp/s + (bsp/e (fixed/xy 8 5 (align/nw :hello)) + (bsp/e (fixed/xy 8 5 (align/n :hello)) + (fixed/xy 8 5 (align/ne :hello)))) + (bsp/s + (bsp/e (fixed/xy 8 5 (align/w :hello)) + (bsp/e (fixed/xy 8 5 (align/c :hello)) + (fixed/xy 8 5 (align/e :hello)))) + (bsp/e (fixed/xy 8 5 (align/sw :hello)) + (bsp/e (fixed/xy 8 5 (align/s :hello)) + (fixed/xy 8 5 (align/se :hello)))))) diff --git a/tui/examples/edn13.edn b/tui/examples/edn13.edn new file mode 100644 index 0000000..8fe81c3 --- /dev/null +++ b/tui/examples/edn13.edn @@ -0,0 +1,11 @@ +(bsp/s + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/nw :hello))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/n :hello))) + (grow/xy 1 1 (fixed/xy 8 5 (align/ne :hello))))) + (bsp/s + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/w :hello))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/c :hello))) + (grow/xy 1 1 (fixed/xy 8 5 (align/e :hello))))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/sw :hello))) + (bsp/e (grow/xy 1 1 (fixed/xy 8 5 (align/s :hello))) + (grow/xy 1 1 (fixed/xy 8 5 (align/se :hello))))))) diff --git a/tui/examples/edn99.edn b/tui/examples/edn99.edn new file mode 100644 index 0000000..cac4726 --- /dev/null +++ b/tui/examples/edn99.edn @@ -0,0 +1,73 @@ +(align/c (bg/behind :bg0 (margin/xy 1 1 (col + (bg/behind :bg1 (border/around :border1 (margin/xy 2 1 :label1))) + (bg/behind :bg2 (border/around :border2 (margin/xy 4 2 :label2))) + (bg/behind :bg3 (border/around :border3 (margin/xy 6 3 :label3))))))) + + fn content (&self) -> dyn Render { + let border_style = Style::default().fg(Color::Rgb(0,0,0)); + Align::Center(Layers::new(move|add|{ + + add(&Background(Color::Rgb(0,128,128)))?; + + add(&Margin::XY(1, 1, Stack::down(|add|{ + + add(&Layers::new(|add|{ + add(&Background(Color::Rgb(128,96,0)))?; + add(&Border(Square(border_style)))?; + add(&Margin::XY(2, 1, "..."))?; + Ok(()) + }).debug())?; + + add(&Layers::new(|add|{ + add(&Background(Color::Rgb(128,64,0)))?; + add(&Border(Lozenge(border_style)))?; + add(&Margin::XY(4, 2, "---"))?; + Ok(()) + }).debug())?; + + add(&Layers::new(|add|{ + add(&Background(Color::Rgb(96,64,0)))?; + add(&Border(SquareBold(border_style)))?; + add(&Margin::XY(6, 3, "~~~"))?; + Ok(()) + }).debug())?; + + Ok(()) + })).debug())?; + + Ok(()) + + })) + //Align::Center(Margin::X(1, Layers::new(|add|{ + //add(&Background(Color::Rgb(128,0,0)))?; + //add(&Stack::down(|add|{ + //add(&Margin::Y(1, Layers::new(|add|{ + //add(&Background(Color::Rgb(0,128,0)))?; + //add(&Align::Center("12345"))?; + //add(&Align::Center("FOO")) + //})))?; + //add(&Margin::XY(1, 1, Layers::new(|add|{ + //add(&Align::Center("1234567"))?; + //add(&Align::Center("BAR"))?; + //add(&Background(Color::Rgb(0,0,128))) + //}))) + //})) + //}))) + + //Align::Y(Layers::new(|add|{ + //add(&Background(Color::Rgb(128,0,0)))?; + //add(&Margin::X(1, Align::Center(Stack::down(|add|{ + //add(&Align::X(Margin::Y(1, Layers::new(|add|{ + //add(&Background(Color::Rgb(0,128,0)))?; + //add(&Align::Center("12345"))?; + //add(&Align::Center("FOO")) + //})))?; + //add(&Margin::XY(1, 1, Layers::new(|add|{ + //add(&Align::Center("1234567"))?; + //add(&Align::Center("BAR"))?; + //add(&Background(Color::Rgb(0,0,128))) + //})))?; + //Ok(()) + //}))))) + //})) + } diff --git a/tui/examples/tui.rs b/tui/examples/tui.rs new file mode 100644 index 0000000..37fd116 --- /dev/null +++ b/tui/examples/tui.rs @@ -0,0 +1,66 @@ +use tek_tui::{*, tek_input::*, tek_output::*}; +use tek_edn::*; +use std::sync::{Arc, RwLock}; +use crossterm::event::{*, KeyCode::*}; +use crate::ratatui::style::Color; +fn main () -> Usually<()> { + let state = Arc::new(RwLock::new(Example(10, Measure::new()))); + Tui::new().unwrap().run(&state)?; + Ok(()) +} +#[derive(Debug)] pub struct Example(usize, Measure); +const KEYMAP: &str = "(:left prev) (:right next)"; +handle!(TuiIn: |self: Example, input|{ + let keymap = SourceIter::new(KEYMAP); + let command = keymap.command::<_, ExampleCommand, _>(self, input); + if let Some(command) = command { + command.execute(self)?; + return Ok(Some(true)) + } + return Ok(None) +}); +enum ExampleCommand { Next, Previous } +atom_command!(ExampleCommand: |app: Example| { + (":prev" [] Some(Self::Previous)) + (":next" [] Some(Self::Next)) +}); +command!(|self: ExampleCommand, state: Example|match self { + Self::Next => + { state.0 = (state.0 + 1) % EXAMPLES.len(); None }, + Self::Previous => + { state.0 = if state.0 > 0 { state.0 - 1 } else { EXAMPLES.len() - 1 }; None }, +}); +const EXAMPLES: &'static [&'static str] = &[ + include_str!("edn01.edn"), + include_str!("edn02.edn"), + include_str!("edn03.edn"), + include_str!("edn04.edn"), + include_str!("edn05.edn"), + include_str!("edn06.edn"), + include_str!("edn07.edn"), + include_str!("edn08.edn"), + include_str!("edn09.edn"), + include_str!("edn10.edn"), + include_str!("edn11.edn"), + include_str!("edn12.edn"), + include_str!("edn13.edn"), +]; +view!(TuiOut: |self: Example|{ + let index = self.0 + 1; + let wh = self.1.wh(); + let src = EXAMPLES[self.0]; + let heading = format!("Example {}/{} in {:?}", index, EXAMPLES.len(), &wh); + let title = Tui::bg(Color::Rgb(60, 10, 10), Push::y(1, Align::n(heading))); + let code = Tui::bg(Color::Rgb(10, 60, 10), Push::y(2, Align::n(format!("{}", src)))); + let content = Tui::bg(Color::Rgb(10, 10, 60), View(self, SourceIter::new(src))); + self.1.of(Bsp::s(title, Bsp::n(""/*code*/, content))) +}; { + ":title" => Tui::bg(Color::Rgb(60, 10, 10), Push::y(1, Align::n(format!("Example {}/{}:", self.0 + 1, EXAMPLES.len())))).boxed(), + ":code" => Tui::bg(Color::Rgb(10, 60, 10), Push::y(2, Align::n(format!("{}", EXAMPLES[self.0])))).boxed(), + ":hello" => Tui::bg(Color::Rgb(10, 100, 10), "Hello").boxed(), + ":world" => Tui::bg(Color::Rgb(100, 10, 10), "world").boxed(), + ":hello-world" => "Hello world!".boxed() +}); +provide_bool!(bool: |self: Example| {}); +provide_num!(u16: |self: Example| {}); +provide_num!(usize: |self: Example| {}); diff --git a/tui/src/lib.rs b/tui/src/lib.rs new file mode 100644 index 0000000..34922d0 --- /dev/null +++ b/tui/src/lib.rs @@ -0,0 +1,59 @@ +mod tui_buffer; pub use self::tui_buffer::*; +mod tui_color; pub use self::tui_color::*; +mod tui_content; pub use self::tui_content::*; +mod tui_engine; pub use self::tui_engine::*; +mod tui_file; pub use self::tui_file::*; +mod tui_input; pub use self::tui_input::*; +mod tui_output; pub use self::tui_output::*; +mod tui_perf; pub use self::tui_perf::*; +pub use ::tek_edn;// pub(crate) use ::tek_edn::*; +//pub use ::tek_time; pub(crate) use ::tek_time::*; +pub use ::tek_input; pub(crate) use tek_input::*; +pub use ::tek_output; pub(crate) use tek_output::*; +pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity}; +pub use ::palette; pub(crate) use ::palette::{*, convert::*, okhsl::*}; +pub use ::crossterm; pub(crate) use crossterm::{ + ExecutableCommand, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, + event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, +}; +pub use ::ratatui; pub(crate) use ratatui::{ + prelude::{Color, Style, Buffer}, + style::Modifier, + backend::{Backend, CrosstermBackend, ClearType}, + layout::{Size, Rect}, + buffer::Cell +}; +pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}}; +pub(crate) use std::io::{stdout, Stdout}; +pub(crate) use std::path::PathBuf; +pub(crate) use std::ffi::OsString; +pub(crate) use atomic_float::AtomicF64; +#[macro_export] macro_rules! from { + ($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => { + impl $(<$($lt),+>)? From<$Source> for $Target { + fn from ($state:$Source) -> Self { $cb } + } + }; +} +#[cfg(test)] #[test] fn test_tui_engine () -> Usually<()> { + use crate::*; + use std::sync::{Arc, RwLock}; + struct TestComponent(String); + impl Content for TestComponent { + fn content (&self) -> impl Render { + Some(self.0.as_str()) + } + } + impl Handle for TestComponent { + fn handle (&mut self, from: &TuiIn) -> Perhaps { + Ok(None) + } + } + let engine = Tui::new()?; + engine.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed); + let state = TestComponent("hello world".into()); + let state = std::sync::Arc::new(std::sync::RwLock::new(state)); + //engine.run(&state)?; + Ok(()) +} diff --git a/tui/src/tui_buffer.rs b/tui/src/tui_buffer.rs new file mode 100644 index 0000000..465cc32 --- /dev/null +++ b/tui/src/tui_buffer.rs @@ -0,0 +1,39 @@ +use crate::*; +pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) { + for row in 0..area.h() { + let y = area.y() + row; + for col in 0..area.w() { + let x = area.x() + col; + if x < buf.area.width && y < buf.area.height { + callback(buf.get_mut(x, y), col, row); + } + } + } +} +#[derive(Default)] pub struct BigBuffer { + pub width: usize, + pub height: usize, + pub content: Vec +} +impl std::fmt::Debug for BigBuffer { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len()) + } +} +impl BigBuffer { + pub fn new (width: usize, height: usize) -> Self { + Self { width, height, content: vec![Cell::default(); width*height] } + } + pub fn get (&self, x: usize, y: usize) -> Option<&Cell> { + let i = self.index_of(x, y); + self.content.get(i) + } + pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> { + let i = self.index_of(x, y); + self.content.get_mut(i) + } + pub fn index_of (&self, x: usize, y: usize) -> usize { + y * self.width + x + } +} +from!(|size:(usize, usize)| BigBuffer = Self::new(size.0, size.1)); diff --git a/tui/src/tui_color.rs b/tui/src/tui_color.rs new file mode 100644 index 0000000..cdf278e --- /dev/null +++ b/tui/src/tui_color.rs @@ -0,0 +1,163 @@ +use crate::*; +use rand::{thread_rng, distributions::uniform::UniformSampler}; +impl Tui { + pub const fn null () -> Color { Color::Reset } + pub const fn g (g: u8) -> Color { Color::Rgb(g, g, g) } + pub const fn red () -> Color { Color::Rgb(255,0, 0) } + pub const fn orange () -> Color { Color::Rgb(255,128,0) } + pub const fn yellow () -> Color { Color::Rgb(255,255,0) } + pub const fn brown () -> Color { Color::Rgb(128,255,0) } + pub const fn green () -> Color { Color::Rgb(0,255,0) } + pub const fn electric () -> Color { Color::Rgb(0,255,128) } + //fn bg0 () -> Color { Color::Rgb(20, 20, 20) } + //fn bg () -> Color { Color::Rgb(28, 35, 25) } + //fn border_bg () -> Color { Color::Rgb(40, 50, 30) } + //fn border_fg (f: bool) -> Color { if f { Self::bo1() } else { Self::bo2() } } + //fn title_fg (f: bool) -> Color { if f { Self::ti1() } else { Self::ti2() } } + //fn separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) } + //fn mode_bg () -> Color { Color::Rgb(150, 160, 90) } + //fn mode_fg () -> Color { Color::Rgb(255, 255, 255) } + //fn status_bar_bg () -> Color { Color::Rgb(28, 35, 25) } + //fn bo1 () -> Color { Color::Rgb(100, 110, 40) } + //fn bo2 () -> Color { Color::Rgb(70, 80, 50) } + //fn ti1 () -> Color { Color::Rgb(150, 160, 90) } + //fn ti2 () -> Color { Color::Rgb(120, 130, 100) } +} +pub trait HasColor { fn color (&self) -> ItemColor; } +#[macro_export] macro_rules! has_color { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? { + fn color (&$self) -> ItemColor { $cb } + } + } +} +/// A color in OKHSL and RGB representations. +#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor { + pub okhsl: Okhsl, + pub rgb: Color, +} +from!(|okhsl: Okhsl|ItemColor = Self { okhsl, rgb: okhsl_to_rgb(okhsl) }); +pub fn okhsl_to_rgb (color: Okhsl) -> Color { + let Srgb { red, green, blue, .. }: Srgb = Srgb::from_color_unclamped(color); + Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,) +} +from!(|rgb: Color|ItemColor = Self { rgb, okhsl: rgb_to_okhsl(rgb) }); +pub fn rgb_to_okhsl (color: Color) -> Okhsl { + if let Color::Rgb(r, g, b) = color { + Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)) + } else { + unreachable!("only Color::Rgb is supported") + } +} +// A single color within item theme parameters, in OKHSL and RGB representations. +impl ItemColor { + pub const fn from_rgb (rgb: Color) -> Self { + Self { rgb, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) } + } + pub const fn from_okhsl (okhsl: Okhsl) -> Self { + Self { rgb: Color::Rgb(0, 0, 0), okhsl } + } + pub fn random () -> Self { + let mut rng = thread_rng(); + let lo = Okhsl::new(-180.0, 0.01, 0.25); + let hi = Okhsl::new( 180.0, 0.9, 0.5); + UniformOkhsl::new(lo, hi).sample(&mut rng).into() + } + pub fn random_dark () -> Self { + let mut rng = thread_rng(); + let lo = Okhsl::new(-180.0, 0.025, 0.075); + let hi = Okhsl::new( 180.0, 0.5, 0.150); + UniformOkhsl::new(lo, hi).sample(&mut rng).into() + } + pub fn random_near (color: Self, distance: f32) -> Self { + color.mix(Self::random(), distance) + } + pub fn mix (&self, other: Self, distance: f32) -> Self { + if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); } + self.okhsl.mix(other.okhsl, distance).into() + } +} +/// A color in OKHSL and RGB with lighter and darker variants. +#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemPalette { + pub base: ItemColor, + pub light: ItemColor, + pub lighter: ItemColor, + pub lightest: ItemColor, + pub dark: ItemColor, + pub darker: ItemColor, + pub darkest: ItemColor, +} +impl ItemPalette { + pub const G: [Self;256] = { + let mut builder = konst::array::ArrayBuilder::new(); + while !builder.is_full() { + let index = builder.len() as u8; + let light = (index as f64 * 1.3) as u8; + let lighter = (index as f64 * 1.6) as u8; + let lightest = (index as f64 * 1.9) as u8; + let dark = (index as f64 * 0.9) as u8; + let darker = (index as f64 * 0.6) as u8; + let darkest = (index as f64 * 0.3) as u8; + builder.push(ItemPalette { + base: ItemColor::from_rgb(Color::Rgb(index, index, index )), + light: ItemColor::from_rgb(Color::Rgb(light, light, light, )), + lighter: ItemColor::from_rgb(Color::Rgb(lighter, lighter, lighter, )), + lightest: ItemColor::from_rgb(Color::Rgb(lightest, lightest, lightest, )), + dark: ItemColor::from_rgb(Color::Rgb(dark, dark, dark, )), + darker: ItemColor::from_rgb(Color::Rgb(darker, darker, darker, )), + darkest: ItemColor::from_rgb(Color::Rgb(darkest, darkest, darkest, )), + }); + } + builder.build() + }; + pub fn random () -> Self { ItemColor::random().into() } + pub fn random_near (color: Self, distance: f32) -> Self { + color.base.mix(ItemColor::random(), distance).into() + } + pub const G00: Self = { + let color: ItemColor = ItemColor { + okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 }, + rgb: Color::Rgb(0, 0, 0) + }; + Self { + base: color, + light: color, + lighter: color, + lightest: color, + dark: color, + darker: color, + darkest: color, + } + }; + pub fn from_tui_color (base: Color) -> Self { + Self::from_item_color(ItemColor::from_rgb(base)) + } + pub fn from_item_color (base: ItemColor) -> Self { + let mut light = base.okhsl; + light.lightness = (light.lightness * 1.3).min(1.0); + let mut lighter = light; + lighter.lightness = (lighter.lightness * 1.3).min(1.0); + let mut lightest = base.okhsl; + lightest.lightness = 0.95; + let mut dark = base.okhsl; + dark.lightness = (dark.lightness * 0.75).max(0.0); + dark.saturation = (dark.saturation * 0.75).max(0.0); + let mut darker = dark; + darker.lightness = (darker.lightness * 0.66).max(0.0); + darker.saturation = (darker.saturation * 0.66).max(0.0); + let mut darkest = darker; + darkest.lightness = 0.1; + darkest.saturation = (darkest.saturation * 0.50).max(0.0); + Self { + base, + light: light.into(), + lighter: lighter.into(), + lightest: lightest.into(), + dark: dark.into(), + darker: darker.into(), + darkest: darkest.into(), + } + } +} +from!(|base: Color| ItemPalette = Self::from_tui_color(base)); +from!(|base: ItemColor|ItemPalette = Self::from_item_color(base)); diff --git a/tui/src/tui_content.rs b/tui/src/tui_content.rs new file mode 100644 index 0000000..5db1dac --- /dev/null +++ b/tui/src/tui_content.rs @@ -0,0 +1,522 @@ +use crate::*; +use crate::Color::*; +use ratatui::prelude::Position; +macro_rules! impl_content_layout_render { + ($Output:ty: |$self:ident: $Struct:ty, $to:ident| layout = $layout:expr; render = $render:expr) => { + impl Content<$Output> for $Struct { + fn layout (&$self, $to: [u16;4]) -> [u16;4] { $layout } + fn render (&$self, $to: &mut $Output) { $render } + } + } +} +impl_content_layout_render!(TuiOut: |self: &str, to| + layout = to.center_xy([self.chars().count() as u16, 1]); + render = {let [x, y, ..] = Content::layout(self, to.area()); + to.blit(self, x, y, None)}); +impl_content_layout_render!(TuiOut: |self: String, to| + layout = to.center_xy([self.chars().count() as u16, 1]); + render = {let [x, y, ..] = Content::layout(self, to.area()); + to.blit(self, x, y, None)}); +impl_content_layout_render!(TuiOut: |self: std::sync::RwLock, to| + layout = Content::::layout(&self.read().unwrap(), to); + render = Content::::render(&self.read().unwrap(), to)); +impl_content_layout_render!(TuiOut: |self: std::sync::RwLockReadGuard<'_, String>, to| + layout = Content::::layout(&**self, to); + render = Content::::render(&**self, to)); +impl_content_layout_render!(TuiOut: |self: Arc, to| + layout = to.center_xy([self.chars().count() as u16, 1]); + render = to.blit(self, to.area.x(), to.area.y(), None)); +impl> Content for std::sync::Arc { + fn layout (&self, to: [u16;4]) -> [u16;4] { + Content::::layout(&**self, to) + } + fn render (&self, to: &mut TuiOut) { + Content::::render(&**self, to) + } +} + +pub struct FieldH(pub ItemPalette, pub T, pub U); +impl, U: Content> Content for FieldH { + fn content (&self) -> impl Render { + let Self(ItemPalette { darkest, dark, lighter, lightest, .. }, title, value) = self; + row!( + Tui::fg_bg(dark.rgb, darkest.rgb, "▐"), + Tui::fg_bg(lighter.rgb, dark.rgb, Tui::bold(true, title)), + Tui::fg_bg(dark.rgb, darkest.rgb, "▌"), + Tui::fg_bg(lightest.rgb, darkest.rgb, value), + ) + } +} + +pub struct FieldV(pub ItemPalette, pub T, pub U); +impl, U: Content> Content for FieldV { + fn content (&self) -> impl Render { + let Self(ItemPalette { darkest, dark, lighter, lightest, .. }, title, value) = self; + let sep1 = Tui::bg(darkest.rgb, Tui::fg(dark.rgb, "▐")); + let sep2 = Tui::bg(darkest.rgb, Tui::fg(dark.rgb, "▌")); + let title = Tui::bg(dark.rgb, Tui::fg(lighter.rgb, Tui::bold(true, title))); + let value = Tui::bg(darkest.rgb, Tui::fg(lightest.rgb, value)); + Bsp::e(Bsp::s(row!(sep1, title, sep2), value), " ") + } +} + +pub struct Repeat<'a>(pub &'a str); +impl Content for Repeat<'_> { + fn layout (&self, to: [u16;4]) -> [u16;4] { to } + fn render (&self, to: &mut TuiOut) { + let [x, y, w, h] = to.area().xywh(); + let a = self.0.len(); + for (_v, y) in (y..y+h).enumerate() { + for (u, x) in (x..x+w).enumerate() { + if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) { + let u = u % a; + cell.set_symbol(&self.0[u..u+1]); + } + } + } + } +} + +pub struct RepeatV<'a>(pub &'a str); +impl Content for RepeatV<'_> { + fn layout (&self, to: [u16;4]) -> [u16;4] { to } + fn render (&self, to: &mut TuiOut) { + let [x, y, _w, h] = to.area().xywh(); + for y in y..y+h { + if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) { + cell.set_symbol(&self.0); + } + } + } +} + +pub struct RepeatH<'a>(pub &'a str); +impl Content for RepeatH<'_> { + fn layout (&self, to: [u16;4]) -> [u16;4] { to } + fn render (&self, to: &mut TuiOut) { + let [x, y, w, _h] = to.area().xywh(); + for x in x..x+w { + if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) { + cell.set_symbol(&self.0); + } + } + } +} + +pub struct ScrollbarV { + pub offset: usize, + pub length: usize, + pub total: usize, +} +impl ScrollbarV { + const ICON_DEC: &[char] = &['▲']; + const ICON_INC: &[char] = &['▼']; +} +impl Content for ScrollbarV { + fn render (&self, to: &mut TuiOut) { + let [x, y1, _w, h] = to.area().xywh(); + let y2 = y1 + h; + for (i, y) in (y1..=y2).enumerate() { + if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) { + if (i as usize) < (Self::ICON_DEC.len()) { + cell.set_fg(Rgb(255, 255, 255)); + cell.set_bg(Rgb(0, 0, 0)); + cell.set_char(Self::ICON_DEC[i as usize]); + } else if (i as usize) > (h as usize - Self::ICON_INC.len()) { + cell.set_fg(Rgb(255, 255, 255)); + cell.set_bg(Rgb(0, 0, 0)); + cell.set_char(Self::ICON_INC[h as usize - i]); + } else if false { + cell.set_fg(Rgb(255, 255, 255)); + cell.set_bg(Reset); + cell.set_char('‖'); // ━ + } else { + cell.set_fg(Rgb(0, 0, 0)); + cell.set_bg(Reset); + cell.set_char('╎'); // ━ + } + } + } + } +} + +pub struct ScrollbarH { + pub offset: usize, + pub length: usize, + pub total: usize, +} +impl ScrollbarH { + const ICON_DEC: &[char] = &[' ', '🞀', ' ']; + const ICON_INC: &[char] = &[' ', '🞂', ' ']; +} +impl Content for ScrollbarH { + fn render (&self, to: &mut TuiOut) { + let [x1, y, w, _h] = to.area().xywh(); + let x2 = x1 + w; + for (i, x) in (x1..=x2).enumerate() { + if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) { + if i < (Self::ICON_DEC.len()) { + cell.set_fg(Rgb(255, 255, 255)); + cell.set_bg(Rgb(0, 0, 0)); + cell.set_char(Self::ICON_DEC[x as usize]); + } else if i > (w as usize - Self::ICON_INC.len()) { + cell.set_fg(Rgb(255, 255, 255)); + cell.set_bg(Rgb(0, 0, 0)); + cell.set_char(Self::ICON_INC[w as usize - i]); + } else if false { + cell.set_fg(Rgb(255, 255, 255)); + cell.set_bg(Reset); + cell.set_char('━'); + } else { + cell.set_fg(Rgb(0, 0, 0)); + cell.set_bg(Reset); + cell.set_char('╌'); + } + } + } + } +} + +/// A cell that takes up 3 rows on its own, +/// but stacks, giving (N+1)*2 rows per N cells. +pub struct Phat { + pub width: u16, + pub height: u16, + pub content: T, + pub colors: [Color;4], +} +impl Phat { + /// A phat line + pub fn lo (fg: Color, bg: Color) -> impl Content { + Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▄"))) + } + /// A phat line + pub fn hi (fg: Color, bg: Color) -> impl Content { + Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▀"))) + } +} +impl> Content for Phat { + fn content (&self) -> impl Render { + let [fg, bg, hi, lo] = self.colors; + let top = Fixed::y(1, Self::lo(bg, hi)); + let low = Fixed::y(1, Self::hi(bg, lo)); + let content = Tui::fg_bg(fg, bg, &self.content); + Min::xy(self.width, self.height, Bsp::s(top, Bsp::n(low, Fill::xy(content)))) + } +} + +pub trait TuiStyle { + fn fg > (color: Color, w: R) -> Foreground { + Foreground(color, w) + } + fn bg > (color: Color, w: R) -> Background { + Background(color, w) + } + fn fg_bg > (fg: Color, bg: Color, w: R) -> Background> { + Background(bg, Foreground(fg, w)) + } + fn bold > (enable: bool, w: R) -> Bold { + Bold(enable, w) + } + fn border , S: BorderStyle> (enable: bool, style: S, w: R) -> Bordered { + Bordered(enable, style, w) + } +} + +impl TuiStyle for Tui {} + +pub struct Bold>(pub bool, R); +impl> Content for Bold { + fn content (&self) -> impl Render { &self.1 } + fn render (&self, to: &mut TuiOut) { + to.fill_bold(to.area(), self.0); + self.1.render(to) + } +} + +pub struct Foreground>(pub Color, R); +impl> Content for Foreground { + fn content (&self) -> impl Render { &self.1 } + fn render (&self, to: &mut TuiOut) { + to.fill_fg(to.area(), self.0); + self.1.render(to) + } +} + +pub struct Background>(pub Color, R); +impl> Content for Background { + fn content (&self) -> impl Render { &self.1 } + fn render (&self, to: &mut TuiOut) { + to.fill_bg(to.area(), self.0); + self.1.render(to) + } +} + +pub struct Styled>(pub Option