diff --git a/Cargo.lock b/Cargo.lock index f104322..9e6f803 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,17 +2,508 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[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" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.13.1", + "crossterm_winapi", + "derive_more", + "document-features", + "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 = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + +[[package]] +name = "darling" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.3", +] + +[[package]] +name = "darling_macro" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + [[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +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 = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" @@ -21,9 +512,442 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.20", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[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.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +dependencies = [ + "libc", +] + +[[package]] +name = "line-clipping" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" +dependencies = [ + "approx", + "libm", + "palette_derive", + "palette_math", +] + +[[package]] +name = "palette_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" +dependencies = [ + "libm", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pest" +version = "2.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pest_meta" +version = "2.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" +dependencies = [ + "pest", +] + +[[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_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "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 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.107" @@ -42,14 +966,239 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "ratatui" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termina", + "ratatui-termwiz", + "ratatui-widgets", + "serde", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags 2.13.1", + "compact_str", + "critical-section", + "hashbrown 0.17.1", + "itertools", + "kasuari", + "lru", + "palette", + "serde", + "strum", + "thiserror 2.0.20", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termina" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" +dependencies = [ + "bitflags 2.13.1", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustkeep" version = "0.1.0" dependencies = [ + "directories", + "ratatui", "serde", "toml", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.229" @@ -77,7 +1226,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -89,6 +1238,121 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[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.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "3.0.3" @@ -100,6 +1364,143 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.1", + "parking_lot", + "rustix", + "signal-hook", + "windows-sys", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.13.1", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + [[package]] name = "toml" version = "1.1.4+spec-1.1.0" @@ -139,14 +1540,257 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "atomic", + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[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-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" diff --git a/Cargo.toml b/Cargo.toml index 026b010..52a05b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,6 @@ edition = "2024" [dependencies] serde = { version = "1.0.229", features = ["derive"] } toml = "1.1.4" +# crossterm kommt als Re-Export mit (Ticket 06/12 — kein eigener Eintrag noetig) +ratatui = "0.30.2" +directories = "6.0.0" diff --git a/mods/core/character_options.toml b/mods/core/character_options.toml new file mode 100644 index 0000000..3d65948 --- /dev/null +++ b/mods/core/character_options.toml @@ -0,0 +1,48 @@ +# Rasse und Klasse sind Charakteroptionen, keine eigenen Konzepte (Ticket 02). +# Die Kategorien sind selbst Inhalt und tragen `pick = N`, damit ein Mod eine +# dritte Wahl ergaenzen kann, ohne dass die Engine davon weiss. + +[[categories]] +id = "race" +name = "Rasse" +order = 10 +pick = 1 + +[[categories]] +id = "class" +name = "Klasse" +order = 20 +pick = 1 + +[[options]] +id = "dwarf" +category = "core:race" +name = "Zwerg" +description = "Zäh, misstrauisch, trinkfest." +modifiers = [ { attribute = "constitution", flat = 2 }, + { attribute = "max_health", flat = 10 } ] + +[[options]] +id = "elf" +category = "core:race" +name = "Elf" +description = "Flink, hochmütig, langlebig." +modifiers = [ { attribute = "dexterity", flat = 2 }, + { attribute = "luck", flat = 1 } ] + +[[options]] +id = "warrior" +category = "core:class" +name = "Krieger" +description = "Schlägt zu, wo andere nachdenken." +modifiers = [ { attribute = "strength", flat = 3 }, + { attribute = "damage", percent = 10 } ] + +[[options]] +id = "thief" +category = "core:class" +name = "Dieb" +description = "Nimmt, was nicht niet- und nagelfest ist." +modifiers = [ { attribute = "dexterity", flat = 2 }, + { attribute = "luck", flat = 2 }, + { attribute = "critical_chance", percent = 15 } ] diff --git a/mods/core/items.toml b/mods/core/items.toml new file mode 100644 index 0000000..3119571 --- /dev/null +++ b/mods/core/items.toml @@ -0,0 +1,38 @@ +# Es gibt keinen Item-Typ (Ticket 02). Ein Schwert ist ein Gegenstand mit einem +# Modifikator auf `damage`, ein Helm einer mit einem Modifikator auf Rüstung — +# der Unterschied steht in den Modifikatoren, nicht in einer Kategorie. + +[[items]] +id = "iron_sword" +name = "Eisenschwert" +base_price = 45 +slot = "core:hand" +symbol = "/" +color = "white" +modifiers = [ { attribute = "damage", flat = 5 } ] + +[[items]] +id = "leather_cap" +name = "Lederkappe" +base_price = 18 +slot = "core:head" +symbol = "^" +color = "yellow" +modifiers = [ { attribute = "max_health", flat = 8 } ] + +[[items]] +id = "health_potion" +name = "Heiltrank" +base_price = 12 +stackable = true +consumed_on_use = true +symbol = "!" +color = "red" + +[[items]] +id = "gold" +name = "Gold" +base_price = 1 +stackable = true +symbol = "$" +color = "yellow" diff --git a/mods/core/manifest.toml b/mods/core/manifest.toml new file mode 100644 index 0000000..a499738 --- /dev/null +++ b/mods/core/manifest.toml @@ -0,0 +1,4 @@ +id = "core" +name = "rustkeep" +version = "0.1.0" +depends = [] diff --git a/mods/core/slots.toml b/mods/core/slots.toml new file mode 100644 index 0000000..74935a8 --- /dev/null +++ b/mods/core/slots.toml @@ -0,0 +1,17 @@ +# Slots sind Inhalt, kein Aufzaehlungswert — der Charakterbogen rendert die +# vorhandenen, statt ein festes Layout zu mauern (Ticket 02). + +[[slots]] +id = "head" +name = "Kopf" +order = 10 + +[[slots]] +id = "body" +name = "Rumpf" +order = 20 + +[[slots]] +id = "hand" +name = "Hand" +order = 30 diff --git a/src/data/loader.rs b/src/data/loader.rs new file mode 100644 index 0000000..b4c999b --- /dev/null +++ b/src/data/loader.rs @@ -0,0 +1,535 @@ +//! Mod-Ladepipeline (Ticket 03) und Fehlerstrategie (Ticket 04). +//! +//! Drei Phasen: Manifeste → Einlesen → Aufloesen. In Phase 2 wird nichts +//! aufgeloest, dadurch gibt es **keine Vorwaertsreferenzen** — ein Mod darf auf +//! etwas zeigen, das erst ein spaeterer liefert. + +use std::collections::HashMap; +use std::fmt; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::Deserialize; +use serde::de::DeserializeOwned; +use toml::Spanned; + +use super::registries::{Category, CharacterOption, Item, Registries, Slot}; + +/// Genau drei harte Fehler (Ticket 04). Alles Uebrige ist Warnung plus +/// Verwerfen des **einzelnen Eintrags**. +/// +/// Handgeschrieben statt `thiserror`: drei Varianten, ein Konsument — und in +/// einem Ticket, das Rust lernen soll, ist dieses `match` der Zweck. +#[derive(Debug)] +pub enum LoadError { + CoreMissing, + DependencyMissing { module: String, dep: String }, + DependencyOrder { module: String, dep: String }, +} + +impl fmt::Display for LoadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LoadError::CoreMissing => write!( + f, + "Das Kernspiel wurde nicht gefunden. Erwartet wird ein Ordner \ + 'mods/core' mit einer manifest.toml." + ), + LoadError::DependencyMissing { module, dep } => { + write!(f, "'{module}' braucht '{dep}', aber '{dep}' ist nicht installiert.") + } + LoadError::DependencyOrder { module, dep } => write!( + f, + "'{module}' muss nach '{dep}' geladen werden, steht in der \ + load_order.toml aber davor." + ), + } + } +} + +// ============================================================================ +// Manifest und Mod-Erkennung +// ============================================================================ + +#[derive(Debug, Deserialize)] +struct Manifest { + id: String, + #[allow(dead_code)] + name: String, + /// Einziger Leser ist der Save-Header (Ticket 03/09). + #[allow(dead_code)] + version: String, + #[serde(default)] + depends: Vec, +} + +#[derive(Debug)] +struct ModInfo { + id: String, + depends: Vec, + path: PathBuf, +} + +/// Zwei Suchpfade (Ticket 03): `mods/` neben der Binary und `mods/` im +/// Benutzerprofil. Bei gleicher Mod-ID **gewinnt das Profil** — es existiert +/// genau dafuer, dass die Sachen des Spielers eine Neuinstallation ueberleben. +fn search_paths() -> Vec { + let mut paths = Vec::new(); + + if let Ok(exe) = std::env::current_exe() + && let Some(dir) = exe.parent() + { + paths.push(dir.join("mods")); + } + // Entwicklungsfall: `cargo run` legt die Binary nach target/debug/. + paths.push(PathBuf::from("mods")); + + if let Some(dir) = profile_dir() { + paths.push(dir.join("mods")); + } + paths +} + +/// `data_dir()` ueber die Crate `directories` — Windows, Linux *und* macOS. +/// Keine Trennung von Daten und Konfiguration: Mods, `load_order.toml`, Save +/// und Logs liegen in einem Ordner (Ticket 03). +pub fn profile_dir() -> Option { + directories::ProjectDirs::from("", "", "rustkeep").map(|d| d.data_dir().to_path_buf()) +} + +fn discover(warnings: &mut Vec) -> Vec { + let mut found: HashMap = HashMap::new(); + + for base in search_paths() { + let Ok(entries) = fs::read_dir(&base) else { continue }; + + for entry in entries.flatten() { + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let manifest_path = path.join("manifest.toml"); + let Ok(raw) = fs::read_to_string(&manifest_path) else { + // Ordner ohne Manifest ist kein Mod — sonst legt eine vergessene + // screenshots/-Kopie das Spiel lahm (Ticket 03). + warnings.push(format!("{}: kein manifest.toml, übersprungen", path.display())); + continue; + }; + + match toml::from_str::(&raw) { + Ok(m) => { + if let Some(old) = found.get(&m.id) { + warnings.push(format!( + "'{}' liegt zweimal: {} gewinnt gegen {}", + m.id, + path.display(), + old.path.display() + )); + } + found.insert( + m.id.clone(), + ModInfo { id: m.id, depends: m.depends, path }, + ); + } + Err(e) => warnings.push(format!("{}: {}", manifest_path.display(), e.message())), + } + } + } + + found.into_values().collect() +} + +// ============================================================================ +// Ladereihenfolge — selbstheilend (Ticket 03) +// ============================================================================ + +#[derive(Debug, Default, Deserialize, serde::Serialize)] +struct LoadOrder { + #[serde(default)] + mods: Vec, +} + +fn resolve_order(found: &[ModInfo], warnings: &mut Vec) -> Vec { + let file = profile_dir().map(|d| d.join("load_order.toml")); + + let mut order: Vec = file + .as_ref() + .and_then(|p| fs::read_to_string(p).ok()) + .and_then(|raw| toml::from_str::(&raw).ok()) + .map(|lo| lo.mods) + .unwrap_or_default(); + + // Eintrag ohne Ordner: Warnzeile und ueberspringen. Das ist der Normalfall + // nach dem Deinstallieren und darf den Start nicht verhindern. + order.retain(|id| { + let exists = found.iter().any(|m| &m.id == id); + if !exists { + warnings.push(format!("'{id}' steht in load_order.toml, ist aber nicht installiert")); + } + exists + }); + + // Ordner ohne Eintrag: hinten anhaengen. Ein entpackter Mod laeuft damit, + // ohne dass der Spieler von der Sortierdatei wissen muss. + let mut fresh: Vec<&str> = found + .iter() + .filter(|m| !order.contains(&m.id)) + .map(|m| m.id.as_str()) + .collect(); + fresh.sort_unstable(); + + if order.is_empty() { + // Erststart: core zuerst, Rest alphabetisch. + if let Some(pos) = fresh.iter().position(|id| *id == "core") { + order.push(fresh.remove(pos).to_string()); + } + } + order.extend(fresh.into_iter().map(str::to_string)); + + if let Some(path) = file { + let _ = fs::create_dir_all(path.parent().unwrap()); + if let Ok(text) = toml::to_string(&LoadOrder { mods: order.clone() }) { + // Schreibfehler wird verschluckt — ein Spiel, das wegen seiner + // Sortierdatei nicht startet, waere die Karikatur guter + // Fehlerbehandlung (Ticket 04). + let _ = fs::write(&path, text); + } + } + order +} + +/// `depends` sortiert nicht, es **prueft** — das ist der Preis dafuer, dass ein +/// Mensch die Reihenfolge pflegt (Ticket 03). +fn check_depends(found: &[ModInfo], order: &[String]) -> Result<(), LoadError> { + for (i, id) in order.iter().enumerate() { + let Some(info) = found.iter().find(|m| &m.id == id) else { continue }; + + for dep in &info.depends { + match order.iter().position(|o| o == dep) { + None => { + return Err(LoadError::DependencyMissing { + module: id.clone(), + dep: dep.clone(), + }); + } + Some(j) if j > i => { + return Err(LoadError::DependencyOrder { + module: id.clone(), + dep: dep.clone(), + }); + } + Some(_) => {} + } + } + } + Ok(()) +} + +// ============================================================================ +// Phase 2 — Einlesen, zweistufig (Ticket 04) +// ============================================================================ + +/// Jede Datei darf jede Registry-Tabelle enthalten (Ticket 03). `toml::Value` +/// als Zwischenstufe ist der Grund, warum ein kaputter Eintrag nur *sich selbst* +/// mitreisst und nicht die ganze Datei. +#[derive(Debug, Default, Deserialize)] +struct ModFile { + #[serde(default)] + items: Vec>, + #[serde(default)] + slots: Vec>, + #[serde(default)] + categories: Vec>, + #[serde(default)] + options: Vec>, +} + +/// Damit die vier Registry-Typen durch dieselbe Einlese-Schleife passen. +trait Entry: DeserializeOwned { + fn id(&self) -> &str; + fn set_id(&mut self, id: String); + fn overrides(&self) -> Option<&str>; +} + +macro_rules! impl_entry { + ($t:ty) => { + impl Entry for $t { + fn id(&self) -> &str { + &self.id + } + fn set_id(&mut self, id: String) { + self.id = id; + } + fn overrides(&self) -> Option<&str> { + self.overrides.as_deref() + } + } + }; +} +impl_entry!(Item); +impl_entry!(Slot); +impl_entry!(Category); +impl_entry!(CharacterOption); + +struct FileCtx<'a> { + module: &'a str, + /// Pfad relativ zum Mod-Ordner — die Mod-ID steht ohnehin vorn. + file: String, + source: &'a str, +} + +/// Zeile aus dem Byte-Offset. `toml::de::Error` und `Spanned` liefern nur +/// Offsets, keine Zeilen (gegen die Doku geprueft). +fn line_of(source: &str, offset: usize) -> usize { + source[..offset.min(source.len())].matches('\n').count() + 1 +} + +fn parse_entries( + raw: Vec>, + ctx: &FileCtx<'_>, + into: &mut HashMap, + warnings: &mut Vec, +) { + for (index, spanned) in raw.into_iter().enumerate() { + let line = line_of(ctx.source, spanned.span().start); + let value = spanned.into_inner(); + + let mut entry = match T::deserialize(value) { + Ok(e) => e, + Err(e) => { + // Ein Eintrag ohne lesbare `id` kann sich nicht benennen und + // heisst nach seiner Position (Ticket 04). + warnings.push(format!( + "{}/{}:{}: [{}] — {}", + ctx.module, ctx.file, line, index, e.message() + )); + continue; + } + }; + + // Der Lader stellt manifest.id automatisch voran (Ticket 03). + let own_id = format!("{}:{}", ctx.module, entry.id()); + + // `overrides` ersetzt den genannten Eintrag und uebernimmt dessen ID — + // die eigene wird dann gar nicht registriert (Ticket 03). + let key = match entry.overrides() { + Some(target) => { + let target = target.to_string(); + if !into.contains_key(&target) { + warnings.push(format!( + "{}/{}:{}: {} überschreibt '{}', das es nicht gibt — verworfen", + ctx.module, ctx.file, line, own_id, target + )); + continue; + } + warnings.push(format!("{} überschreibt '{}'", ctx.module, target)); + target + } + None => { + if into.contains_key(&own_id) { + warnings.push(format!("{} überschreibt '{}'", ctx.module, own_id)); + } + own_id + } + }; + + entry.set_id(key.clone()); + into.insert(key, entry); + } +} + +// ============================================================================ +// Phase 3 — Aufloesen +// ============================================================================ + +fn resolve(reg: &mut Registries, warnings: &mut Vec) { + let known_slots: Vec = reg.slots.keys().cloned().collect(); + reg.items.retain(|id, item| match &item.slot { + Some(slot) if !known_slots.contains(slot) => { + warnings.push(format!("{id}: Slot '{slot}' existiert nicht — verworfen")); + false + } + _ => true, + }); + + let known_categories: Vec = reg.categories.keys().cloned().collect(); + reg.options.retain(|id, option| { + if known_categories.contains(&option.category) { + true + } else { + warnings.push(format!( + "{id}: Kategorie '{}' existiert nicht — verworfen", + option.category + )); + false + } + }); +} + +// ============================================================================ +// Einstiegspunkt +// ============================================================================ + +pub fn load_mods() -> Result<(Registries, Vec), LoadError> { + let mut warnings = Vec::new(); + + // Phase 1 — Manifeste, Reihenfolge, depends + let found = discover(&mut warnings); + if !found.iter().any(|m| m.id == "core") { + return Err(LoadError::CoreMissing); + } + let order = resolve_order(&found, &mut warnings); + check_depends(&found, &order)?; + + // Phase 2 — Einlesen in Ladereihenfolge + let mut reg = Registries::default(); + for id in &order { + let Some(info) = found.iter().find(|m| &m.id == id) else { continue }; + + for file in toml_files(&info.path) { + let name = file + .strip_prefix(&info.path) + .unwrap_or(&file) + .to_string_lossy() + .into_owned(); + + let Ok(source) = fs::read_to_string(&file) else { continue }; + + // Kaputte Syntax reisst die ganze Datei mit — da kommt der Parser + // schlicht nicht weiter. Das ist keine Entscheidung, sondern eine + // Tatsache (Ticket 04). + let parsed: ModFile = match toml::from_str(&source) { + Ok(p) => p, + Err(e) => { + let line = e.span().map(|s| line_of(&source, s.start)).unwrap_or(0); + warnings.push(format!("{}/{}:{}: {}", info.id, name, line, e.message())); + continue; + } + }; + + let ctx = FileCtx { module: &info.id, file: name, source: &source }; + parse_entries(parsed.items, &ctx, &mut reg.items, &mut warnings); + parse_entries(parsed.slots, &ctx, &mut reg.slots, &mut warnings); + parse_entries(parsed.categories, &ctx, &mut reg.categories, &mut warnings); + parse_entries(parsed.options, &ctx, &mut reg.options, &mut warnings); + } + } + + // Phase 3 — Referenzen aufloesen + resolve(&mut reg, &mut warnings); + + Ok((reg, warnings)) +} + +fn toml_files(dir: &Path) -> Vec { + let mut files: Vec = fs::read_dir(dir) + .into_iter() + .flatten() + .flatten() + .map(|e| e.path()) + .filter(|p| p.extension().is_some_and(|e| e == "toml")) + .filter(|p| p.file_name().is_some_and(|n| n != "manifest.toml")) + .collect(); + files.sort(); + files +} + +// ============================================================================ +// Warnungen ins Log (Ticket 04) +// ============================================================================ + +/// Schreibt die Warnliste, behaelt die letzten zehn Laeufe und gibt den +/// Dateinamen zurueck. Ein sauberer Lauf legt keine Datei an. +pub fn write_log(warnings: &[String]) -> Option { + if warnings.is_empty() { + return None; + } + let dir = profile_dir()?.join("logs"); + fs::create_dir_all(&dir).ok()?; + + // ponytail: Sekunden seit Epoch statt JJJJMMTT-HHMMSS. Sortierbar, und die + // Alternative waere Datumsarithmetik von Hand oder eine Crate dafuer. + let stamp = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs(); + let name = format!("load-{stamp}.log"); + fs::write(dir.join(&name), warnings.join("\n")).ok()?; + + let mut old: Vec = fs::read_dir(&dir) + .into_iter() + .flatten() + .flatten() + .map(|e| e.path()) + .filter(|p| { + p.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.starts_with("load-") && n.ends_with(".log")) + }) + .collect(); + old.sort(); + for path in old.iter().rev().skip(10) { + let _ = fs::remove_file(path); + } + + Some(name) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn zeilennummer_aus_offset() { + let src = "a\nb\nc"; + assert_eq!(line_of(src, 0), 1); + assert_eq!(line_of(src, 2), 2); + assert_eq!(line_of(src, 4), 3); + } + + /// Der Check aus Ticket 04: Ein kaputtes Kernspiel faellt beim `cargo test` + /// auf, nicht beim Spieler — und die Ladelogik bleibt frei von Sonderregeln. + #[test] + fn core_laedt_ohne_warnungen() { + let (reg, warnings) = load_mods().expect("core muss laden"); + let relevant: Vec<&String> = + warnings.iter().filter(|w| !w.contains("load_order.toml")).collect(); + assert!(relevant.is_empty(), "core lädt nicht sauber: {relevant:#?}"); + assert!(reg.items.contains_key("core:iron_sword")); + assert!(reg.slots.contains_key("core:hand")); + assert!(reg.options.contains_key("core:dwarf")); + } + + /// Ticket 04 versprach, dass ein kaputter Eintrag nur *sich selbst* + /// mitreisst. Das ist mit serde nicht gratis — hier wird es geprueft. + #[test] + fn kaputter_eintrag_reisst_die_nachbarn_nicht_mit() { + let source = r#" +[[items]] +id = "gut" +name = "Gut" + +[[items]] +id = "kaputt" +name = 42 + +[[items]] +id = "auch_gut" +name = "Auch gut" +"#; + let parsed: ModFile = toml::from_str(source).expect("Syntax ist in Ordnung"); + let ctx = FileCtx { module: "test", file: "items.toml".into(), source }; + let mut items: HashMap = HashMap::new(); + let mut warnings = Vec::new(); + parse_entries(parsed.items, &ctx, &mut items, &mut warnings); + + assert_eq!(items.len(), 2, "die beiden heilen Eintraege muessen ueberleben"); + assert!(items.contains_key("test:gut")); + assert!(items.contains_key("test:auch_gut")); + assert_eq!(warnings.len(), 1); + // `Spanned` zeigt auf den Anfang des Eintrags (`[[items]]`, Zeile 6), + // nicht auf das schuldige Feld — genau der Zeilenbereich, den Ticket 04 + // zugesagt hat. Der Feldname steht dafuer in `message()`. + assert!(warnings[0].contains("items.toml:6"), "Zeile fehlt: {}", warnings[0]); + assert!(warnings[0].contains("expected a string")); + } +} diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..3e069db --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1,8 @@ +//! Definitionen und Mod-Lader. Kennt ratatui nicht — Farben sind hier Namen, +//! uebersetzt wird in `ui/theme.rs` (Ticket 11). + +pub mod loader; +pub mod registries; + +pub use loader::{load_mods, write_log}; +pub use registries::Registries; diff --git a/src/data/registries.rs b/src/data/registries.rs new file mode 100644 index 0000000..c768aaf --- /dev/null +++ b/src/data/registries.rs @@ -0,0 +1,116 @@ +//! Die Definitionen, die aus TOML geladen werden — Ticket 02. +//! +//! Einige Felder liest in M1 noch niemand: `base_price` wartet auf den Handel, +//! `stackable` und `consumed_on_use` aufs Inventar, `symbol` auf Gegenstaende, +//! die auf dem Boden liegen. Sie stehen trotzdem hier, weil sie **das +//! Datenmodell** sind und `mods/core` sie bereits schreibt — sie erst spaeter +//! einzufuehren hiesse, die TOML-Dateien zweimal anzufassen. +//! +//! ponytail: modulweites `allow` — es versteckt ab M2 auch echte Leichen. +//! Raus, sobald Handel und Inventar die Felder tatsaechlich lesen. +#![allow(dead_code)] + +use std::collections::HashMap; + +use serde::Deserialize; + +use crate::game::Modifier; + +/// Ein Ding im Spiel, **einheitlich strukturiert**. Es gibt keinen Item-Typ: +/// `enum ItemType` vermischte Slot, Verhalten und Kategorie (Ticket 02). +#[derive(Debug, Clone, Deserialize)] +pub struct Item { + pub id: String, + pub name: String, + #[serde(default)] + pub base_price: u32, + /// Leer = nicht ausruestbar. + pub slot: Option, + #[serde(default)] + pub stackable: bool, + #[serde(default)] + pub consumed_on_use: bool, + #[serde(default = "default_symbol")] + pub symbol: char, + #[serde(default)] + pub color: Option, + #[serde(default)] + pub modifiers: Vec, + /// Ersetzt den genannten Eintrag und uebernimmt dessen ID (Ticket 03). + #[serde(default)] + pub overrides: Option, + // TODO(M3+): `actions`, `abilities`, `triggers` — brauchen erst Kampf und + // Event-System. +} + +fn default_symbol() -> char { + '?' +} + +/// Eine benannte Stelle am Charakter. Moddbarer Inhalt, kein fester +/// Aufzaehlungswert (Ticket 02). +#[derive(Debug, Clone, Deserialize)] +pub struct Slot { + pub id: String, + pub name: String, + #[serde(default)] + pub order: i32, + #[serde(default)] + pub overrides: Option, +} + +/// Kategorie von Charakteroptionen — selbst Inhalt, mit `pick = N`. +#[derive(Debug, Clone, Deserialize)] +pub struct Category { + pub id: String, + pub name: String, + #[serde(default)] + pub order: i32, + #[serde(default = "one")] + pub pick: u32, + #[serde(default)] + pub overrides: Option, +} + +fn one() -> u32 { + 1 +} + +/// Eine Wahl beim Charakterbau. Rasse und Klasse sind beides davon. +#[derive(Debug, Clone, Deserialize)] +pub struct CharacterOption { + pub id: String, + pub category: String, + pub name: String, + #[serde(default)] + pub description: String, + #[serde(default)] + pub modifiers: Vec, + #[serde(default)] + pub overrides: Option, + // TODO(M3): `abilities` und `on_create` (Ticket 02) — Faehigkeiten und + // Startausruestung brauchen Kampf bzw. Inventar. +} + +/// Alle geladenen Definitionen. Liegt **neben** `Game`, nicht darin — als Feld +/// kollidierte `&mut game.player` mit `&game.registries` bei jedem +/// Methodenaufruf (Ticket 07). +#[derive(Debug, Default)] +pub struct Registries { + pub items: HashMap, + pub slots: HashMap, + pub categories: HashMap, + pub options: HashMap, + // TODO(M2+): `affixes`, `abilities`, `quests`, `creatures`, `prefabs`, + // `floors`, `loot_tables`, `scripts` — zwoelf sind es am Ende. +} + +impl Registries { + /// Slots in Anzeigereihenfolge. Der Charakterbogen schleift darueber, + /// statt ein Layout zu mauern (Ticket 02). + pub fn slots_ordered(&self) -> Vec<&Slot> { + let mut v: Vec<&Slot> = self.slots.values().collect(); + v.sort_by_key(|s| (s.order, s.id.clone())); + v + } +} diff --git a/src/game/attributes.rs b/src/game/attributes.rs new file mode 100644 index 0000000..80148f7 --- /dev/null +++ b/src/game/attributes.rs @@ -0,0 +1,161 @@ +//! Attributkatalog und Modifikatoren — Ticket 01. +//! +//! Der Katalog ist **fest** und Engine-Vokabular ohne Namensraum. Ein Modder +//! kann ihn nicht erweitern; das ist Absicht (siehe Glossar, „Attribut"). + +use serde::Deserialize; + +/// Fester Katalog aus Ticket 01. `level` steht hier, weil Bedingungen und +/// Formeln es lesen — die Quelle fuer Aufstiegspunkte ist aber `base.level` +/// und nicht dieser gelesene Wert (Ticket 15). +/// +// TODO(M3): `resistance` fehlt — es ist typ-indiziert (sieben Schadenstypen aus +// Ticket 01) und braucht damit eine andere Form als die uebrigen. +// Kommt mit dem Kampf. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +#[repr(usize)] +pub enum Attribute { + Strength, + Dexterity, + Constitution, + Charisma, + Luck, + Level, + MaxHealth, + MaxStamina, + Damage, + HitChance, + CriticalChance, + CriticalFactor, + PriceModifier, + CarryCapacity, +} + +impl Attribute { + pub const COUNT: usize = 14; + + pub const ALL: [Attribute; Self::COUNT] = [ + Attribute::Strength, + Attribute::Dexterity, + Attribute::Constitution, + Attribute::Charisma, + Attribute::Luck, + Attribute::Level, + Attribute::MaxHealth, + Attribute::MaxStamina, + Attribute::Damage, + Attribute::HitChance, + Attribute::CriticalChance, + Attribute::CriticalFactor, + Attribute::PriceModifier, + Attribute::CarryCapacity, + ]; + + pub fn name(self) -> &'static str { + match self { + Attribute::Strength => "Stärke", + Attribute::Dexterity => "Geschicklichkeit", + Attribute::Constitution => "Konstitution", + Attribute::Charisma => "Charisma", + Attribute::Luck => "Glück", + Attribute::Level => "Level", + Attribute::MaxHealth => "Leben (max)", + Attribute::MaxStamina => "Ausdauer (max)", + Attribute::Damage => "Schaden", + Attribute::HitChance => "Trefferchance", + Attribute::CriticalChance => "Kritische Chance", + Attribute::CriticalFactor => "Kritischer Faktor", + Attribute::PriceModifier => "Preis-Modifikator", + Attribute::CarryCapacity => "Tragfähigkeit", + } + } +} + +/// Die Grundwerte eines Wesens. Bewusst ein Array statt vierzehn benannter +/// Felder: `get` und `set` sind damit je eine Zeile statt je eines +/// vierzehnarmigen `match`. +#[derive(Debug, Clone)] +pub struct Attributes([f32; Attribute::COUNT]); + +impl Attributes { + pub fn get(&self, a: Attribute) -> f32 { + self.0[a as usize] + } + + pub fn set(&mut self, a: Attribute, v: f32) { + self.0[a as usize] = v; + } +} + +impl Default for Attributes { + /// Startwerte eines frischen Helden. Zahlen sind bewusst grob — Balancing + /// setzt ein spielbares Spiel voraus (siehe Map, „Not yet specified"). + fn default() -> Self { + let mut a = Attributes([0.0; Attribute::COUNT]); + a.set(Attribute::Strength, 1.0); + a.set(Attribute::Dexterity, 1.0); + a.set(Attribute::Constitution, 1.0); + a.set(Attribute::Charisma, 1.0); + a.set(Attribute::Luck, 1.0); + a.set(Attribute::Level, 1.0); + a.set(Attribute::MaxHealth, 100.0); + a.set(Attribute::MaxStamina, 100.0); + a.set(Attribute::Damage, 2.0); + a.set(Attribute::HitChance, 0.8); + a.set(Attribute::CriticalFactor, 1.5); + a.set(Attribute::CarryCapacity, 50.0); + a + } +} + +/// Rechnet ein Attribut um, solange er existiert — Ticket 01. Er wird nie +/// ausgefuehrt, sondern beim Auslesen aufsummiert. +#[derive(Debug, Clone, Deserialize)] +pub struct Modifier { + pub attribute: Attribute, + #[serde(default)] + pub flat: f32, + #[serde(default)] + pub percent: f32, + // TODO(M3): `duration`, `target` und `condition` aus Ticket 01/08. In M1 + // sind alle Modifikatoren dauerhaft und wirken auf den Traeger. +} + +/// Stapelregel aus Ticket 01: `(Basis + Σ flach) × (1 + Σ prozentual)`, +/// intern `f32`, gerundet an genau einer Stelle — hier gar nicht, weil das +/// Runden dorthin gehoert, wo ein Wert angezeigt oder verrechnet wird. +pub fn stack<'a>(base: f32, modifiers: impl Iterator, attr: Attribute) -> f32 { + let (flat, percent) = modifiers + .filter(|m| m.attribute == attr) + .fold((0.0, 0.0), |(f, p), m| (f + m.flat, p + m.percent)); + (base + flat) * (1.0 + percent / 100.0) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// `ALL` wird von Hand neben dem Enum gepflegt. Wer eine Variante ergaenzt + /// und die Liste vergisst, faellt sonst nirgends auf — der Charakterbogen + /// zeigt das Attribut dann einfach nie. + #[test] + fn all_ist_vollstaendig_und_in_indexreihenfolge() { + assert_eq!(Attribute::ALL.len(), Attribute::COUNT); + for (i, a) in Attribute::ALL.iter().enumerate() { + assert_eq!(*a as usize, i, "{a:?} steht an der falschen Stelle in ALL"); + } + } + + #[test] + fn stapelregel_erst_flach_dann_prozentual() { + let mods = [ + Modifier { attribute: Attribute::Strength, flat: 2.0, percent: 0.0 }, + Modifier { attribute: Attribute::Strength, flat: 3.0, percent: 10.0 }, + // Fremdes Attribut darf nicht mitzaehlen: + Modifier { attribute: Attribute::Damage, flat: 100.0, percent: 0.0 }, + ]; + // (10 + 5) * 1.10 == 16.5, nicht 10*1.1 + 5 == 16.0 + assert_eq!(stack(10.0, mods.iter(), Attribute::Strength), 16.5); + } +} diff --git a/src/game/command.rs b/src/game/command.rs new file mode 100644 index 0000000..6fd41d0 --- /dev/null +++ b/src/game/command.rs @@ -0,0 +1,32 @@ +//! Kommandos der Oberflaeche an das Spiel — Ticket 11. +//! +//! **Nicht** zu verwechseln mit `Action`, dem TOML-Effektvokabular aus Ticket +//! 01. „Geh nach Norden" ist keine der zehn Aktionen und wird nie eine. + +#[derive(Debug, Clone, Copy)] +pub enum Direction { + North, + South, + West, + East, +} + +/// Ticket 11 hat zwoelf Commands festgelegt; M1 braucht zwei davon. +/// +// TODO(M2+): `UseAbility`, `UseItem`, `Equip`, `Unequip`, `Drop`, `PickUp`, +// `Descend`, `Talk`, `Buy`, `Sell` — und `AssignPoints` aus 15. +#[derive(Debug, Clone)] +pub enum Command { + Move(Direction), + Wait, +} + +/// Ergebnis eines Kommandos — der Befund aus dem Durchstich in Ticket 12: +/// „alle Commands kosten einen Zug" heisst **alle ausgefuehrten**. Gegen eine +/// Wand zu laufen darf keine Zeit verbrauchen, sonst tickt ein Gift-Debuff, +/// weil jemand danebengetippt hat. +#[derive(Debug, Clone)] +pub enum Outcome { + TurnPassed, + Rejected(&'static str), +} diff --git a/src/game/creature.rs b/src/game/creature.rs new file mode 100644 index 0000000..a889edd --- /dev/null +++ b/src/game/creature.rs @@ -0,0 +1,73 @@ +//! Wesen — Held und Gegner sind derselbe Typ (Ticket 07). +//! +//! `health`, `stamina`, `tags` und `CreatureRef::Enemy` liest in M1 noch +//! niemand: Sie brauchen den Kampf. Sie stehen trotzdem hier, weil sie die +//! Struktur aus Ticket 07 sind und nicht deren halbe Vorstufe. +#![allow(dead_code)] + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use super::attributes::Attributes; + +/// Ein Exemplar eines Gegenstands. Traegt **nur IDs** — die Affixe sind +/// Registry-Eintraege, keine eingebetteten Modifikatoren (Ticket 07). Genau +/// deshalb bleibt der Speicherstand eine ID-Liste. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ItemInstance { + pub def: String, + #[serde(default)] + pub affixes: Vec, +} + +impl ItemInstance { + pub fn new(def: impl Into) -> Self { + Self { def: def.into(), affixes: Vec::new() } + } +} + +/// Adresse eines Wesens. Faellt direkt aus der Zwei-Felder-Entscheidung in +/// Ticket 07: Held und Gegner liegen getrennt, es gibt also keinen +/// gemeinsamen Index. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CreatureRef { + Player, + Enemy(usize), +} + +#[derive(Debug, Clone)] +pub struct Creature { + pub name: String, + /// Grundwerte. `base.level` ist die einzige Quelle fuer Aufstiegspunkte + /// (Ticket 15) — das *gelesene* Attribut darf hoeher ausfallen. + pub base: Attributes, + /// Zustand, scharf getrennt von den Attributen (Ticket 01). + pub health: f32, + pub stamina: f32, + /// Gewaehlte Charakteroptionen. Ohne dieses Feld haette der Held nach dem + /// Charakterbau keine Rasse mehr (Ticket 07). + pub options: Vec, + pub inventory: Vec, + pub equipment: HashMap, + pub tags: Vec, + // TODO(M3): `modifiers: Vec` (nur befristete), + // `initiative: i32`, `cooldowns: HashMap`. + // Alle drei brauchen den Kampf, um ueberhaupt etwas zu tun. +} + +impl Creature { + pub fn new(name: impl Into) -> Self { + let base = Attributes::default(); + Self { + name: name.into(), + health: base.get(super::attributes::Attribute::MaxHealth), + stamina: base.get(super::attributes::Attribute::MaxStamina), + base, + options: Vec::new(), + inventory: Vec::new(), + equipment: HashMap::new(), + tags: Vec::new(), + } + } +} diff --git a/src/game/floor.rs b/src/game/floor.rs new file mode 100644 index 0000000..e4de2d2 --- /dev/null +++ b/src/game/floor.rs @@ -0,0 +1,91 @@ +//! Etage — Ticket 10 entscheidet den Innenbau, M1 baut einen Testraum von Hand. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Tile { + Wall, + Floor, + Door, + Stairs, + Entrance, +} + +#[derive(Debug)] +pub struct Floor { + pub number: u8, + pub width: u16, + pub height: u16, + pub tiles: Vec, + // TODO(M2): `rooms: Vec` und `player_room: Option` aus Ticket + // 10 — mit ihnen kommen `room_entered`, Kampfbeginn und die + // Raum-Beutetabellen. M1 hat genau einen Raum und braucht die + // Buchfuehrung noch nicht. +} + +impl Floor { + pub fn tile(&self, x: u16, y: u16) -> Tile { + if x >= self.width || y >= self.height { + return Tile::Wall; + } + self.tiles[y as usize * self.width as usize + x as usize] + } + + /// Handgebauter Testraum. Ticket 10 legt fest, dass Prefabs als ASCII-Bloecke + /// geschrieben werden — deshalb steht der Raum hier schon in derselben Form, + /// auch wenn der Generator erst in M2 kommt. + pub fn test_room() -> Self { + const LAYOUT: &[&str] = &[ + "########################", + "#......................#", + "#..####....######......#", + "#..#..........#........#", + "#..#..........#...##...#", + "#.....####....#...##...#", + "#........#........>....#", + "#..<.....#.............#", + "########################", + ]; + + let width = LAYOUT[0].len() as u16; + let height = LAYOUT.len() as u16; + let tiles = LAYOUT + .iter() + .flat_map(|row| row.chars()) + .map(|c| match c { + '#' => Tile::Wall, + '>' => Tile::Stairs, + '<' => Tile::Entrance, + '+' => Tile::Door, + _ => Tile::Floor, + }) + .collect(); + + Self { number: 1, width, height, tiles } + } + + /// Startfeld: das `<` im Layout, sonst das erste begehbare Feld. + pub fn entrance(&self) -> (u16, u16) { + for y in 0..self.height { + for x in 0..self.width { + if self.tile(x, y) == Tile::Entrance { + return (x, y); + } + } + } + (1, 1) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn testraum_ist_rechteckig_und_hat_eingang() { + let f = Floor::test_room(); + assert_eq!(f.tiles.len(), f.width as usize * f.height as usize); + assert_eq!(f.tile(f.entrance().0, f.entrance().1), Tile::Entrance); + // Rand ist geschlossen, sonst laeuft der Held aus der Karte. + assert_eq!(f.tile(0, 0), Tile::Wall); + assert_eq!(f.tile(f.width - 1, f.height - 1), Tile::Wall); + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs new file mode 100644 index 0000000..7564c57 --- /dev/null +++ b/src/game/mod.rs @@ -0,0 +1,191 @@ +//! Der Spielkern. **Kennt ratatui nicht** — `tests/boundaries.rs` prueft das. + +pub mod attributes; +pub mod command; +pub mod creature; +pub mod floor; + +pub use attributes::{Attribute, Modifier}; +pub use command::{Command, Direction, Outcome}; +pub use creature::{Creature, CreatureRef, ItemInstance}; +pub use floor::{Floor, Tile}; + +use crate::data::Registries; + +pub struct Game { + pub player: Creature, + /// Der **laufende Kampf**, nicht die Etage (Ticket 07/14). Leer = kein Kampf. + pub enemies: Vec, + pub floor: Floor, + pub player_pos: (u16, u16), + pub turn: u64, + /// Letzte Meldung fuer die Statuszeile. + pub last: String, + // TODO(M3): `events: Vec` (Warteschlange aus Ticket 08) und der + // xorshift-Seed aus Ticket 14. Beide brauchen erst einen Trigger + // bzw. einen Wurf, um etwas zu tun. + // TODO(M4): `quests`, `current_exp`, `unspent_points` (Ticket 15 — an + // `Game`, nicht am `Creature`). +} + +impl Game { + pub fn new(player: Creature) -> Self { + let floor = Floor::test_room(); + let player_pos = floor.entrance(); + Self { + player, + enemies: Vec::new(), + floor, + player_pos, + turn: 0, + last: String::from("—"), + } + } + + pub fn creature(&self, who: CreatureRef) -> &Creature { + match who { + CreatureRef::Player => &self.player, + CreatureRef::Enemy(i) => &self.enemies[i], + } + } + + /// Der Lesezugang liegt auf `Game`, nicht auf `Creature` — eine Aura wohnt + /// beim Traeger und wird beim Auslesen des Ziels eingesammelt (Ticket 01), + /// ein `Creature` kann seine Attribute also gar nicht allein ausrechnen. + /// + /// Vier Quellen: `base` → `options` → `equipment` samt Affixen → befristete + /// Modifikatoren. **Kein Cache** (Ticket 07): Das passiert pro Tastendruck, + /// nicht pro Frame. + pub fn attribute( + &self, + who: CreatureRef, + attr: Attribute, + _against: Option, + reg: &Registries, + ) -> f32 { + let c = self.creature(who); + + let from_options = c + .options + .iter() + .filter_map(|id| reg.options.get(id)) + .flat_map(|o| o.modifiers.iter()); + + let from_equipment = c.equipment.values().flat_map(|inst| { + reg.items + .get(&inst.def) + .map(|i| i.modifiers.as_slice()) + .unwrap_or(&[]) + .iter() + }); + + // TODO(M3): Affix-Modifikatoren des Exemplars, befristete Modifikatoren + // am Traeger, und `_against` fuer bedingte Modifikatoren + // („+5 Schaden gegen Untote", Ticket 08). + attributes::stack(c.base.get(attr), from_options.chain(from_equipment), attr) + } + + /// Der **eine** Eingang, an dem ein Zug endet (Ticket 11). + pub fn execute(&mut self, cmd: Command, _reg: &Registries) -> Outcome { + let outcome = match cmd { + Command::Move(dir) => self.step(dir), + Command::Wait => { + self.last = String::from("gewartet"); + Outcome::TurnPassed + } + }; + + // Nur ausgefuehrte Kommandos kosten einen Zug (Ticket 12). + if matches!(outcome, Outcome::TurnPassed) { + self.turn += 1; + // TODO(M3): befristete Modifikatoren ticken, Ausdauer regeneriert, + // Abklingzeiten sinken, `turn_started` feuern, dann die + // Event-Warteschlange leerarbeiten (Ticket 08/14). + } + outcome + } + + fn step(&mut self, dir: Direction) -> Outcome { + let (x, y) = self.player_pos; + let (nx, ny) = match dir { + Direction::North => (x, y.saturating_sub(1)), + Direction::South => (x, y + 1), + Direction::West => (x.saturating_sub(1), y), + Direction::East => (x + 1, y), + }; + + if self.floor.tile(nx, ny) == Tile::Wall { + return Outcome::Rejected("Da ist eine Wand"); + } + + self.player_pos = (nx, ny); + self.last = match self.floor.tile(nx, ny) { + Tile::Stairs => String::from("Eine Treppe führt nach unten"), + _ => format!("{},{}", nx, ny), + }; + Outcome::TurnPassed + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data::load_mods; + + fn zwergenkrieger() -> (Game, Registries) { + let (reg, _) = load_mods().expect("core muss laden"); + let mut hero = Creature::new("Testheld"); + hero.options = vec![String::from("core:dwarf"), String::from("core:warrior")]; + hero.equipment + .insert(String::from("core:hand"), ItemInstance::new("core:iron_sword")); + (Game::new(hero), reg) + } + + /// Der Zielsatz von Meilenstein 1 als Behauptung: ein Wert, der aus einem + /// Modifikator stammt, der aus einer TOML-Datei kam. + #[test] + fn attribut_sammelt_aus_optionen_und_ausruestung() { + let (game, reg) = zwergenkrieger(); + let p = CreatureRef::Player; + + // Basis 1, Zwerg +2 (character_options.toml) + assert_eq!(game.attribute(p, Attribute::Constitution, None, ®), 3.0); + + // Basis 2, Eisenschwert +5 flach (items.toml), Krieger +10 % ⇒ (2+5)*1.1 + let damage = game.attribute(p, Attribute::Damage, None, ®); + assert!((damage - 7.7).abs() < 0.001, "erwartet 7.7, war {damage}"); + + // Unberuehrtes Attribut bleibt bei der Basis. + assert_eq!(game.attribute(p, Attribute::Charisma, None, ®), 1.0); + } + + /// Ticket 07 hat `Registries` **neben** `Game` gelegt, weil ein Feld bei + /// jedem Methodenaufruf mit `&mut self` kollidiert waere. Dass dieser Test + /// kompiliert, *ist* der Beweis — Ticket 12 konnte ihn nicht fuehren, weil + /// der Durchstich keine Registries lud. + #[test] + fn disjunkte_borrows_ueber_methodenaufrufe_hinweg() { + let (mut game, reg) = zwergenkrieger(); + + game.execute(Command::Move(Direction::North), ®); + let _ = game.attribute(CreatureRef::Player, Attribute::Damage, None, ®); + game.execute(Command::Wait, ®); + } + + /// Befund aus Ticket 12: „alle Commands kosten einen Zug" heisst **alle + /// ausgefuehrten**. + #[test] + fn wand_kostet_keinen_zug() { + let (mut game, reg) = zwergenkrieger(); + game.player_pos = (1, 1); + + assert!(matches!( + game.execute(Command::Move(Direction::North), ®), + Outcome::Rejected(_) + )); + assert_eq!(game.turn, 0, "eine Wand darf keine Zeit verbrauchen"); + + assert!(matches!(game.execute(Command::Wait, ®), Outcome::TurnPassed)); + assert_eq!(game.turn, 1); + } +} diff --git a/src/items.rs b/src/items.rs deleted file mode 100644 index 4b48381..0000000 --- a/src/items.rs +++ /dev/null @@ -1,82 +0,0 @@ -use serde::Deserialize; -use serde::de::DeserializeOwned; - -pub mod armor; -pub mod weapon; - -pub use armor::ItemArmor; -pub use weapon::ItemWeapon; - -// === ENUMS === -#[derive(Debug, Deserialize)] -pub enum ItemType { - Weapon, - OffHand, - Helmet, - Chestplate, - Gloves, - Boots, - Consumable, - Currency, -} - -// === STRUCTS === -#[derive(Deserialize)] -pub struct Item { - pub id: String, - pub name: String, - pub value: u32, - pub item_type: ItemType, -} - -/// Rohform einer Item-Datei. Jede TOML nutzt `[[items]]` als Wurzel, -/// dadurch bedient dieser eine Wrapper alle Dateien. -#[derive(Deserialize)] -struct ItemFile { - items: Vec, -} - -/// Laufzeit-Registry: einmal beim Start laden, danach als `&ItemCatalog` durchreichen. -// TODO: OffHand, Consumable und Currency haben noch keine Structs/TOMLs — hier ergaenzen. -pub struct ItemCatalog { - pub weapons: Vec, - pub armors: Vec, -} - -// === FUNCTIONS === -pub fn load_items() -> ItemCatalog { - ItemCatalog { - weapons: parse(include_str!("items/weapons.toml"), "weapons.toml"), - armors: parse(include_str!("items/armors.toml"), "armors.toml"), - } -} - -fn parse(raw: &str, file: &str) -> Vec { - let parsed: ItemFile = - toml::from_str(raw).unwrap_or_else(|e| panic!("{} ist kaputt:\n{}", file, e)); - parsed.items -} - -/// Rahmen um beliebig viele Zeilen. Wird von jedem `print_values` genutzt. -pub(crate) fn print_box(lines: &[String]) { - let longest: usize = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0); - let border: String = format!("+-{}-+", "-".repeat(longest)); - - println!("{}", border); - for line in lines { - println!("| {}{} |", line, " ".repeat(longest - line.chars().count())); - } - println!("{}", border); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn item_files_parse() { - let catalog: ItemCatalog = load_items(); - assert!(!catalog.weapons.is_empty()); - assert!(!catalog.armors.is_empty()); - } -} diff --git a/src/items/armor.rs b/src/items/armor.rs deleted file mode 100644 index 5a1c751..0000000 --- a/src/items/armor.rs +++ /dev/null @@ -1,22 +0,0 @@ -use serde::Deserialize; - -use super::{Item, print_box}; - -#[derive(Deserialize)] -pub struct ItemArmor { - pub armor: u32, - #[serde(flatten)] - pub item: Item, -} - -impl ItemArmor { - pub fn print_values(&self) { - print_box(&[ - format!("Stats for {} ", self.item.name), - format!("ID: {}", self.item.id), - format!("Slot: {:?}", self.item.item_type), - format!("Armor: {}", self.armor), - format!("Value: {}", self.item.value), - ]); - } -} diff --git a/src/items/armors.toml b/src/items/armors.toml deleted file mode 100644 index fe8bccd..0000000 --- a/src/items/armors.toml +++ /dev/null @@ -1,13 +0,0 @@ -[[items]] -id = "iron_helmet" -name = "Iron Helmet" -armor = 4 -value = 6 -item_type = "Helmet" - -[[items]] -id = "leather_boots" -name = "Leather Boots" -armor = 2 -value = 3 -item_type = "Boots" diff --git a/src/items/weapon.rs b/src/items/weapon.rs deleted file mode 100644 index 29c4e60..0000000 --- a/src/items/weapon.rs +++ /dev/null @@ -1,26 +0,0 @@ -use serde::Deserialize; - -use super::{Item, print_box}; - -#[derive(Deserialize)] -pub struct ItemWeapon { - pub base_damage: u32, - pub critical_factor: f32, - #[serde(flatten)] - pub item: Item, -} - -impl ItemWeapon { - pub fn print_values(&self) { - let crit_damage: u32 = (self.base_damage as f32 * self.critical_factor).ceil() as u32; - let crit_percent: i32 = ((self.critical_factor - 1.0) * 100.0).round() as i32; - - print_box(&[ - format!("Stats for {} ", self.item.name), - format!("ID: {}", self.item.id), - format!("Base Damage: {} ({})", self.base_damage, crit_damage), - format!("Critical Factor: {}%", crit_percent), - format!("Value: {}", self.item.value), - ]); - } -} diff --git a/src/items/weapons.toml b/src/items/weapons.toml deleted file mode 100644 index a936998..0000000 --- a/src/items/weapons.toml +++ /dev/null @@ -1,15 +0,0 @@ -[[items]] -id = "iron_sword" -name = "Iron Sword" -base_damage = 10 -critical_factor = 1.1 -value = 5 -item_type = "Weapon" - -[[items]] -id = "iron_dagger" -name = "Iron Dagger" -base_damage = 8 -critical_factor = 1.5 -value = 3 -item_type = "Weapon" diff --git a/src/main.rs b/src/main.rs index c34aa72..9422feb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,50 @@ -mod items; -mod player; -mod stats; +//! rustkeep — Meilenstein 1: vertikaler Durchstich. +//! +//! Ziel: Der Held laeuft in einem handgebauten Testraum herum, oeffnet den +//! Charakterbogen und sieht dort einen Wert, der aus einem Modifikator stammt, +//! der aus einer TOML-Datei kam. +//! +//! Damit stehen die Zusagen auf dem Pruefstand, die bisher nur auf Papier +//! standen: `Registries` neben `Game` (07), zweistufige Deserialisierung (04), +//! `Game::attribute` ueber mehrere Quellen (07/08), `game/` bleibt ratatui-frei +//! (11), Press-Filter unter Windows (06/12). -use items::*; -use player::*; +mod data; +mod game; +mod ui; + +use game::{Creature, ItemInstance}; fn main() { - let catalog: ItemCatalog = load_items(); + // Die `Result`-Kette endet **vor** `main` (Ticket 04): `main() -> Result<…>` + // druckt ueber `Debug` und setzt dem Spieler `LoadError { .. }` vor. + let (registries, warnings) = match data::load_mods() { + Ok(v) => v, + Err(e) => { + eprintln!("{e}"); + std::process::exit(1); + } + }; - for weapon in &catalog.weapons { - weapon.print_values(); - } - for armor in &catalog.armors { - armor.print_values(); - } + let log_name = data::write_log(&warnings); + let game = game::Game::new(starting_hero()); - let player: Player = Player::new("Reya".to_string()); - player.show_stats(); + if let Err(e) = ui::run(game, registries, warnings.len(), log_name) { + eprintln!("Terminal-Fehler: {e}"); + std::process::exit(1); + } +} + +/// Vorgefertigter Held, damit der Durchstich etwas zu zeigen hat. +/// +// TODO(M2): faellt weg, sobald der `CharacterCreation`-Screen aus Ticket 11 +// existiert — dort waehlt der Spieler die Optionen selbst, und +// `on_create` (Ticket 02) legt die Startausruestung an. +fn starting_hero() -> Creature { + let mut hero = Creature::new("Reya"); + hero.options = vec![String::from("core:dwarf"), String::from("core:warrior")]; + hero.equipment.insert(String::from("core:hand"), ItemInstance::new("core:iron_sword")); + hero.equipment.insert(String::from("core:head"), ItemInstance::new("core:leather_cap")); + hero.inventory.push(ItemInstance::new("core:health_potion")); + hero } diff --git a/src/player.rs b/src/player.rs deleted file mode 100644 index c6dc8f9..0000000 --- a/src/player.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::items::print_box; -use crate::stats::Stats; - -pub struct Player { - pub name: String, - pub stats: Stats, - pub next_level_exp: usize, - pub current_exp: usize, -} - -impl Player { - pub fn new(name: String) -> Self { - Self { - name, - stats: Stats::new(), - next_level_exp: 10, - current_exp: 0, - } - } - - pub fn show_stats(&self) { - let stats: &Stats = &self.stats; - - print_box(&[ - format!("Name: {}", self.name), - format!( - "Level: {} ({}/{})", - stats.level, self.current_exp, self.next_level_exp - ), - format!("Strength: {}", stats.strength), - format!("Dexterity: {}", stats.dexterity), - format!("Constitution: {}", stats.constitution), - format!("Charisma: {}", stats.charisma), - format!("Luck: {}", stats.luck), - ]); - } -} diff --git a/src/stats.rs b/src/stats.rs deleted file mode 100644 index bbc6be9..0000000 --- a/src/stats.rs +++ /dev/null @@ -1,68 +0,0 @@ -pub struct Stats { - pub max_health: f32, - pub current_health: f32, - pub max_stamina: f32, - pub current_stamina: f32, - pub strength: usize, // Determines base_damage modifier - pub dexterity: usize, // Determines Critical Factor Modifier - pub constitution: usize, // Determines Max Health Bonuses - pub charisma: usize, // Determines Shop Prices - pub luck: usize, // Determines Loot Drop & Critical Chance - pub level: usize, -} - -impl Stats { - pub fn new() -> Self { - let health: f32 = 100.0; - let stamina: f32 = 100.0; - Self { - max_health: health, - current_health: health, - max_stamina: stamina, - current_stamina: stamina, - strength: 1, - dexterity: 1, - constitution: 1, - charisma: 1, - luck: 1, - level: 1, - } - } - - /// Delta auf die aktuelle Health: positiv heilt, negativ verletzt. - pub fn change_health(&mut self, delta: f32) { - self.current_health = (self.current_health + delta).clamp(0.0, self.max_health); - } - - /// Delta auf die aktuelle Stamina: positiv regeneriert, negativ verbraucht. - // TODO: noch ungenutzt — wird vom Kampfsystem aufgerufen, sobald es existiert. - pub fn change_stamina(&mut self, delta: f32) { - self.current_stamina = (self.current_stamina + delta).clamp(0.0, self.max_stamina); - } - - pub fn is_dead(&self) -> bool { - self.current_health <= 0.0 - } - - // TODO: noch ungenutzt — wird vom Exp-/Level-System aufgerufen, sobald es existiert. - pub fn level_up(&mut self) { - self.level = (self.level + 1).clamp(1, 50); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn health_clamps_at_both_ends() { - let mut stats: Stats = Stats::new(); - - stats.change_health(50.0); - assert_eq!(stats.current_health, stats.max_health); - - stats.change_health(-500.0); - assert_eq!(stats.current_health, 0.0); - assert!(stats.is_dead()); - } -} diff --git a/src/ui/app.rs b/src/ui/app.rs new file mode 100644 index 0000000..d02a9ac --- /dev/null +++ b/src/ui/app.rs @@ -0,0 +1,205 @@ +//! Die Schleife. Blockierendes `event::read()`, kein Tick (Ticket 06). + +use ratatui::Frame; +use ratatui::crossterm::event::{self, Event, KeyEventKind}; +use ratatui::layout::{Constraint, Layout, Rect}; +use ratatui::style::{Color, Style, Stylize}; +use ratatui::text::Line; +use ratatui::widgets::{Block, Paragraph, Row, Table}; + +use super::grid::GridWidget; +use super::screen::{Response, Screen}; +use super::theme; +use crate::data::Registries; +use crate::game::{Attribute, CreatureRef, Game, Outcome}; + +pub struct App { + /// `game` und `registries` sind **getrennte Felder**, damit die disjunkten + /// Borrows aus Ticket 07 funktionieren. + pub game: Game, + pub registries: Registries, + screens: Vec, + warnings: usize, + log_name: Option, + rejected: Option<&'static str>, +} + +pub fn run(game: Game, registries: Registries, warnings: usize, log_name: Option) -> std::io::Result<()> { + let mut app = App { + game, + registries, + screens: vec![Screen::MainMenu], + warnings, + log_name, + rejected: None, + }; + + ratatui::run(|terminal| { + loop { + terminal.draw(|frame| draw(frame, &app))?; + + let Event::Key(key) = event::read()? else { continue }; + // Pflichtfilter unter Windows: Ticket 12 hat gemessen, dass jeder + // Tastendruck zwei Events erzeugt (Press und Release). + if key.kind != KeyEventKind::Press { + continue; + } + + let Some(screen) = app.screens.last_mut() else { break Ok(()) }; + + match screen.handle_key(key.code) { + Response::None => {} + Response::Pop => { + app.screens.pop(); + if app.screens.is_empty() { + break Ok(()); + } + } + Response::Push(s) => app.screens.push(s), + Response::Command(cmd) => { + // Hier steht die Zusage aus Ticket 07 auf dem Pruefstand: + // `&mut app.game` neben `&app.registries` in einem Aufruf. + app.rejected = match app.game.execute(cmd, &app.registries) { + Outcome::TurnPassed => None, + Outcome::Rejected(reason) => Some(reason), + }; + while app.screens.last().is_some_and(Screen::is_overlay) { + app.screens.pop(); + } + } + Response::Quit => break Ok(()), + } + } + }) +} + +fn draw(frame: &mut Frame, app: &App) { + let area = frame.area(); + let first = app.screens.iter().rposition(|s| !s.is_overlay()).unwrap_or(0); + + for screen in &app.screens[first..] { + match screen { + Screen::MainMenu => draw_main_menu(frame, area, app), + Screen::Explore => draw_explore(frame, area, app), + Screen::CharacterSheet => draw_sheet(frame, area, app), + } + } +} + +fn draw_main_menu(frame: &mut Frame, area: Rect, app: &App) { + let mut text = vec![ + Line::from("rustkeep".bold()), + Line::from(""), + Line::from("Enter Dungeon betreten"), + Line::from("q beenden"), + ]; + + // Eine Zeile, kein Screen — der Inhalt gehoert in einen Texteditor + // (Ticket 04). + if app.warnings > 0 { + let note = match &app.log_name { + Some(file) => format!("{} Ladewarnungen — logs/{file}", app.warnings), + None => format!("{} Ladewarnungen (Log nicht schreibbar)", app.warnings), + }; + text.push(Line::from("")); + text.push(Line::from(note).style(Style::new().fg(Color::Yellow))); + } + + frame.render_widget( + Paragraph::new(text).block(Block::bordered().title(" Hauptmenü ")), + area, + ); +} + +fn draw_explore(frame: &mut Frame, area: Rect, app: &App) { + let block = Block::bordered().title(format!(" Etage {} ", app.game.floor.number)); + let inner = block.inner(area); + frame.render_widget(block, area); + + let [grid, bar] = Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).areas(inner); + frame.render_widget( + GridWidget { floor: &app.game.floor, player: app.game.player_pos }, + grid, + ); + + let (note, color) = match app.rejected { + Some(reason) => (reason.to_string(), Color::Red), + None => (app.game.last.clone(), Color::Cyan), + }; + frame.render_widget( + Paragraph::new(format!( + "Zug {} │ {} │ c Charakterbogen │ {}", + app.game.turn, app.game.player.name, note + )) + .right_aligned() + .style(Style::new().fg(color)), + bar, + ); +} + +/// Der Charakterbogen ist eine **Schleife ueber Registries**, kein gemauertes +/// Layout — Modder koennen Slots und Wahlkategorien ergaenzen (Ticket 02). +fn draw_sheet(frame: &mut Frame, area: Rect, app: &App) { + let block = Block::bordered().title(" Charakterbogen "); + let inner = block.inner(area); + frame.render_widget(block, area); + + let [left, right] = Layout::horizontal([Constraint::Percentage(50); 2]).areas(inner); + + // Attribute: jeder Wert kommt aus Game::attribute, laeuft also durch alle + // Quellen — das ist der Punkt des ganzen Durchstichs. + let rows: Vec = Attribute::ALL + .iter() + .map(|a| { + let base = app.game.player.base.get(*a); + let total = + app.game + .attribute(CreatureRef::Player, *a, None, &app.registries); + let cell = if (total - base).abs() < f32::EPSILON { + format!("{total:.0}") + } else { + format!("{total:.0} ({base:.0} Basis)") + }; + Row::new(vec![a.name().to_string(), cell]) + }) + .collect(); + + frame.render_widget( + Table::new(rows, [Constraint::Length(20), Constraint::Min(0)]) + .block(Block::bordered().title(" Attribute ")), + left, + ); + + let mut lines = Vec::new(); + + lines.push(Line::from("Herkunft".bold())); + for id in &app.game.player.options { + let name = app.registries.options.get(id).map(|o| o.name.as_str()).unwrap_or(id); + lines.push(Line::from(format!(" {name}"))); + } + + lines.push(Line::from("")); + lines.push(Line::from("Ausrüstung".bold())); + for slot in app.registries.slots_ordered() { + let worn = app.game.player.equipment.get(&slot.id).and_then(|inst| { + app.registries.items.get(&inst.def).map(|i| (i.name.as_str(), i.color.as_deref())) + }); + match worn { + Some((name, color)) => lines.push(Line::from(vec![ + format!(" {:<10} ", slot.name).into(), + name.to_string().fg(theme::color(color)), + ])), + None => lines.push( + Line::from(format!(" {:<10} —", slot.name)).style(Style::new().fg(Color::DarkGray)), + ), + } + } + + lines.push(Line::from("")); + lines.push(Line::from("Esc zurück").style(Style::new().fg(Color::DarkGray))); + + frame.render_widget( + Paragraph::new(lines).block(Block::bordered().title(" Herkunft & Ausrüstung ")), + right, + ); +} diff --git a/src/ui/grid.rs b/src/ui/grid.rs new file mode 100644 index 0000000..9a03fd1 --- /dev/null +++ b/src/ui/grid.rs @@ -0,0 +1,44 @@ +//! Dungeon-Grid als eigenes Widget. Ticket 06 hat festgestellt, dass es weder +//! mitgeliefert noch als Drittanbieter-Crate existiert — also Eigenbau mit +//! direktem Buffer-Zugriff. Der Durchstich in Ticket 12 hat das bestaetigt. + +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::widgets::Widget; + +use crate::game::{Floor, Tile}; + +pub struct GridWidget<'a> { + pub floor: &'a Floor, + pub player: (u16, u16), +} + +impl Widget for GridWidget<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + for y in 0..self.floor.height { + for x in 0..self.floor.width { + let (px, py) = (area.x + x, area.y + y); + if px >= area.right() || py >= area.bottom() { + continue; + } + + let (symbol, color) = match self.floor.tile(x, y) { + Tile::Wall => ("█", Color::DarkGray), + Tile::Floor => ("·", Color::Rgb(60, 60, 70)), + Tile::Door => ("+", Color::Yellow), + Tile::Stairs => (">", Color::Green), + Tile::Entrance => ("<", Color::Blue), + }; + buf[(px, py)].set_symbol(symbol).set_style(Style::new().fg(color)); + } + } + + let (sx, sy) = (area.x + self.player.0, area.y + self.player.1); + if sx < area.right() && sy < area.bottom() { + buf[(sx, sy)] + .set_symbol("@") + .set_style(Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)); + } + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..8053d11 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,8 @@ +//! Alles, was ratatui kennen darf. + +mod app; +mod grid; +mod screen; +mod theme; + +pub use app::run; diff --git a/src/ui/screen.rs b/src/ui/screen.rs new file mode 100644 index 0000000..4b51b8b --- /dev/null +++ b/src/ui/screen.rs @@ -0,0 +1,73 @@ +//! Screen-Stack und Antwort — Ticket 11, im Durchstich (12) bestaetigt. + +use ratatui::crossterm::event::KeyCode; + +use crate::game::{Command, Direction}; + +/// Popups sind gewoehnliche Screens, die sich beim Rendern als Overlay +/// ausweisen — ein gemeinsamer `Vec` statt `current` plus +/// `Option`, weil Popup ueber Popup real ist. +pub enum Screen { + MainMenu, + Explore, + CharacterSheet, + // TODO(M2+): `CharacterCreation`, `Combat`, `Inventory`, `QuestLog`, + // `Shop`, `Dialog`, `Confirm`, `GameOver` — elf sind es am Ende + // (Ticket 11/14). +} + +/// Der Screen **gibt zurueck, statt selbst zu schieben**: sonst haelt er ein +/// `&mut` auf genau die `Vec`, in der er liegt. Nebeneffekt — `Response::Command` +/// ist der einzige Weg nach `game/`. +pub enum Response { + None, + Pop, + Push(Screen), + Command(Command), + Quit, +} + +impl Screen { + pub fn is_overlay(&self) -> bool { + // TODO(M2): `Confirm` ist das erste echte Overlay. Bis dahin haelt die + // Regel „ein Command schliesst alle Overlays" nur die Luft. + false + } + + pub fn handle_key(&mut self, key: KeyCode) -> Response { + match self { + Screen::MainMenu => match key { + KeyCode::Enter => Response::Push(Screen::Explore), + KeyCode::Char('q') | KeyCode::Esc => Response::Quit, + _ => Response::None, + }, + + Screen::Explore => match key { + // Pfeiltasten *und* hjkl — drei zusaetzliche Arme sind die + // billige Hoeflichkeit gegenueber Vim-Fingern (Ticket 11). + KeyCode::Up | KeyCode::Char('k') => { + Response::Command(Command::Move(Direction::North)) + } + KeyCode::Down | KeyCode::Char('j') => { + Response::Command(Command::Move(Direction::South)) + } + KeyCode::Left | KeyCode::Char('h') => { + Response::Command(Command::Move(Direction::West)) + } + KeyCode::Right | KeyCode::Char('l') => { + Response::Command(Command::Move(Direction::East)) + } + KeyCode::Char('.') => Response::Command(Command::Wait), + // Screen-Navigation ist reine UI und fasst `Game` nie an. + KeyCode::Char('c') => Response::Push(Screen::CharacterSheet), + KeyCode::Esc => Response::Pop, + _ => Response::None, + }, + + Screen::CharacterSheet => match key { + KeyCode::Esc | KeyCode::Char('c') | KeyCode::Char('q') => Response::Pop, + _ => Response::None, + }, + } + } +} diff --git a/src/ui/theme.rs b/src/ui/theme.rs new file mode 100644 index 0000000..0929bfb --- /dev/null +++ b/src/ui/theme.rs @@ -0,0 +1,26 @@ +//! Die einzige Stelle, an der aus einem Farb*namen* eine ratatui-Farbe wird. +//! +//! Ticket 11 hat `symbol` als Schluessel verworfen, weil `ui/theme.rs` **Code** +//! ist: Ein Modder, der `meinmod:frostbite` anlegt, koennte dort nichts +//! eintragen. Deshalb tragen die TOML-Daten Zeichen und Farbnamen selbst, und +//! hier steht nur die Uebersetzung. + +use ratatui::style::Color; + +pub fn color(name: Option<&str>) -> Color { + match name.unwrap_or_default() { + "black" => Color::Black, + "red" => Color::Red, + "green" => Color::Green, + "yellow" => Color::Yellow, + "blue" => Color::Blue, + "magenta" => Color::Magenta, + "cyan" => Color::Cyan, + "white" => Color::White, + "gray" | "grey" => Color::Gray, + "dark_gray" | "dark_grey" => Color::DarkGray, + // Unbekannter Name ist kein Fehler, sondern Standardfarbe — ein Tippfehler + // im Mod soll das Spiel nicht anhalten (Ticket 04). + _ => Color::Reset, + } +} diff --git a/tests/boundaries.rs b/tests/boundaries.rs new file mode 100644 index 0000000..43a8d48 --- /dev/null +++ b/tests/boundaries.rs @@ -0,0 +1,51 @@ +//! Ticket 11: `game/` kennt ratatui nicht — und zwar mechanisch geprueft. +//! +//! Ein Cargo-Workspace mit `game` als eigener Crate waere echte +//! Compiler-Durchsetzung und ist die Decke, falls es je ernst wird. Bis dahin +//! faengt dieser Test denselben Fehler und kostet keine Umstrukturierung. + +use std::fs; +use std::path::Path; + +fn rust_files(dir: &Path, out: &mut Vec) { + for entry in fs::read_dir(dir).expect("Verzeichnis lesbar").flatten() { + let path = entry.path(); + if path.is_dir() { + rust_files(&path, out); + } else if path.extension().is_some_and(|e| e == "rs") { + out.push(path); + } + } +} + +#[test] +fn game_bleibt_ratatui_frei() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + + for dir in ["src/game", "src/data"] { + let mut files = Vec::new(); + rust_files(&root.join(dir), &mut files); + assert!(!files.is_empty(), "{dir} sollte Dateien enthalten"); + + for file in files { + let source = fs::read_to_string(&file).expect("Datei lesbar"); + + // Kommentare ausnehmen. Ticket 11 hat „liest die Dateien und prueft + // auf ratatui" gesagt — die woertliche Fassung schlaegt an, sobald + // ein Doc-Kommentar erklaert, dass die Grenze existiert. + let offender = source + .lines() + .enumerate() + .find(|(_, l)| !l.trim_start().starts_with("//") && l.contains("ratatui")); + + if let Some((n, line)) = offender { + panic!( + "{}:{}: „{}\" — die Grenze aus Ticket 11 ist gefallen", + file.display(), + n + 1, + line.trim() + ); + } + } + } +}