diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1d9350 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +prototype/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bb1f0bc --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3443 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[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 = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" + +[[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 = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "compact_str" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "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 = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[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 0.61.2", +] + +[[package]] +name = "everjoy" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "crossterm", + "directories", + "futures", + "glob", + "ignore", + "indexmap", + "notify", + "parking_lot", + "pulldown-cmark", + "ratatui", + "regex", + "reqwest", + "ropey", + "rusqlite", + "schemars", + "serde", + "serde_json", + "syntect", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-tungstenite", + "toml", + "tui-term", + "tui-textarea", + "uuid", + "walkdir", + "which", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "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 = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.0", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inotify" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533e68a5842e734946fe159fb03fc9bbbb254f590dd0d8ad321ae5ff7beca2c1" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "onig" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[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 = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ropey" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" +dependencies = [ + "smallvec", + "str_indices", +] + +[[package]] +name = "rusqlite" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143" +dependencies = [ + "bitflags", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[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 = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.18", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[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", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tui-term" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72af159125ce32b02ceaced6cffae6394b0e6b6dfd4dc164a6c59a2db9b3c0b0" +dependencies = [ + "ratatui", + "vt100", +] + +[[package]] +name = "tui-textarea" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" +dependencies = [ + "crossterm", + "ratatui", + "unicode-width 0.2.0", +] + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[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 = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vt100" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de" +dependencies = [ + "itoa", + "log", + "unicode-width 0.1.14", + "vte", +] + +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[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.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix 1.1.4", + "winsafe", +] + +[[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-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[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 = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..28766fd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "everjoy" +version = "0.1.0" +edition = "2021" + +[dependencies] +# TUI rendering +ratatui = "0.29" +crossterm = "0.28" +tui-textarea = "0.7" +tui-term = "0.2" + +# Async + network +tokio = { version = "1.4", features = ["full"] } +reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls"] } +tokio-tungstenite = "0.26" +futures = "0.3" + +# Serialization + Agent protocol +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +schemars = "0.8" +async-trait = "0.1" + +# Data persistence +rusqlite = { version = "0.34", features = ["bundled", "chrono"] } +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1.16", features = ["v4", "serde"] } + +# Text + files +ropey = "1.6" +pulldown-cmark = "0.13" +syntect = "5.2" +notify = "8.0" +ignore = "0.4" +walkdir = "2.5" + +# Config + utilities +toml = "0.8" +directories = "5.0" +regex = "1.11" +which = "7.0" +tempfile = "3.19" +glob = "0.3" + +# Infrastructure +anyhow = "1.0" +thiserror = "2.0" +parking_lot = "0.12" +indexmap = "2.7" + +[dev-dependencies] diff --git a/NOTES.MD b/NOTES.MD new file mode 100644 index 0000000..3957277 --- /dev/null +++ b/NOTES.MD @@ -0,0 +1 @@ +i18n support \ No newline at end of file diff --git a/TECH_STACK.md b/TECH_STACK.md new file mode 100644 index 0000000..241a1b5 --- /dev/null +++ b/TECH_STACK.md @@ -0,0 +1,361 @@ +# Everjoy Tech Stack + +> **Everjoy is not only an IDE but a completely fresh AI workflow.** +> +> An ideal agent tool with a TUI interacting interface, divided into several workspaces: +> file editing, vibe interacting, and project/life management. + +--- + +## 1. TUI 渲染与交互框架 + +| 库 | 用途 | +|----|------| +| **ratatui** | TUI 渲染核心,多工作区布局、面板分割、状态管理 | +| **crossterm** | 跨平台输入输出(键鼠事件、颜色、光标、屏幕缓冲) | +| **tui-textarea** | 编辑器工作区的文本编辑组件 | +| **tui-term** | 内置终端面板(Agent 执行命令时展示实时输出) | + +### 为什么选 ratatui + +- 工作区(Workspace)需要复杂布局:侧边栏 + 编辑区 + AI 对话面板 + 状态栏 +- 支持鼠标点击切换面板、拖拽分割线 +- 活跃的组件生态,适合快速组装多面板界面 + +```bash +cargo add ratatui crossterm tui-textarea tui-term +``` + +--- + +## 2. 异步运行时与网络通信 + +| 库 | 用途 | +|----|------| +| **tokio** | 异步运行时,同时处理 UI 事件、AI 流式响应、文件监听 | +| **reqwest** | HTTP 客户端,调用 LLM API(OpenAI、Claude、本地 Ollama 等) | +| **tokio-tungstenite** | WebSocket 客户端(实时连接、本地服务通信) | +| **futures** | 流处理工具 | + +### AI 流式响应处理 + +```bash +cargo add tokio --features full +cargo add reqwest --features json,stream,rustls-tls +cargo add tokio-tungstenite +cargo add futures +``` + +--- + +## 3. Agent 核心引擎 + +这是 Everjoy 的灵魂。不依赖现有框架,自研一个轻量但可扩展的 Agent 循环。 + +### 核心架构 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Observer │ --> │ Thinker │ --> │ Actor │ +│ (观察环境) │ │ (推理决策) │ │ (执行动作) │ +└─────────────┘ └─────────────┘ └─────────────┘ + ^ | + └────────────── Feedback <─────────────┘ +``` + +### Agent 相关依赖 + +| 库 | 用途 | +|----|------| +| **serde** + **serde_json** | LLM API 请求/响应的结构化序列化 | +| **schemars** | 为工具函数生成 JSON Schema,供 LLM function calling 使用 | +| **async-trait** | Agent 组件的异步 trait 抽象 | + +```bash +cargo add serde --features derive +cargo add serde_json +cargo add schemars +cargo add async-trait +``` + +### 工具系统(Tools) + +Agent 需要能操作环境的工具集: + +| 工具 | 实现方式 | +|------|---------| +| 文件读写 | `std::fs` + `ropey`(大文件) | +| 命令执行 | `tokio::process::Command` + `tui-term` 展示输出 | +| 代码搜索 | `regex` + `ignore`(支持 .gitignore) | +| 网络请求 | `reqwest`(Agent 自主查询文档/API) | +| 任务管理 | 调用 SQLite 存储层(见第 5 节) | + +```bash +cargo add regex ignore +``` + +--- + +## 4. 记忆系统(Memory) + +Agent 不能金鱼脑。需要短期对话记忆 + 长期知识记忆。 + +### 短期记忆:对话上下文 + +直接维护 `Vec`,按 token 限制滑动窗口截断。 + +### 长期记忆:向量检索 + +| 库 | 用途 | +|----|------| +| **rusqlite** | 主数据库(对话历史、任务、配置、笔记) | +| **sqlite-vec** 或 **pgvector 本地版** | 向量存储与相似度检索 | +| **fastembed-rs** | 本地文本 Embedding(不依赖外部 API) | + +> 如果追求极简,可以先只用 SQLite 全文搜索(`fts5`),后期再上向量。 + +```bash +cargo add rusqlite --features bundled +# 向量检索后续按需添加 +``` + +--- + +## 5. 数据持久化层 + +Everjoy 管理项目和人生,需要稳定的数据层。 + +| 库 | 用途 | +|----|------| +| **rusqlite** | 嵌入式数据库,存储一切 | +| **chrono** | 时间处理、截止日期、日历事件 | +| **uuid** | 任务/对话/项目的唯一标识 | + +### 数据库表设计(初版) + +```sql +-- 对话历史 +CREATE TABLE conversations ( + id TEXT PRIMARY KEY, + title TEXT, + created_at DATETIME, + updated_at DATETIME +); + +CREATE TABLE messages ( + id TEXT PRIMARY KEY, + conversation_id TEXT, + role TEXT, -- 'user' | 'assistant' | 'system' | 'tool' + content TEXT, + tool_calls TEXT, -- JSON + created_at DATETIME +); + +-- 任务/项目管理 +CREATE TABLE tasks ( + id TEXT PRIMARY KEY, + title TEXT, + description TEXT, + status TEXT, -- 'todo' | 'doing' | 'done' | 'archived' + priority INTEGER, + due_date DATETIME, + project TEXT, + tags TEXT, -- JSON array + created_at DATETIME +); + +-- 笔记/知识 +CREATE TABLE notes ( + id TEXT PRIMARY KEY, + title TEXT, + content TEXT, + tags TEXT, + linked_file TEXT, + created_at DATETIME +); +``` + +```bash +cargo add chrono --features serde +cargo add uuid --features v4,serde +``` + +--- + +## 6. 内容渲染与解析 + +| 库 | 用途 | +|----|------| +| **pulldown-cmark** | 解析 AI 回复的 Markdown,渲染到 TUI 面板 | +| **syntect** | 代码块语法高亮(Markdown 中的 ```rust 块) | +| **syntect-tui** | syntect 与 ratatui 的桥接 | + +> AI 回复基本是 Markdown,需要解析成 ratatui 的 `Text`/`Span` 结构渲染。 + +```bash +cargo add pulldown-cmark +cargo add syntect syntect-tui +``` + +--- + +## 7. 文件系统与项目感知 + +| 库 | 用途 | +|----|------| +| **ropey** | 大文件高效编辑(编辑工作区核心) | +| **notify** | 监听文件变化,Agent 实时感知项目改动 | +| **ignore** | 按 .gitignore 规则遍历文件树 | +| **walkdir** | 目录遍历 | + +```bash +cargo add ropey notify ignore walkdir +``` + +--- + +## 8. 配置管理 + +| 库 | 用途 | +|----|------| +| **toml** | 用户配置(AI API key、主题、快捷键、Agent 人设) | +| **serde** | 配置反序列化 | +| **directories** | 跨平台定位配置目录 `~/.config/everjoy/` | + +### 配置文件结构 + +```toml +# ~/.config/everjoy/config.toml +[ai] +provider = "openai" +api_key = "sk-..." +model = "gpt-4o" +stream = true + +[agent] +personality = "你是一位高效且略带幽默的编程伙伴..." +auto_execute_tools = false -- 危险操作需确认 + +[ui] +theme = "everforest" +sidebar_width = 30 +editor_tab_size = 4 + +[keymap] +"ctrl+space" = "toggle_agent_panel" +"ctrl+shift+a" = "ask_agent" +``` + +```bash +cargo add toml serde --features derive directories +``` + +--- + +## 9. 实用工具库 + +| 库 | 用途 | +|----|------| +| **anyhow** | 快速原型期的错误处理 | +| **thiserror** | 后期结构化错误定义 | +| **parking_lot** | 更轻量的同步原语 | +| **indexmap** | 有序 HashMap(保持面板顺序、配置项顺序) | +| **which** | 查找系统命令路径(检查 git、node 等是否存在) | +| **tempfile** | 临时文件(Agent 编辑前备份) | +| **glob** | 文件通配匹配 | + +```bash +cargo add anyhow thiserror parking_lot indexmap which tempfile glob +``` + +--- + +## 10. 完整 Cargo.toml + +```toml +[package] +name = "everjoy" +version = "0.1.0" +edition = "2024" + +[dependencies] +# TUI 渲染 +ratatui = "0.29" +crossterm = "0.28" +tui-textarea = "0.7" +tui-term = "0.2" + +# 异步 + 网络 +tokio = { version = "1.4", features = ["full"] } +reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls"] } +tokio-tungstenite = "0.26" +futures = "0.3" + +# 序列化 + Agent 协议 +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +schemars = "0.8" +async-trait = "0.1" + +# 数据持久化 +rusqlite = { version = "0.34", features = ["bundled", "chrono"] } +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1.16", features = ["v4", "serde"] } + +# 文本 + 文件 +ropey = "1.6" +pulldown-cmark = "0.13" +syntect = "5.2" +# syntect-tui = "..." # 视情况选择或自研桥接 +notify = "8.0" +ignore = "0.4" +walkdir = "2.5" + +# 配置 + 工具 +toml = "0.8" +directories = "5.0" +regex = "1.11" +which = "7.0" +tempfile = "3.19" +glob = "0.3" + +# 基础设施 +anyhow = "1.0" +thiserror = "2.0" +parking_lot = "0.12" +indexmap = "2.7" + +[dev-dependencies] +# 测试工具后续添加 +``` + +--- + +## 12. 关键设计决策 + +| 维度 | 决策 | 理由 | +|------|------|------| +| **编辑模式** | **非模态**(直接打字) | 开箱即用,AI 随时可介入 | +| **Agent 架构** | **自研 ReAct 循环** | 现有框架太重,自研更灵活 | +| **LLM 接入** | **多 Provider 抽象** | 支持 OpenAI/Claude/Ollama/本地模型 | +| **流式响应** | **SSE 实时渲染** | AI 思考过程实时展示在对话面板 | +| **数据存储** | **SQLite 单文件** | 用户项目 + 人生数据一盘棋 | +| **工具执行** | **人工确认 + 自动执行分级** | 读文件自动,写文件/执行命令需确认 | +| **文件编辑** | **临时缓冲 + 用户确认写入** | Agent 的修改先预览,用户 OK 再落盘 | + +--- + +## 13. 开发里程碑(建议) + +| 阶段 | 目标 | 核心组件 | +|------|------|---------| +| **MVP** | TUI 多面板 + 基本编辑器 + 能聊天的 Agent | ratatui, tui-textarea, reqwest, 简单 ReAct | +| **v0.2** | 文件工具 + 项目内代码问答 | ropey, ignore, notify, 文件读写工具 | +| **v0.3** | 任务/笔记管理 + SQLite 持久化 | rusqlite, chrono, 任务面板 | +| **v0.4** | 内嵌终端 + Agent 自主执行命令 | tui-term, tokio-process, 命令工具 | +| **v0.5** | 记忆系统 + 跨对话上下文 | sqlite-vec, embedding, 长期记忆检索 | + +--- + +*for Hoshu Chiu & Everjoy Chiu 🐕* +*Just have fun with Everjoy.* diff --git a/doc/Architecture-Runtime-and-Plugin.md b/doc/Architecture-Runtime-and-Plugin.md new file mode 100644 index 0000000..f7024c2 --- /dev/null +++ b/doc/Architecture-Runtime-and-Plugin.md @@ -0,0 +1,218 @@ +# Everjoy Architecture: Runtime Subsystem + Plugin Subsystem + +## Design Principle + +**Separate concerns:** +- **Runtime Subsystem** = the engine. Defines interfaces, manages lifecycle, routes events, schedules rendering. Everything (built-in + external) depends on it. +- **Plugin Subsystem** = the adapter. Loads external code, translates C-ABI ↔ Rust trait. Only external modules go through it. + +``` +┌─────────────────────────────────────────┐ +│ Runtime Subsystem │ +│ (supporter for everything) │ +├─────────────────────────────────────────┤ +│ Module Registry │ +│ - PanelProvider registry │ +│ - MenuItem registry │ +│ - StatusWidget registry │ +│ - Tool registry │ +├─────────────────────────────────────────┤ +│ Lifecycle Manager │ +│ - init → render → event → hide → │ +│ show → shutdown │ +├─────────────────────────────────────────┤ +│ Event Router │ +│ - Layer 1: System shortcuts │ +│ - Layer 2: Focused PanelProvider │ +│ - Layer 3: Global shortcuts / Menubar │ +├─────────────────────────────────────────┤ +│ Render Scheduler │ +│ - Calls Provider::render() each frame │ +│ - Blits cell grid → Buffer for │ +│ external providers │ +└─────────────────────────────────────────┘ + ↑ ↓ traits +┌─────────────────────────────────────────┐ +│ Plugin Subsystem │ +│ (loader + translator) │ +├─────────────────────────────────────────┤ +│ Extension Loader │ +│ - Scan plugin directory │ +│ - Load .wasm / .so / static │ +│ - Symbol resolution │ +├─────────────────────────────────────────┤ +│ Translation Layer │ +│ - C-ABI ↔ Rust trait │ +│ - EjSurface memory management │ +│ - Cell grid → Buffer blitting │ +│ - Style resolution │ +│ - Panic isolation │ +└─────────────────────────────────────────┘ +``` + +--- + +## Runtime Subsystem + +Defines the **trait interfaces** that all modules must satisfy. Built-in modules implement these directly. External modules satisfy them via the Plugin Subsystem. + +### Core Traits + +```rust +/// Anything that can render into a panel +pub trait PanelProvider: Send { + fn init(&mut self, ctx: &mut dyn ProviderCtx); + fn render(&mut self, area: Rect, buf: &mut Buffer); + fn handle_event(&mut self, event: Event) -> EventResult; + fn focus_changed(&mut self, focused: bool); + fn hide(&mut self); + fn show(&mut self); + fn shutdown(&mut self); + fn can_open(&self, context: &OpenContext) -> bool; + fn keep_alive_on_hide(&self) -> bool { false } +} + +/// Callable by Agents +pub trait Tool: Send { + fn name(&self) -> &str; + fn description(&self) -> &str; + fn schema(&self) -> ToolSchema; // JSON Schema for LLM function calling + fn execute(&self, input: ToolInput) -> ToolOutput; +} + +/// Menubar entry +pub trait MenuItem: Send { + fn id(&self) -> &str; + fn label(&self) -> &str; + fn parent(&self) -> Option<&str>; + fn shortcut(&self) -> Option<&str>; + fn invoke(&self); +} + +/// Status bar widget +pub trait StatusWidget: Send { + fn id(&self) -> &str; + fn position(&self) -> StatusPosition; + fn render(&self, area: Rect, buf: &mut Buffer); +} +``` + +### Module Registry + +```rust +pub struct ModuleRegistry { + panel_providers: HashMap>, + menu_items: Vec>, + status_widgets: Vec>, + tools: HashMap>, +} +``` + +Built-in modules register directly: + +```rust +// In everjoy/src/main.rs or a built-in module initializer +runtime.register_panel_provider("editor", Box::new(EditorProvider::new())); +runtime.register_panel_provider("terminal", Box::new(TerminalProvider::new())); +runtime.register_panel_provider("tasktree", Box::new(TaskTreeProvider::new())); +runtime.register_tool("read_file", Box::new(ReadFileTool)); +runtime.register_menu_item(Box::new(QuitMenuItem)); +``` + +### Lifecycle (Runtime-managed) + +``` +Load (built-in = static init, external = via Plugin Subsystem) + ↓ +bootstrap() — register all contributions + ↓ +Running — render / event / command loop + ↓ +Unload — shutdown() all providers, unregister all contributions +``` + +--- + +## Plugin Subsystem + +Only responsible for **external modules**. Built-in modules never touch it. + +### Extension Loader + +```rust +pub struct ExtensionLoader { + plugin_dir: PathBuf, +} + +impl ExtensionLoader { + pub fn scan_and_load(&self, runtime: &mut Runtime) -> Result<()> { + for entry in fs::read_dir(&self.plugin_dir)? { + let path = entry?.path(); + let plugin = self.load(&path)?; + plugin.bootstrap(runtime.registrar())?; + } + Ok(()) + } + + fn load(&self, path: &Path) -> Result> { + match path.extension() { + Some("wasm") => self.load_wasm(path), + Some("so") | Some("dll") => self.load_dylib(path), + _ => Err("Unknown plugin format"), + } + } +} +``` + +### Translation Layer + +For external plugins (especially WASM), the Plugin Subsystem provides a translation layer: + +```rust +/// Wraps a C-ABI / WASM plugin into Runtime-compatible traits +pub struct TranslatedProvider { + wasm_instance: wasmtime::Instance, + // ... cached function handles +} + +impl PanelProvider for TranslatedProvider { + fn render(&mut self, area: Rect, buf: &mut Buffer) { + // 1. Allocate EjSurface + let mut surface = EjSurface::new(area.width, area.height); + // 2. Call plugin's C render function + self.call_render(&mut surface); + // 3. Blit cell grid into ratatui Buffer + self.blit(&surface, area, buf); + } + // ... other methods translated similarly +} +``` + +### Key Rule + +> **Built-in modules implement Runtime traits directly.** +> **External modules are wrapped by Plugin Subsystem to satisfy Runtime traits.** + +```rust +// Built-in: direct trait implementation +runtime.register_panel_provider("editor", Box::new(EditorProvider::new())); + +// External: loaded via Plugin Subsystem, wrapped to satisfy trait +let plugin = plugin_subsystem.load("image-viewer.wasm")?; +let provider = plugin.create_provider("image-viewer.main")?; +runtime.register_panel_provider("image-viewer", Box::new(provider)); +// ^^^^ provider here is a TranslatedProvider +// that implements PanelProvider +``` + +--- + +## Summary + +| Concern | Runtime Subsystem | Plugin Subsystem | +|---------|-------------------|------------------| +| **Defines** | Trait interfaces | Loading + translation mechanism | +| **Used by** | Everyone (built-in + external) | Only external modules | +| **Manages** | Registry, lifecycle, events, render | File I/O, WASM runtime, FFI | +| **Depends on** | ratatui, crossterm, tokio | wasmtime / libloading, Runtime traits | +| **Can be omitted?** | No — without it, no app | Yes — without it, only built-in modules work | diff --git a/doc/Architecture.md b/doc/Architecture.md new file mode 100644 index 0000000..ed8f45b --- /dev/null +++ b/doc/Architecture.md @@ -0,0 +1,414 @@ +# Everjoy Architecture + +## Overview + +Everjoy is a TUI IDE with an AI-native workflow. The architecture separates concerns into four subsystems: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ UI Framework │ +│ - Layout, focus, render scheduling │ +│ - Menubar, Status Bar, Splash Screen │ +│ - Panel management (Sidebar | Main | Agent) │ +├─────────────────────────────────────────────────────────────┤ +│ Runtime Subsystem │ +│ - Trait definitions (Provider, Tool, Plugin) │ +│ - Module Registry (providers, tools, menu, status) │ +│ - Lifecycle management │ +│ - Command dispatch (Provider → App) │ +├─────────────────────────────────────────────────────────────┤ +│ Agent Framework │ +│ - Multi-agent orchestration │ +│ - Role system (Secretary, Driver, Executor...) │ +│ - Tool invocation via LLM function calling │ +│ - Memory & context management │ +├─────────────────────────────────────────────────────────────┤ +│ Plugin Subsystem │ +│ - Extension loader (.wasm / .so / static) │ +│ - C-ABI ↔ Rust trait translation │ +│ - Optional — built-in modules don't use it │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 1. Runtime Subsystem + +The foundation. Defines what modules can do and manages their lifecycle. + +### 1.1 Core Traits + +```rust +/// A plugin registers its contributions during bootstrap +pub trait Plugin: Send { + fn bootstrap(registrar: &mut dyn Registrar); +} + +/// Where content comes from (file, URL, memory) +pub enum Source { + File { path: PathBuf }, + Url { url: String }, + Memory { name: String, data: Vec }, +} + +/// Actions a Provider can request from the system +pub enum Command { + /// Present content in a panel. System bids providers on that panel. + Present { panel: PanelType, source: Source, mime_hint: Option }, + + /// Launch a tool/session by id. Direct dispatch, no bidding. + Launch { panel: PanelType, tool_id: String, params: serde_json::Value }, + + FocusPanel(PanelType), + ShowMessage { msg: String, level: MessageLevel }, +} + +/// Renders content into a panel +pub trait PanelProvider: Send { + fn id(&self) -> &str; + fn init(&mut self); + fn render(&mut self, area: Rect, buf: &mut Buffer); + /// Returns (handled?, commands to dispatch) + fn handle_event(&mut self, event: Event) -> (EventResult, Vec); + fn focus_changed(&mut self, focused: bool); + fn hide(&mut self); + fn show(&mut self); + fn shutdown(&mut self); + /// Score how well this provider handles the source. 0 = cannot handle. + fn can_open(&self, source: &Source, mime_hint: Option<&str>) -> i32; + fn keep_alive_on_hide(&self) -> bool { false } +} + +/// Callable by Agents via LLM function calling +pub trait Tool: Send { + fn name(&self) -> &str; + fn description(&self) -> &str; + fn schema(&self) -> ToolSchema; + fn execute(&self, input: ToolInput) -> ToolOutput; +} +``` + +### 1.2 Registrar + +```rust +pub trait Registrar { + fn register_provider(&mut self, desc: ProviderDesc, factory: ProviderFactory); + fn register_tool(&mut self, desc: ToolDesc, handler: Box); + fn register_menu_item(&mut self, desc: MenuItemDesc, handler: Box); + fn register_status_widget(&mut self, desc: StatusWidgetDesc, render: Box); +} +``` + +### 1.3 Command Pattern (replaces ProviderCtx) + +**Removed:** `ProviderCtx` trait. Providers no longer call into the system via a context object. + +**New:** Providers return `Vec` from `handle_event()`. The App (main.rs) is the sole dispatcher. + +Why: `ProviderCtx::open_file()` was an implicit black-box router — the caller didn't know which provider would handle the file, and adding new providers required modifying core code. The Command pattern makes every intent explicit and traceable. + +```rust +// Before: implicit, hard-coded routing +fn handle_event(&mut self, ev: Event) -> EventResult { + self.ctx.open_file("main.rs"); // who handles this? nobody knows + Consumed +} + +// After: explicit intent, App decides +fn handle_event(&mut self, ev: Event) -> (EventResult, Vec) { + let cmd = Command::Present { + panel: PanelType::Main, + source: Source::File { path: "main.rs".into() }, + mime_hint: None, + }; + (Consumed, vec![cmd]) +} +``` + +### 1.4 Module Registry + +```rust +pub struct ModuleRegistry { + pub panel_providers: HashMap, + pub tools: HashMap>, + pub menu_items: Vec>, + pub status_widgets: Vec>, +} + +pub struct RegisteredProvider { + pub desc: ProviderDesc, + pub factory: ProviderFactory, + pub instance: Option>, +} +``` + +Key methods: +- `providers_for_panel(panel) -> Vec<&RegisteredProvider>` — filter by panel type +- `ensure_instance(id) -> Option<&mut Box>` — lazy instantiate +- `find_best_provider(panel, source, mime_hint) -> Option<(String, i32)>` — bid scoring +- `shutdown_all()` — graceful teardown + +### 1.5 Lifecycle + +``` +Load + ├─ Built-in: registered into ModuleRegistry at App startup + └─ External: via Plugin Subsystem + ↓ +Bootstrap (Plugin::bootstrap) + ├─ Register providers (with panel type + capabilities) + ├─ Register tools + ├─ Register menu items + └─ Register status widgets + ↓ +Running + ├─ Render loop: App asks registry for active provider per panel, calls render() + ├─ Event loop: App routes to focused provider, receives Commands, dispatches + ├─ Tool calls: Agent Framework → Tool::execute() + └─ Present/Launch: App bids/routes to the best provider + ↓ +Unload (atomic) + ├─ Provider::shutdown() for all active providers + ├─ Unregister all contributions + └─ Plugin::shutdown() +``` + +--- + +## 2. Plugin Subsystem + +Optional subsystem for external modules. Built-in modules implement Runtime traits directly. + +### 2.1 Architecture + +``` +External Plugin (.wasm / .so) + ↓ +Plugin Subsystem + ├─ Extension Loader: file I/O, symbol resolution + └─ Translation Layer + ├─ C-ABI ↔ Rust trait + ├─ EjSurface memory management + ├─ Cell grid → Buffer blitting + └─ Panic isolation + ↓ +Runtime Subsystem (receives Box, Box) +``` + +### 2.2 Key Rule + +> Built-in modules implement Runtime traits directly. +> External modules are wrapped by Plugin Subsystem to satisfy Runtime traits. + +--- + +## 3. UI Framework + +Manages what the user sees and how they interact. + +### 3.1 Layout + +``` +┌─────────────────────────────────────────────────────────────┐ +│ _F_ile _E_dit _V_iew _A_gent _H_elp │ Menubar +├──────────┬──────────────────────────────┬───────────────────┤ +│ 📁 src │ 📁 src > main.rs │ 🤖 Agent Chat │ +│ 📄 Cargo │ [File Editor] │ Claude GPT-4 Local│ +│ 📄 doc │ │ ───────────────── │ +│ 📁 targ │ fn main() { │ You: ... │ +│ │ let x = 42; │ Claude: ... │ +│ │ } │ │ +│ │ │ Quick Links: │ +│ │ │ [agents.md] ... │ +│ │ │ │ +├──────────┴──────────────────────────────┴───────────────────┤ +│ main.rs | Rust | UTF-8 | Ln 3, Col 10 | [Main] │ Status Bar +└─────────────────────────────────────────────────────────────┘ +``` + +- **Sidebar** — 20% width, FileTree (navigate files, Enter to open in Main) +- **Main Panel** — 52% width, dynamic content (Editor, Terminal, etc.) +- **Agent Panel** — 28% width, chat + quick links +- **Menubar** — 1 row, underlined capitals for Alt+ shortcuts +- **Status Bar** — 1 row, file info + focused panel + +### 3.2 Focus Management + +| Input | Action | +|-------|--------| +| `Alt` | Focus Menubar | +| `Ctrl+J` | Focus Sidebar | +| `Ctrl+K` | Focus Main Panel | +| `Ctrl+L` | Focus Agent Panel | +| Mouse click | Focus clicked panel directly | + +### 3.3 Panel State + +- **Main Panel** — dynamic provider, single content area +- **Provider switching** — `hide()` old, `show()` new, old may `keep_alive_on_hide()` +- **Navigation** — stack-based history, no tab bar (too heavy for TUI) +- **Bidding** — when `Command::Present` arrives, all providers on the target panel score themselves via `can_open()`. Highest score wins. + +### 3.4 Event Routing + +``` +Layer 1: System shortcuts (Ctrl+Q quit) — intercepted by App +Layer 2: Menubar — if active +Layer 3: Focus shortcuts (Ctrl+J/K/L) — intercepted by App +Layer 4: Focused PanelProvider → returns (EventResult, Vec) +Layer 5: App dispatches returned Commands +``` + +### 3.5 Render Order + +Per frame: +1. Clear +2. Menubar +3. Panel backgrounds (uniform dark) +4. Sidebar Provider::render() +5. Main Panel Provider::render() +6. Agent Panel Provider::render() +7. Status Bar + +--- + +## 4. Agent Framework + +### 4.1 Role System + +Managed by core system, not plugins. + +| Role | Icon | Purpose | Trigger | +|------|------|---------|---------| +| Secretary | 👤 | User conversation | Always | +| Driver | 🧭 | Exploration, curiosity | Auto | +| NegFeedback | 🛡️ | Error detection | Auto | +| Executor | ⚡ | Command execution | Auto | + +### 4.2 Tool Invocation + +Agent → LLM function calling → Tool Registry → Tool::execute() → result → Agent + +Tools are registered by plugins. The Agent Framework queries the Tool Registry for available tools and generates JSON Schema for the LLM. + +--- + +## 5. Data Flows + +### 5.1 User Opens a File + +``` +User navigates to main.rs in Sidebar (FileTree) + → FileTreeProvider::handle_event(Enter) + → Returns Command::Present { + panel: Main, + source: Source::File { path: "src/main.rs" } + } + → App::dispatch_command() + → App asks registry: find_best_provider(Main, File("main.rs"), "text/rust") + → EditorProvider::can_open(File, "text/rust") = 100 + → TerminalProvider::can_open(File, ...) = 0 + → Editor wins + → App hides old Main provider, shows Editor + → EditorProvider.render() +``` + +### 5.2 Agent Opens a File + +``` +Agent decides to open src/main.rs + → LLM function calling: "open_file" tool + → Tool Registry finds "open_file" + → OpenFileTool.execute(path: "src/main.rs") + → Read file from disk + → Returns Command::Present { panel: Main, source: File(...) } + → App dispatches → routes to EditorProvider + → Tool returns "File opened" to Agent +``` + +### 5.3 Provider Renders a Frame + +``` +UI Framework render loop + → Determine panel areas from layout + → Look up active provider id per panel from App.active_providers + → Ask registry for instance + → Sidebar area → FileTreeProvider::render(area, buf) + → Main area → EditorProvider::render(area, buf) (or whoever is active) + → Agent area → AgentChatProvider::render(area, buf) + → Draw Menubar and Status Bar +``` + +--- + +## 6. Directory Structure + +``` + everjoy/ + ├── src/ + │ ├── main.rs # Entry point, App event loop, Command dispatch + │ ├── lib.rs # Public API + │ │ + │ ├── runtime/ # Runtime Subsystem + │ │ ├── mod.rs + │ │ ├── traits.rs # Plugin, PanelProvider, Tool, Command, Source + │ │ ├── registry.rs # ModuleRegistry, provider bidding + │ │ ├── lifecycle.rs # Provider lifecycle management + │ │ └── ctx.rs # RuntimeServices (ProviderCtx removed) + │ │ + │ ├── ui/ # UI Framework + │ │ ├── mod.rs + │ │ ├── layout.rs # Panel sizing + │ │ ├── focus.rs # Focus management + │ │ ├── router.rs # Event routing (legacy, to be removed) + │ │ ├── scheduler.rs # Render scheduling + │ │ ├── menubar.rs # Menubar rendering + │ │ ├── statusbar.rs # Status bar rendering + │ │ └── splash.rs # Splash screen + │ │ + │ ├── agent/ # Agent Framework + │ │ ├── mod.rs + │ │ ├── roles.rs # Agent role definitions + │ │ ├── orchestrator.rs # Multi-agent coordination + │ │ ├── memory.rs # Short/long-term memory + │ │ └── tools.rs # Tool invocation for LLM + │ │ + │ ├── plugin/ # Plugin Subsystem + │ │ ├── mod.rs + │ │ ├── loader.rs # ExtensionLoader (.wasm / .so) + │ │ ├── translator.rs # C-ABI ↔ Rust trait + │ │ ├── surface.rs # EjSurface, cell grid + │ │ └── panic.rs # Panic isolation + │ │ + │ └── builtin/ # Built-in modules + │ ├── editor/ + │ ├── terminal/ + │ ├── tasktree/ + │ ├── agentchat/ + │ └── filetree/ + │ + ├── doc/ + │ └── Architecture.md # This file + │ + └── Cargo.toml +``` + +--- + +## 7. Key Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Provider → System comms | **Command pattern** | Explicit intents, no hidden routing, easy to log/intercept | +| Content routing | **Bidding (can_open score)** | Multiple providers can compete; new providers need no core changes | +| Provider rendering | Dual-track | Built-in direct Buffer, external cell grid | +| Event routing | 5-layer | System → Menubar → Focus shortcuts → Provider → Command dispatch | +| Tool lifecycle | Plugin-level | Independent of Provider activation | +| Main Panel | Single content | No tab bar, stack nav, keep_alive_on_hide | +| Workspaces | Not in MVP | Manual provider switching sufficient | +| Plugin loading | Eager + config | All at startup, user-configurable blacklist | +| Focus | Keyboard + mouse | Ctrl+J/K/L for panels (avoids terminal tab interception) | + +--- + +*Architecture approved? If yes, this becomes the authoritative spec.* diff --git a/doc/Plugin-Lifecycle-Tool-vs-Provider.md b/doc/Plugin-Lifecycle-Tool-vs-Provider.md new file mode 100644 index 0000000..4ee1206 --- /dev/null +++ b/doc/Plugin-Lifecycle-Tool-vs-Provider.md @@ -0,0 +1,157 @@ +# Tool vs Provider: Different Lifecycles + +## The Problem + +> "How can text plugin receive a tool call when it is not activated to main panel?" + +**Answer: Tool and Provider have different lifecycles.** + +``` +Plugin (EditorPlugin) ── lives as long as the app runs (or until plugin is unloaded) +├── Tool: open_file ── always available +├── Tool: save_file ── always available +├── Tool: find_and_replace ── always available +│ +└── Provider: EditorProvider ── created/destroyed per Main Panel instance + ├── Instance A (showing main.rs) + ├── Instance B (showing lib.rs) ← if we support split later + └── destroyed when user closes the file +``` + +## Key Insight + +**Tool is registered at Plugin level, not Provider level.** + +```rust +pub trait Plugin { + fn bootstrap(registrar: &mut dyn Registrar); +} + +pub trait Registrar { + // Tool lives with the Plugin + fn register_tool(&mut self, desc: ToolDesc, handler: Box); + + // Provider is a factory — created on demand when panel needs it + fn register_provider(&mut self, desc: ProviderDesc, factory: ProviderFactory); +} +``` + +## How It Works + +### Plugin State (Shared) + +```rust +pub struct EditorPlugin { + // Plugin-level state: all open documents + documents: HashMap, + config: EditorConfig, +} + +impl Plugin for EditorPlugin { + fn bootstrap(registrar: &mut dyn Registrar) { + let plugin = Arc::new(Mutex::new(EditorPlugin::new())); + + // Register tools — these live with the plugin, not any provider instance + registrar.register_tool( + ToolDesc { name: "open_file", ... }, + Box::new(OpenFileTool { plugin: plugin.clone() }), + ); + registrar.register_tool( + ToolDesc { name: "find_and_replace", ... }, + Box::new(FindReplaceTool { plugin: plugin.clone() }), + ); + + // Register provider factory + registrar.register_provider( + ProviderDesc { id: "editor", panel: PanelType::Main, ... }, + Box::new(move |_| Box::new(EditorProvider::new(plugin.clone()))), + ); + } +} +``` + +### Tool Execution (Always Available) + +```rust +pub struct OpenFileTool { + plugin: Arc>, +} + +impl Tool for OpenFileTool { + fn execute(&self, input: ToolInput) -> ToolOutput { + let path = input.args["path"].as_str().unwrap(); + let content = std::fs::read_to_string(path).unwrap(); + + // 1. Store in plugin-level state + self.plugin.lock().documents.insert(path.into(), Document::new(content)); + + // 2. Request UI to open this file in Main Panel + // This goes through a UI action, not the tool system + runtime.ui_actions().open_file(path); + + ToolOutput::ok("File opened") + } +} +``` + +### Provider Render (Reads from Shared State) + +```rust +pub struct EditorProvider { + plugin: Arc>, + current_file: PathBuf, +} + +impl PanelProvider for EditorProvider { + fn render(&mut self, area: Rect, buf: &mut Buffer) { + let plugin = self.plugin.lock(); + let doc = plugin.documents.get(&self.current_file).unwrap(); + // render document content + } +} +``` + +## Lifecycle Comparison + +| | Tool | Provider | +|---|---|---| +| **Registered by** | Plugin | Plugin | +| **Created when** | Plugin bootstrap | Panel needs to display content | +| **Destroyed when** | Plugin unload | Panel switches away (unless keep_alive) | +| **Can be called when** | Always | Only when panel is active (render/event) | +| **Has state** | Yes, Plugin-level shared state | Yes, instance-level state (cursor, scroll) | + +## Agent Opens a File: Full Flow + +``` +Agent: "Open src/main.rs" + → LLM decides to call tool "open_file" + → Tool Registry finds "open_file" (registered by EditorPlugin) + → OpenFileTool.execute(path: "src/main.rs") + → Read file from disk + → Store in EditorPlugin.documents + → Call ui_actions.open_file("src/main.rs") + → UI Framework: Main Panel provider = "editor" + → UI Framework: EditorProvider.current_file = "src/main.rs" + → EditorProvider.init() + → EditorProvider.render() — reads from shared state + → Tool returns "File opened" to Agent + → Agent displays "I've opened src/main.rs for you" +``` + +## Why This Matters + +**Without this separation:** +- Agent calls `open_file` → Editor Provider not active → crash or no-op +- Tool must wait for Provider to be created → Agent workflow broken + +**With this separation:** +- Tool operates on Plugin-level state, independent of UI +- UI switches to Provider after Tool completes +- Provider renders from shared state + +## Design Rule + +> **Plugin = shared state + tools + provider factory.** +> **Tool = operates on shared state, always callable.** +> **Provider = renders shared state into a panel, only active when visible.** diff --git a/src/agent/memory.rs b/src/agent/memory.rs new file mode 100644 index 0000000..deb947c --- /dev/null +++ b/src/agent/memory.rs @@ -0,0 +1,12 @@ +/// Short-term and long-term memory for agents. +pub struct MemorySystem; + +impl MemorySystem { + pub fn new() -> Self { + Self + } + + // TODO: conversation context sliding window + // TODO: vector embedding for long-term retrieval + // TODO: SQLite storage +} diff --git a/src/agent/mod.rs b/src/agent/mod.rs new file mode 100644 index 0000000..c16a96b --- /dev/null +++ b/src/agent/mod.rs @@ -0,0 +1,4 @@ +pub mod memory; +pub mod orchestrator; +pub mod roles; +pub mod tools; diff --git a/src/agent/orchestrator.rs b/src/agent/orchestrator.rs new file mode 100644 index 0000000..c0020f3 --- /dev/null +++ b/src/agent/orchestrator.rs @@ -0,0 +1,12 @@ +/// Coordinates multiple agents and dispatches tasks. +pub struct Orchestrator; + +impl Orchestrator { + pub fn new() -> Self { + Self + } + + // TODO: task decomposition + // TODO: agent assignment + // TODO: result aggregation +} diff --git a/src/agent/roles.rs b/src/agent/roles.rs new file mode 100644 index 0000000..a109ccb --- /dev/null +++ b/src/agent/roles.rs @@ -0,0 +1,62 @@ +use ratatui::style::Color; + +/// Agent role definition. Managed by core system, not plugins. +pub struct AgentRole { + pub id: String, + pub name: String, + pub icon: String, + pub color: Color, + pub system_prompt: String, + pub tools: Vec, + pub trigger: TriggerMode, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TriggerMode { + Always, + OnUserRequest, + OnEvent(String), + Auto, +} + +/// Built-in roles. +pub fn builtin_roles() -> Vec { + vec![ + AgentRole { + id: "secretary".into(), + name: "Secretary".into(), + icon: "👤".into(), + color: Color::Cyan, + system_prompt: "You are a helpful assistant...".into(), + tools: vec![], + trigger: TriggerMode::Always, + }, + AgentRole { + id: "driver".into(), + name: "Driver".into(), + icon: "🧭".into(), + color: Color::Yellow, + system_prompt: "You are a curious explorer...".into(), + tools: vec!["search_web".into(), "read_file".into()], + trigger: TriggerMode::Auto, + }, + AgentRole { + id: "negfeedback".into(), + name: "NegFeedback".into(), + icon: "🛡️".into(), + color: Color::Red, + system_prompt: "You check for errors and potential issues...".into(), + tools: vec![], + trigger: TriggerMode::Auto, + }, + AgentRole { + id: "executor".into(), + name: "Executor".into(), + icon: "⚡".into(), + color: Color::Magenta, + system_prompt: "You execute commands and tasks...".into(), + tools: vec!["run_shell".into()], + trigger: TriggerMode::Auto, + }, + ] +} diff --git a/src/agent/tools.rs b/src/agent/tools.rs new file mode 100644 index 0000000..ecd334b --- /dev/null +++ b/src/agent/tools.rs @@ -0,0 +1,15 @@ +use crate::runtime::{ToolInput, ToolOutput}; + +/// Invokes tools on behalf of agents via LLM function calling. +pub struct ToolInvoker; + +impl ToolInvoker { + pub fn new() -> Self { + Self + } + + pub fn invoke(&self, _name: &str, _input: ToolInput) -> ToolOutput { + // TODO: look up tool in registry, execute, return result + ToolOutput::err("not implemented") + } +} diff --git a/src/builtin/agentchat/mod.rs b/src/builtin/agentchat/mod.rs new file mode 100644 index 0000000..9bfe8fe --- /dev/null +++ b/src/builtin/agentchat/mod.rs @@ -0,0 +1,277 @@ +use crate::runtime::{Command, EventResult, PanelProvider, Source}; +use crossterm::event::{Event, KeyCode, KeyModifiers}; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph, Widget, Wrap}, +}; + +#[derive(Clone, Debug)] +pub(crate) struct ChatMessage { + pub(crate) role: String, + pub(crate) content: String, +} + +pub struct AgentChatProvider { + pub(crate) messages: Vec, + pub(crate) input_buffer: String, + cursor_pos: usize, + scroll_offset: usize, + pub(crate) selected_backend: usize, + backends: Vec, +} + +impl AgentChatProvider { + pub fn new() -> Self { + let mut provider = Self { + messages: vec![ + ChatMessage { + role: "user".into(), + content: "write a sorting function".into(), + }, + ChatMessage { + role: "assistant".into(), + content: "Here's a clean implementation:\n\nfn sort(list: &mut [T]) {\n list.sort();\n}\n\nOr if you want a custom quicksort:\n\nfn quicksort(arr: &mut [T]) {\n if arr.len() <= 1 { return; }\n let pivot = arr.len() - 1;\n // ... partition and recurse\n}".into(), + }, + ], + input_buffer: String::new(), + cursor_pos: 0, + scroll_offset: 0, + selected_backend: 0, + backends: vec!["Claude".into(), "GPT-4".into(), "Local".into()], + }; + provider.scroll_to_bottom(); + provider + } + + fn scroll_to_bottom(&mut self) { + self.scroll_offset = self.messages.len().saturating_sub(1); + } + + fn add_message(&mut self, role: &str, content: &str) { + self.messages.push(ChatMessage { + role: role.into(), + content: content.into(), + }); + self.scroll_to_bottom(); + } + + fn handle_input(&mut self, c: char) { + self.input_buffer.insert(self.cursor_pos, c); + self.cursor_pos += 1; + } + + fn handle_backspace(&mut self) { + if self.cursor_pos > 0 { + self.cursor_pos -= 1; + self.input_buffer.remove(self.cursor_pos); + } + } + + fn handle_delete(&mut self) { + if self.cursor_pos < self.input_buffer.len() { + self.input_buffer.remove(self.cursor_pos); + } + } + + fn move_cursor(&mut self, delta: isize) { + if delta < 0 { + self.cursor_pos = self.cursor_pos.saturating_sub((-delta) as usize); + } else { + self.cursor_pos = (self.cursor_pos + delta as usize).min(self.input_buffer.len()); + } + } + + fn submit(&mut self) { + let text = self.input_buffer.trim().to_string(); + if !text.is_empty() { + self.add_message("user", &text); + self.add_message( + "assistant", + &format!("[{}] Processing: {}...", self.backends[self.selected_backend], text), + ); + } + self.input_buffer.clear(); + self.cursor_pos = 0; + } + + fn cycle_backend(&mut self) { + self.selected_backend = (self.selected_backend + 1) % self.backends.len(); + } + + fn message_lines(&self) -> Vec> { + let mut lines = Vec::new(); + for msg in &self.messages { + let (label, label_color) = match msg.role.as_str() { + "user" => ("You:", Color::White), + "assistant" => ("Agent:", Color::Green), + "system" => ("System:", Color::Yellow), + _ => ("??:", Color::DarkGray), + }; + lines.push(Line::styled( + label.to_string(), + Style::default().fg(label_color).add_modifier(Modifier::BOLD), + )); + for content_line in msg.content.lines() { + lines.push(Line::styled( + content_line.to_string(), + Style::default().fg(Color::Rgb(200, 200, 200)), + )); + } + lines.push(Line::from("")); + } + lines + } +} + +impl PanelProvider for AgentChatProvider { + fn as_any(&self) -> &dyn std::any::Any { self } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn id(&self) -> &str { "agentchat" } + + fn render(&mut self, area: Rect, buf: &mut Buffer) { + let vertical = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Min(0), + Constraint::Length(1), + Constraint::Length(2), + Constraint::Length(4), + ]) + .split(area); + + let tabs: Vec = self + .backends + .iter() + .enumerate() + .flat_map(|(i, name)| { + let style = if i == self.selected_backend { + Style::default() + .bg(Color::Rgb(60, 60, 60)) + .fg(Color::Rgb(204, 204, 204)) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::DarkGray) + }; + vec![Span::raw(" "), Span::styled(format!("{} ", name), style)] + }) + .collect(); + Paragraph::new(Line::from(tabs)) + .style(Style::default().bg(Color::Rgb(35, 35, 35))) + .render(vertical[0], buf); + + let all_lines = self.message_lines(); + let visible_height = vertical[1].height as usize; + let line_count = all_lines.len(); + + let scroll_offset = if self.scroll_offset + visible_height > line_count { + line_count.saturating_sub(visible_height) + } else { + self.scroll_offset + }; + + let visible_lines: Vec = all_lines + .iter() + .skip(scroll_offset) + .take(visible_height) + .cloned() + .collect(); + + Paragraph::new(visible_lines) + .style(Style::default().bg(Color::Rgb(25, 25, 25))) + .wrap(Wrap { trim: false }) + .render(vertical[1], buf); + + let sep = Block::default() + .borders(Borders::TOP) + .border_style(Color::Rgb(60, 60, 60)); + sep.render(vertical[2], buf); + + let input_lines = vec![ + Line::from(vec![ + Span::styled( + "> ", + Style::default().fg(Color::Green).add_modifier(Modifier::BOLD), + ), + Span::raw(&self.input_buffer), + ]), + Line::styled( + "Enter to send | Ctrl+B cycle backend | PgUp/PgDn scroll", + Style::default().fg(Color::DarkGray), + ), + ]; + Paragraph::new(input_lines) + .style(Style::default().bg(Color::Rgb(25, 25, 25))) + .render(vertical[3], buf); + + let links = Paragraph::new(vec![ + Line::from(""), + Line::from(vec![ + Span::styled("[agents.md]", Style::default().fg(Color::Cyan)), + Span::raw(" "), + Span::styled("[soul.md]", Style::default().fg(Color::Cyan)), + ]), + Line::from(vec![ + Span::styled("[memory.json]", Style::default().fg(Color::Cyan)), + Span::raw(" "), + Span::styled("[context.md]", Style::default().fg(Color::Cyan)), + ]), + ]) + .block( + Block::default() + .title(" Quick Links ") + .borders(Borders::TOP) + .border_style(Color::Rgb(60, 60, 60)), + ) + .style(Style::default().bg(Color::Rgb(25, 25, 25))); + links.render(vertical[4], buf); + } + + fn handle_event(&mut self, event: Event) -> (EventResult, Vec) { + let commands = Vec::new(); + if let Event::Key(key) = event { + if key.code == KeyCode::Char('b') && key.modifiers.contains(KeyModifiers::CONTROL) { + self.cycle_backend(); + return (EventResult::Consumed, commands); + } + match key.code { + KeyCode::Char(c) => { self.handle_input(c); return (EventResult::Consumed, commands); } + KeyCode::Backspace => { self.handle_backspace(); return (EventResult::Consumed, commands); } + KeyCode::Delete => { self.handle_delete(); return (EventResult::Consumed, commands); } + KeyCode::Enter => { self.submit(); return (EventResult::Consumed, commands); } + KeyCode::Left => { self.move_cursor(-1); return (EventResult::Consumed, commands); } + KeyCode::Right => { self.move_cursor(1); return (EventResult::Consumed, commands); } + KeyCode::Home => { self.cursor_pos = 0; return (EventResult::Consumed, commands); } + KeyCode::End => { self.cursor_pos = self.input_buffer.len(); return (EventResult::Consumed, commands); } + KeyCode::PageUp => { + self.scroll_offset = self.scroll_offset.saturating_sub(5); + return (EventResult::Consumed, commands); + } + KeyCode::PageDown => { + let max = self.messages.len().saturating_sub(1); + self.scroll_offset = (self.scroll_offset + 5).min(max); + return (EventResult::Consumed, commands); + } + _ => {} + } + } + (EventResult::PassThrough, commands) + } + + fn focus_changed(&mut self, _focused: bool) {} + fn hide(&mut self) {} + fn show(&mut self) {} + fn shutdown(&mut self) {} + + fn can_open(&self, _source: &Source, _mime_hint: Option<&str>) -> i32 { + // AgentChat doesn't compete for content presentation + 0 + } +} + +#[cfg(test)] +mod tests; diff --git a/src/builtin/agentchat/tests.rs b/src/builtin/agentchat/tests.rs new file mode 100644 index 0000000..25c7cc7 --- /dev/null +++ b/src/builtin/agentchat/tests.rs @@ -0,0 +1,65 @@ +use crate::builtin::agentchat::AgentChatProvider; +use crate::runtime::PanelProvider; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; + +fn char_event(c: char) -> Event { + Event::Key(KeyEvent::from(KeyCode::Char(c))) +} + +fn key_event(code: KeyCode) -> Event { + Event::Key(KeyEvent::from(code)) +} + +fn ctrl_event(c: char) -> Event { + Event::Key(KeyEvent { + code: KeyCode::Char(c), + modifiers: KeyModifiers::CONTROL, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::empty(), + }) +} + +#[test] +fn agentchat_has_initial_messages() { + let chat = AgentChatProvider::new(); + assert!(!chat.messages.is_empty()); +} + +#[test] +fn agentchat_typing() { + let mut chat = AgentChatProvider::new(); + chat.handle_event(char_event('h')); + chat.handle_event(char_event('i')); + assert_eq!(chat.input_buffer, "hi"); +} + +#[test] +fn agentchat_submit_adds_message() { + let mut chat = AgentChatProvider::new(); + let before = chat.messages.len(); + chat.handle_event(char_event('t')); + chat.handle_event(char_event('e')); + chat.handle_event(char_event('s')); + chat.handle_event(char_event('t')); + chat.handle_event(key_event(KeyCode::Enter)); + let after = chat.messages.len(); + assert!(after > before); + assert_eq!(chat.input_buffer, ""); +} + +#[test] +fn agentchat_cycle_backend() { + let mut chat = AgentChatProvider::new(); + let first = chat.selected_backend; + chat.handle_event(ctrl_event('b')); + assert_ne!(chat.selected_backend, first); +} + +#[test] +fn agentchat_empty_submit_ignored() { + let mut chat = AgentChatProvider::new(); + let before = chat.messages.len(); + chat.handle_event(key_event(KeyCode::Enter)); + let after = chat.messages.len(); + assert_eq!(after, before); +} diff --git a/src/builtin/editor/mod.rs b/src/builtin/editor/mod.rs new file mode 100644 index 0000000..9778bc2 --- /dev/null +++ b/src/builtin/editor/mod.rs @@ -0,0 +1,271 @@ +use crate::runtime::{Command, EventResult, PanelProvider, Source}; +use crossterm::event::{Event, KeyCode, KeyModifiers}; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Paragraph, Widget}, +}; + +pub struct EditorProvider { + lines: Vec, + cursor_line: usize, + cursor_col: usize, + scroll_offset: usize, + filename: String, + modified: bool, +} + +impl EditorProvider { + pub fn new() -> Self { + let code = vec![ + "// main.rs".into(), + "".into(), + "fn main() {".into(), + " let data = fetch_data();".into(), + " process(&data);".into(), + " println!(\"done\");".into(), + "}".into(), + "".into(), + "fn fetch_data() -> Vec {".into(), + " vec![1, 2, 3]".into(), + "}".into(), + "".into(), + "fn process(data: &[i32]) {".into(), + " let sum: i32 = data.iter().sum();".into(), + " println!(\"sum = {}\", sum);".into(), + "}".into(), + ]; + Self { + lines: code, + cursor_line: 2, + cursor_col: 9, + scroll_offset: 0, + filename: "main.rs".into(), + modified: false, + } + } + + pub fn cursor_line(&self) -> usize { self.cursor_line + 1 } + pub fn cursor_col(&self) -> usize { self.cursor_col + 1 } + pub fn filename(&self) -> &str { &self.filename } + pub fn is_modified(&self) -> bool { self.modified } + + fn move_cursor(&mut self, dx: isize, dy: isize) { + if dy != 0 { + let new_line = if dy < 0 { + self.cursor_line.saturating_sub((-dy) as usize) + } else { + (self.cursor_line + dy as usize).min(self.lines.len().saturating_sub(1)) + }; + self.cursor_line = new_line; + let line_len = self.lines.get(self.cursor_line).map(|l| l.len()).unwrap_or(0); + self.cursor_col = self.cursor_col.min(line_len); + } + if dx != 0 { + let line_len = self.lines.get(self.cursor_line).map(|l| l.len()).unwrap_or(0); + if dx < 0 { + self.cursor_col = self.cursor_col.saturating_sub((-dx) as usize); + } else { + self.cursor_col = (self.cursor_col + dx as usize).min(line_len); + } + } + } + + fn ensure_cursor_visible(&mut self, visible_height: usize) { + if self.cursor_line >= self.scroll_offset + visible_height { + self.scroll_offset = self.cursor_line.saturating_sub(visible_height - 1); + } else if self.cursor_line < self.scroll_offset { + self.scroll_offset = self.cursor_line; + } + } +} + +impl PanelProvider for EditorProvider { + fn as_any(&self) -> &dyn std::any::Any { self } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn id(&self) -> &str { "editor" } + + fn render(&mut self, area: Rect, buf: &mut Buffer) { + let horizontal = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(4), Constraint::Min(0)]) + .split(area); + + let gutter_area = horizontal[0]; + let content_area = horizontal[1]; + let visible_height = content_area.height as usize; + + self.ensure_cursor_visible(visible_height); + + let gutter_lines: Vec = (self.scroll_offset + ..(self.scroll_offset + visible_height).min(self.lines.len())) + .map(|i| { + let num = format!("{:>3} ", i + 1); + let style = if i == self.cursor_line { + Style::default().fg(Color::Cyan).add_modifier(ratatui::style::Modifier::BOLD) + } else { + Style::default().fg(Color::DarkGray) + }; + Line::styled(num, style) + }) + .collect(); + Paragraph::new(gutter_lines).render(gutter_area, buf); + + let content_lines: Vec = self + .lines + .iter() + .skip(self.scroll_offset) + .take(visible_height) + .enumerate() + .map(|(i, line)| { + let absolute_line = self.scroll_offset + i; + let is_current = absolute_line == self.cursor_line; + let bg = if is_current { Some(Color::Rgb(40, 40, 40)) } else { None }; + highlight_rust_line(line, bg) + }) + .collect(); + Paragraph::new(content_lines).render(content_area, buf); + } + + fn handle_event(&mut self, event: Event) -> (EventResult, Vec) { + let commands = Vec::new(); + if let Event::Key(key) = event { + match key.code { + KeyCode::Up => { self.move_cursor(0, -1); return (EventResult::Consumed, commands); } + KeyCode::Down => { self.move_cursor(0, 1); return (EventResult::Consumed, commands); } + KeyCode::Left => { self.move_cursor(-1, 0); return (EventResult::Consumed, commands); } + KeyCode::Right => { self.move_cursor(1, 0); return (EventResult::Consumed, commands); } + KeyCode::Home => { self.cursor_col = 0; return (EventResult::Consumed, commands); } + KeyCode::End => { + let line_len = self.lines.get(self.cursor_line).map(|l| l.len()).unwrap_or(0); + self.cursor_col = line_len; + return (EventResult::Consumed, commands); + } + KeyCode::PageUp => { self.move_cursor(0, -10); return (EventResult::Consumed, commands); } + KeyCode::PageDown => { self.move_cursor(0, 10); return (EventResult::Consumed, commands); } + KeyCode::Char('g') if key.modifiers.contains(KeyModifiers::CONTROL) => { + self.cursor_line = 0; self.cursor_col = 0; + return (EventResult::Consumed, commands); + } + _ => {} + } + } + (EventResult::PassThrough, commands) + } + + fn focus_changed(&mut self, _focused: bool) {} + fn hide(&mut self) {} + fn show(&mut self) {} + fn shutdown(&mut self) {} + + fn can_open(&self, source: &Source, mime_hint: Option<&str>) -> i32 { + let mime = mime_hint + .map(|m| m.to_string()) + .or_else(|| source.mime_hint()); + match mime.as_deref() { + Some("text/rust") | Some("text/plain") | Some("text/toml") | Some("text/markdown") + | Some("text/x-shellscript") | Some("text/x-python") | Some("text/javascript") + | Some("text/typescript") | Some("application/json") => 100, + Some(m) if m.starts_with("text/") => 50, + _ => 0, + } + } +} + +fn highlight_rust_line(line: &str, bg: Option) -> Line<'_> { + let base_style = Style::default().bg(bg.unwrap_or(Color::Reset)); + if line.trim().starts_with("//") { + return Line::styled(line.to_string(), base_style.fg(Color::DarkGray)); + } + + let keywords = ["fn", "let", "mut", "use", "pub", "struct", "impl", "if", "else", "for", "while", "match", "return", "self", "Self", "where", "async", "await", "trait", "enum", "type", "const", "static", "move", "ref", "dyn"]; + let types = ["i32", "i64", "u32", "u64", "f32", "f64", "bool", "char", "str", "String", "Vec", "Option", "Result", "Box", "HashMap", "Path", "PathBuf"]; + let builtins = ["println!", "format!", "vec!", "panic!", "todo!", "unimplemented!", "assert!"]; + + let mut spans = Vec::new(); + let mut remaining = line; + + while !remaining.is_empty() { + let mut found = false; + + if remaining.starts_with('"') { + if let Some(end) = remaining[1..].find('"') { + let lit = &remaining[..end + 2]; + spans.push(Span::styled(lit.to_string(), base_style.fg(Color::Green))); + remaining = &remaining[end + 2..]; + found = true; + } + } + + if !found { + for kw in &keywords { + if remaining.starts_with(kw) { + let after = &remaining[kw.len()..]; + if after.is_empty() || !after.starts_with(|c: char| c.is_alphanumeric() || c == '_') { + spans.push(Span::styled(kw.to_string(), base_style.fg(Color::Magenta))); + remaining = after; + found = true; + break; + } + } + } + } + if !found { + for t in &types { + if remaining.starts_with(t) { + let after = &remaining[t.len()..]; + if after.is_empty() || !after.starts_with(|c: char| c.is_alphanumeric() || c == '_') { + spans.push(Span::styled(t.to_string(), base_style.fg(Color::Yellow))); + remaining = after; + found = true; + break; + } + } + } + } + if !found { + for b in &builtins { + if remaining.starts_with(b) { + spans.push(Span::styled(b.to_string(), base_style.fg(Color::Cyan))); + remaining = &remaining[b.len()..]; + found = true; + break; + } + } + } + + if !found && remaining.starts_with(|c: char| c.is_ascii_digit()) { + let end = remaining.find(|c: char| !c.is_ascii_digit()).unwrap_or(remaining.len()); + let num = &remaining[..end]; + spans.push(Span::styled(num.to_string(), base_style.fg(Color::Yellow))); + remaining = &remaining[end..]; + found = true; + } + + if !found { + if let Some(i) = remaining.find(|c: char| !c.is_alphanumeric() && c != '_') { + if remaining[i..].starts_with('(') && i > 0 { + let name = &remaining[..i]; + spans.push(Span::styled(name.to_string(), base_style.fg(Color::Cyan))); + remaining = &remaining[i..]; + found = true; + } + } + } + + if !found { + let ch = remaining.chars().next().unwrap(); + let s = ch.to_string(); + spans.push(Span::styled(s, base_style.fg(Color::Rgb(204, 204, 204)))); + remaining = &remaining[ch.len_utf8()..]; + } + } + + Line::from(spans) +} + +#[cfg(test)] +mod tests; diff --git a/src/builtin/editor/tests.rs b/src/builtin/editor/tests.rs new file mode 100644 index 0000000..a744b96 --- /dev/null +++ b/src/builtin/editor/tests.rs @@ -0,0 +1,59 @@ +use crate::builtin::editor::EditorProvider; +use crate::runtime::PanelProvider; +use crossterm::event::{Event, KeyCode, KeyEvent}; + +fn key_event(code: KeyCode) -> Event { + Event::Key(KeyEvent::from(code)) +} + +#[test] +fn editor_cursor_moves() { + let mut editor = EditorProvider::new(); + let start_line = editor.cursor_line(); + let start_col = editor.cursor_col(); + editor.handle_event(key_event(KeyCode::Down)); + assert!(editor.cursor_line() > start_line || editor.cursor_line() == start_line); + editor.handle_event(key_event(KeyCode::Right)); + assert!(editor.cursor_col() > start_col || editor.cursor_col() == start_col); +} + +#[test] +fn editor_cursor_bounded() { + let mut editor = EditorProvider::new(); + for _ in 0..100 { editor.handle_event(key_event(KeyCode::Up)); } + for _ in 0..100 { editor.handle_event(key_event(KeyCode::Left)); } + assert_eq!(editor.cursor_line(), 1); + assert_eq!(editor.cursor_col(), 1); +} + +#[test] +fn editor_consumes_arrow_keys() { + let mut editor = EditorProvider::new(); + for code in [KeyCode::Up, KeyCode::Down, KeyCode::Left, KeyCode::Right] { + let (result, _) = editor.handle_event(key_event(code)); + assert!( + matches!(result, crate::runtime::EventResult::Consumed), + "Expected Consumed for {:?}", code + ); + } +} + +#[test] +fn editor_has_filename() { + let editor = EditorProvider::new(); + assert_eq!(editor.filename(), "main.rs"); +} + +#[test] +fn editor_can_open_text_files() { + let editor = EditorProvider::new(); + let source = crate::runtime::Source::File { path: "test.rs".into() }; + assert!(editor.can_open(&source, Some("text/rust")) > 0); +} + +#[test] +fn editor_rejects_binary_files() { + let editor = EditorProvider::new(); + let source = crate::runtime::Source::File { path: "image.png".into() }; + assert_eq!(editor.can_open(&source, Some("image/png")), 0); +} diff --git a/src/builtin/filetree/mod.rs b/src/builtin/filetree/mod.rs new file mode 100644 index 0000000..7454a89 --- /dev/null +++ b/src/builtin/filetree/mod.rs @@ -0,0 +1,304 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::runtime::{Command, EventResult, PanelProvider, Source}; +use crossterm::event::{Event, KeyCode}; +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Paragraph, Widget, Wrap}, +}; + +// --------------------------------------------------------------------------- +// FileNode +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +pub(crate) struct FileNode { + pub(crate) name: String, + pub(crate) path: PathBuf, + pub(crate) is_dir: bool, + pub(crate) expanded: bool, + pub(crate) children: Vec, + pub(crate) depth: usize, +} + +impl FileNode { + fn from_path(path: &Path, depth: usize) -> Option { + let name = path.file_name()?.to_string_lossy().to_string(); + let is_dir = path.is_dir(); + Some(Self { + name, + path: path.to_path_buf(), + is_dir, + expanded: depth == 0, + children: Vec::new(), + depth, + }) + } + + fn load_children(&mut self) { + if !self.is_dir || !self.children.is_empty() { + return; + } + let Ok(entries) = fs::read_dir(&self.path) else { return }; + let mut children: Vec<_> = entries + .filter_map(|e| e.ok()) + .filter_map(|e| FileNode::from_path(&e.path(), self.depth + 1)) + .collect(); + children.sort_by(|a, b| { + match (a.is_dir, b.is_dir) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.name.cmp(&b.name), + } + }); + self.children = children; + } + + fn toggle(&mut self) { + if self.is_dir { + self.expanded = !self.expanded; + if self.expanded { + self.load_children(); + } + } + } +} + +// --------------------------------------------------------------------------- +// FileTreeProvider +// --------------------------------------------------------------------------- + +pub struct FileTreeProvider { + root: FileNode, + pub(crate) selected: usize, + pub(crate) scroll_offset: usize, +} + +impl FileTreeProvider { + pub fn new() -> Self { + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let mut root = FileNode::from_path(&cwd, 0).unwrap_or_else(|| FileNode { + name: ".".into(), + path: cwd.clone(), + is_dir: true, + expanded: true, + children: Vec::new(), + depth: 0, + }); + root.load_children(); + Self { + root, + selected: 0, + scroll_offset: 0, + } + } + + pub(crate) fn flat_nodes(&self) -> Vec<&FileNode> { + let mut result = Vec::new(); + Self::collect_visible(&self.root, &mut result); + result + } + + fn collect_visible<'a>(node: &'a FileNode, out: &mut Vec<&'a FileNode>) { + out.push(node); + if node.expanded { + for child in &node.children { + Self::collect_visible(child, out); + } + } + } + + fn node_by_index_mut(&mut self, idx: usize) -> Option<&mut FileNode> { + let mut stack = vec![&mut self.root]; + let mut count = 0; + while let Some(node) = stack.pop() { + if count == idx { + return Some(node); + } + count += 1; + if node.expanded { + for child in node.children.iter_mut().rev() { + stack.push(child); + } + } + } + None + } + + fn move_selection(&mut self, delta: isize) { + let flat = self.flat_nodes(); + if flat.is_empty() { + return; + } + let new_idx = if delta < 0 { + self.selected.saturating_sub((-delta) as usize) + } else { + (self.selected + delta as usize).min(flat.len().saturating_sub(1)) + }; + self.selected = new_idx; + } +} + +impl PanelProvider for FileTreeProvider { + fn as_any(&self) -> &dyn std::any::Any { self } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn id(&self) -> &str { "filetree" } + + fn render(&mut self, area: Rect, buf: &mut Buffer) { + let visible_height = area.height as usize; + + self.scroll_offset = if self.selected >= self.scroll_offset + visible_height { + self.selected.saturating_sub(visible_height - 1) + } else if self.selected < self.scroll_offset { + self.selected + } else { + self.scroll_offset + }; + + let flat = self.flat_nodes(); + let lines: Vec = flat + .iter() + .skip(self.scroll_offset) + .take(visible_height) + .enumerate() + .map(|(i, node)| { + let absolute_idx = self.scroll_offset + i; + let is_selected = absolute_idx == self.selected; + + let indent = " ".repeat(node.depth); + let prefix = if node.is_dir { + if node.expanded { "📂 " } else { "📁 " } + } else { + " " + }; + let icon = if node.is_dir { "" } else { file_icon(&node.name) }; + + let name_style = if is_selected { + Style::default() + .fg(Color::Black) + .bg(Color::Cyan) + .add_modifier(Modifier::BOLD) + } else if node.is_dir { + Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Rgb(204, 204, 204)) + }; + + Line::from(vec![ + Span::styled(indent, Style::default().fg(Color::DarkGray)), + Span::raw(prefix), + Span::styled(format!("{}{}", icon, node.name), name_style), + ]) + }) + .collect(); + + let title = format!(" 📁 {} ", self.root.name); + let block = ratatui::widgets::Block::default() + .title(title) + .borders(ratatui::widgets::Borders::NONE); + + Paragraph::new(lines) + .block(block) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn handle_event(&mut self, event: Event) -> (EventResult, Vec) { + let mut commands = Vec::new(); + if let Event::Key(key) = event { + match key.code { + KeyCode::Up => { + self.move_selection(-1); + return (EventResult::Consumed, commands); + } + KeyCode::Down => { + self.move_selection(1); + return (EventResult::Consumed, commands); + } + KeyCode::Left => { + let parent_path = self.node_by_index_mut(self.selected) + .filter(|n| n.depth > 0 && !(n.is_dir && n.expanded)) + .map(|n| n.path.parent().map(|p| p.to_path_buf())); + + if let Some(Some(parent)) = parent_path { + let flat = self.flat_nodes(); + for (i, n) in flat.iter().enumerate() { + if n.path == parent { + self.selected = i; + break; + } + } + } else if let Some(node) = self.node_by_index_mut(self.selected) { + if node.is_dir && node.expanded { + node.toggle(); + } + } + return (EventResult::Consumed, commands); + } + KeyCode::Right | KeyCode::Enter => { + if let Some(node) = self.node_by_index_mut(self.selected) { + if node.is_dir { + node.toggle(); + } else { + // Emit a command to open this file in Main panel + commands.push(Command::Present { + panel: crate::runtime::PanelType::Main, + source: Source::File { path: node.path.clone() }, + mime_hint: None, + }); + } + } + return (EventResult::Consumed, commands); + } + KeyCode::Char(' ') => { + if let Some(node) = self.node_by_index_mut(self.selected) { + if node.is_dir { + node.toggle(); + } + } + return (EventResult::Consumed, commands); + } + _ => {} + } + } + (EventResult::PassThrough, commands) + } + + fn focus_changed(&mut self, _focused: bool) {} + fn hide(&mut self) {} + fn show(&mut self) {} + fn shutdown(&mut self) {} + + fn can_open(&self, _source: &Source, _mime_hint: Option<&str>) -> i32 { + // FileTree doesn't compete for content; it's always the sidebar navigator + 0 + } +} + +fn file_icon(name: &str) -> &'static str { + if name.ends_with(".rs") { + "🦀 " + } else if name.ends_with(".toml") || name.ends_with(".lock") { + "⚙️ " + } else if name.ends_with(".md") { + "📝 " + } else if name.ends_with(".json") { + "📋 " + } else if name.ends_with(".sh") || name.ends_with(".bash") { + "🐚 " + } else if name.ends_with(".py") { + "🐍 " + } else if name.ends_with(".js") || name.ends_with(".ts") { + "📜 " + } else { + "📄 " + } +} + +#[cfg(test)] +mod tests; diff --git a/src/builtin/filetree/tests.rs b/src/builtin/filetree/tests.rs new file mode 100644 index 0000000..36c03c3 --- /dev/null +++ b/src/builtin/filetree/tests.rs @@ -0,0 +1,71 @@ +use crate::builtin::filetree::FileTreeProvider; +use crate::runtime::PanelProvider; +use crossterm::event::{Event, KeyCode, KeyEvent}; + +fn key_event(code: KeyCode) -> Event { + Event::Key(KeyEvent::from(code)) +} + +#[test] +fn filetree_starts_at_root() { + let tree = FileTreeProvider::new(); + let flat = tree.flat_nodes(); + assert!(!flat.is_empty()); + assert_eq!(flat[0].depth, 0); +} + +#[test] +fn filetree_navigate_down() { + let mut tree = FileTreeProvider::new(); + let before = tree.flat_nodes().len(); + assert!(before > 0); + tree.handle_event(key_event(KeyCode::Down)); +} + +#[test] +fn filetree_expand_collapse() { + let mut tree = FileTreeProvider::new(); + let flat = tree.flat_nodes(); + let first_dir_idx = flat.iter().position(|n| n.is_dir).unwrap_or(0); + for _ in 0..first_dir_idx { + tree.handle_event(key_event(KeyCode::Down)); + } + let before = tree.flat_nodes().len(); + tree.handle_event(key_event(KeyCode::Left)); + let after_collapse = tree.flat_nodes().len(); + tree.handle_event(key_event(KeyCode::Right)); + let after_expand = tree.flat_nodes().len(); + assert!(after_expand >= after_collapse); +} + +#[test] +fn filetree_consumes_navigation_keys() { + let mut tree = FileTreeProvider::new(); + let (result, _) = tree.handle_event(key_event(KeyCode::Down)); + assert!(matches!(result, crate::runtime::EventResult::Consumed)); + let (result, _) = tree.handle_event(key_event(KeyCode::Up)); + assert!(matches!(result, crate::runtime::EventResult::Consumed)); +} + +#[test] +fn filetree_passes_unknown_keys() { + let mut tree = FileTreeProvider::new(); + let (result, _) = tree.handle_event(key_event(KeyCode::Char('x'))); + assert!(matches!(result, crate::runtime::EventResult::PassThrough)); +} + +#[test] +fn filetree_enter_emits_present_command() { + let mut tree = FileTreeProvider::new(); + // Move to first file (not directory) + let flat = tree.flat_nodes(); + let first_file_idx = flat.iter().position(|n| !n.is_dir); + if let Some(idx) = first_file_idx { + for _ in 0..idx { + tree.handle_event(key_event(KeyCode::Down)); + } + let (_, commands) = tree.handle_event(key_event(KeyCode::Enter)); + assert!(!commands.is_empty()); + assert!(matches!(&commands[0], crate::runtime::Command::Present { .. })); + } +} diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs new file mode 100644 index 0000000..6ac48d9 --- /dev/null +++ b/src/builtin/mod.rs @@ -0,0 +1,5 @@ +pub mod agentchat; +pub mod editor; +pub mod filetree; +pub mod tasktree; +pub mod terminal; diff --git a/src/builtin/tasktree/mod.rs b/src/builtin/tasktree/mod.rs new file mode 100644 index 0000000..6fb1a9f --- /dev/null +++ b/src/builtin/tasktree/mod.rs @@ -0,0 +1,83 @@ +use crate::runtime::{Command, EventResult, PanelProvider, Source}; +use crossterm::event::Event; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Paragraph, Widget, Wrap}, +}; + +pub struct TaskTreeProvider; + +impl TaskTreeProvider { + pub fn new() -> Self { Self } +} + +impl PanelProvider for TaskTreeProvider { + fn as_any(&self) -> &dyn std::any::Any { self } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn id(&self) -> &str { "tasktree" } + + fn render(&mut self, area: Rect, buf: &mut Buffer) { + let vertical = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)]) + .split(area); + + let tabs = Paragraph::new("[Tree] [Chain] [By Agent]") + .style(Style::default().fg(Color::DarkGray)); + tabs.render(vertical[0], buf); + + let content = Paragraph::new(vec![ + Line::from(""), + agent_line("👤", "Secretary", Color::Cyan, "idle", Color::Green), + task_line(" └─", "✅", Color::Green, "Explain API to user"), + Line::from(""), + agent_line("🧭", "Driver", Color::Yellow, "work", Color::Yellow), + task_line(" ├─", "🟡", Color::Yellow, "Research async patterns"), + task_line(" └─", "⚪", Color::DarkGray, "Check error handling"), + Line::from(""), + agent_line("🛡️", "NegFeedback", Color::Red, "idle", Color::Green), + task_line(" └─", "✅", Color::Green, "Flag race condition"), + Line::from(""), + agent_line("⚡", "Executor", Color::Magenta, "work", Color::Yellow), + task_line(" └─", "🟡", Color::Yellow, "cargo test..."), + Line::from(""), + Line::from("3 idle | 2 work").style(Style::default().fg(Color::DarkGray)), + ]) + .wrap(Wrap { trim: true }); + + content.render(vertical[1], buf); + } + + fn handle_event(&mut self, _event: Event) -> (EventResult, Vec) { + (EventResult::PassThrough, Vec::new()) + } + + fn focus_changed(&mut self, _focused: bool) {} + fn hide(&mut self) {} + fn show(&mut self) {} + fn shutdown(&mut self) {} + + fn can_open(&self, _source: &Source, _mime_hint: Option<&str>) -> i32 { 0 } +} + +fn agent_line<'a>(icon: &'a str, name: &'a str, name_color: Color, status: &'a str, status_color: Color) -> Line<'a> { + Line::from(vec![ + Span::raw(icon), + Span::raw(" "), + Span::styled(name, Style::default().fg(name_color).add_modifier(ratatui::style::Modifier::BOLD)), + Span::raw(" "), + Span::styled(format!("● {}", status), Style::default().fg(status_color)), + ]) +} + +fn task_line<'a>(indent: &'a str, icon: &'a str, icon_color: Color, text: &'a str) -> Line<'a> { + Line::from(vec![ + Span::styled(indent, Style::default().fg(Color::DarkGray)), + Span::styled(format!("{} ", icon), Style::default().fg(icon_color)), + Span::raw(text), + ]) +} diff --git a/src/builtin/terminal/mod.rs b/src/builtin/terminal/mod.rs new file mode 100644 index 0000000..6dc50c0 --- /dev/null +++ b/src/builtin/terminal/mod.rs @@ -0,0 +1,266 @@ +use crate::runtime::{Command, EventResult, PanelProvider, Source}; +use crossterm::event::{Event, KeyCode}; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Rect}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph, Widget}, +}; + +pub struct TerminalProvider { + pub(crate) lines: Vec, + pub(crate) scroll_offset: usize, + pub(crate) input_buffer: String, + cursor_pos: usize, + pub(crate) history: Vec, + pub(crate) history_index: Option, +} + +impl TerminalProvider { + pub fn new() -> Self { + let mut provider = Self { + lines: vec![ + "Everjoy v0.1.0 — Integrated Terminal".into(), + "Type 'help' for available commands".into(), + "".into(), + ], + scroll_offset: 0, + input_buffer: String::new(), + cursor_pos: 0, + history: Vec::new(), + history_index: None, + }; + provider.add_line("$ echo 'Hello, Everjoy!'"); + provider.add_line("Hello, Everjoy!"); + provider.add_line(""); + provider.add_line("$ cargo build"); + provider.add_line(" Compiling everjoy v0.1.0"); + provider.add_line(" Finished `dev` profile [unoptimized] target(s) in 2.34s"); + provider.add_line(""); + provider + } + + fn add_line(&mut self, line: impl Into) { + self.lines.push(line.into()); + let visible = 20usize; + if self.lines.len() > visible { + self.scroll_offset = self.lines.len() - visible; + } + } + + fn handle_input(&mut self, c: char) { + self.input_buffer.insert(self.cursor_pos, c); + self.cursor_pos += 1; + self.history_index = None; + } + + fn handle_backspace(&mut self) { + if self.cursor_pos > 0 { + self.cursor_pos -= 1; + self.input_buffer.remove(self.cursor_pos); + } + } + + fn handle_delete(&mut self) { + if self.cursor_pos < self.input_buffer.len() { + self.input_buffer.remove(self.cursor_pos); + } + } + + fn move_cursor(&mut self, delta: isize) { + if delta < 0 { + self.cursor_pos = self.cursor_pos.saturating_sub((-delta) as usize); + } else { + self.cursor_pos = (self.cursor_pos + delta as usize).min(self.input_buffer.len()); + } + } + + fn submit(&mut self) { + let cmd = self.input_buffer.trim().to_string(); + if !cmd.is_empty() { + self.history.push(cmd.clone()); + self.add_line(format!("$ {}", cmd)); + self.process_command(&cmd); + } else { + self.add_line("$ "); + } + self.input_buffer.clear(); + self.cursor_pos = 0; + self.history_index = None; + } + + fn process_command(&mut self, cmd: &str) { + match cmd { + "help" => { + self.add_line("Available commands:"); + self.add_line(" help Show this help message"); + self.add_line(" clear Clear terminal output"); + self.add_line(" pwd Print working directory"); + self.add_line(" ls List files"); + self.add_line(" agents List active agents"); + self.add_line(" version Show version info"); + } + "clear" => { self.lines.clear(); } + "pwd" => { + if let Ok(dir) = std::env::current_dir() { + self.add_line(dir.display().to_string()); + } + } + "ls" => { + if let Ok(entries) = std::fs::read_dir(".") { + let mut names: Vec<_> = entries + .filter_map(|e| e.ok()) + .map(|e| e.file_name().to_string_lossy().to_string()) + .collect(); + names.sort(); + for name in names { + let is_dir = std::path::Path::new(&name).is_dir(); + self.add_line(format!("{}{}", if is_dir { "📁 " } else { " " }, name)); + } + } + } + "agents" => { + self.add_line("Active agents:"); + self.add_line(" 👤 Secretary — idle"); + self.add_line(" 🧭 Driver — working"); + self.add_line(" 🛡️ NegFeedback — idle"); + self.add_line(" ⚡ Executor — working"); + } + "version" => { + self.add_line("Everjoy v0.1.0"); + self.add_line("Built with Rust + Ratatui"); + } + _ => { + self.add_line(format!("Command not found: {}", cmd)); + self.add_line("Type 'help' for available commands"); + } + } + self.add_line(""); + } + + fn prev_history(&mut self) { + if self.history.is_empty() { return; } + let idx = match self.history_index { + Some(i) if i > 0 => i - 1, + Some(_) => 0, + None => self.history.len() - 1, + }; + self.history_index = Some(idx); + self.input_buffer = self.history[idx].clone(); + self.cursor_pos = self.input_buffer.len(); + } + + fn next_history(&mut self) { + if let Some(idx) = self.history_index { + if idx + 1 < self.history.len() { + self.history_index = Some(idx + 1); + self.input_buffer = self.history[idx + 1].clone(); + self.cursor_pos = self.input_buffer.len(); + } else { + self.history_index = None; + self.input_buffer.clear(); + self.cursor_pos = 0; + } + } + } +} + +impl PanelProvider for TerminalProvider { + fn as_any(&self) -> &dyn std::any::Any { self } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn id(&self) -> &str { "terminal" } + + fn render(&mut self, area: Rect, buf: &mut Buffer) { + let vertical = ratatui::layout::Layout::default() + .direction(ratatui::layout::Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(1)]) + .split(area); + + let output_area = vertical[0]; + let input_area = vertical[1]; + let visible_lines = output_area.height as usize; + if self.lines.len() > visible_lines { + self.scroll_offset = self.scroll_offset.min(self.lines.len() - visible_lines); + } + + let output_lines: Vec = self + .lines + .iter() + .skip(self.scroll_offset) + .take(visible_lines) + .map(|l| { + if l.starts_with("$") { + Line::from(vec![ + Span::styled("$", Style::default().fg(Color::Green).add_modifier(ratatui::style::Modifier::BOLD)), + Span::styled(&l[1..], Style::default().fg(Color::Rgb(204, 204, 204))), + ]) + } else if l.starts_with("Error") || l.starts_with("Command not found") { + Line::styled(l.clone(), Style::default().fg(Color::Red)) + } else if l.starts_with(" Compiling") || l.starts_with(" Finished") { + Line::styled(l.clone(), Style::default().fg(Color::Cyan)) + } else { + Line::styled(l.clone(), Style::default().fg(Color::Rgb(200, 200, 200))) + } + }) + .collect(); + + Paragraph::new(output_lines) + .block(Block::default().borders(Borders::NONE)) + .render(output_area, buf); + + let input = Paragraph::new(vec![Line::from(vec![ + Span::styled("$ ", Style::default().fg(Color::Green).add_modifier(ratatui::style::Modifier::BOLD)), + Span::raw(&self.input_buffer), + ])]) + .style(Style::default().bg(Color::Rgb(35, 35, 35))); + input.render(input_area, buf); + } + + fn handle_event(&mut self, event: Event) -> (EventResult, Vec) { + let commands = Vec::new(); + if let Event::Key(key) = event { + match key.code { + KeyCode::Char(c) => { self.handle_input(c); return (EventResult::Consumed, commands); } + KeyCode::Backspace => { self.handle_backspace(); return (EventResult::Consumed, commands); } + KeyCode::Delete => { self.handle_delete(); return (EventResult::Consumed, commands); } + KeyCode::Enter => { self.submit(); return (EventResult::Consumed, commands); } + KeyCode::Left => { self.move_cursor(-1); return (EventResult::Consumed, commands); } + KeyCode::Right => { self.move_cursor(1); return (EventResult::Consumed, commands); } + KeyCode::Home => { self.cursor_pos = 0; return (EventResult::Consumed, commands); } + KeyCode::End => { self.cursor_pos = self.input_buffer.len(); return (EventResult::Consumed, commands); } + KeyCode::Up => { self.prev_history(); return (EventResult::Consumed, commands); } + KeyCode::Down => { self.next_history(); return (EventResult::Consumed, commands); } + KeyCode::PageUp => { + let visible = 10; + self.scroll_offset = self.scroll_offset.saturating_sub(visible); + return (EventResult::Consumed, commands); + } + KeyCode::PageDown => { + let visible = 10; + let max = self.lines.len().saturating_sub(visible); + self.scroll_offset = (self.scroll_offset + visible).min(max); + return (EventResult::Consumed, commands); + } + _ => {} + } + } + (EventResult::PassThrough, commands) + } + + fn focus_changed(&mut self, _focused: bool) {} + fn hide(&mut self) {} + fn show(&mut self) {} + fn shutdown(&mut self) {} + + fn can_open(&self, _source: &Source, _mime_hint: Option<&str>) -> i32 { + // Terminal doesn't compete for content presentation + 0 + } + + fn keep_alive_on_hide(&self) -> bool { true } +} + +#[cfg(test)] +mod tests; diff --git a/src/builtin/terminal/tests.rs b/src/builtin/terminal/tests.rs new file mode 100644 index 0000000..4015346 --- /dev/null +++ b/src/builtin/terminal/tests.rs @@ -0,0 +1,62 @@ +use crate::builtin::terminal::TerminalProvider; +use crate::runtime::PanelProvider; +use crossterm::event::{Event, KeyCode, KeyEvent}; + +fn char_event(c: char) -> Event { + Event::Key(KeyEvent::from(KeyCode::Char(c))) +} + +fn key_event(code: KeyCode) -> Event { + Event::Key(KeyEvent::from(code)) +} + +#[test] +fn terminal_has_initial_content() { + let term = TerminalProvider::new(); + assert!(!term.lines.is_empty()); +} + +#[test] +fn terminal_typing_accumulates() { + let mut term = TerminalProvider::new(); + term.handle_event(char_event('h')); + term.handle_event(char_event('i')); + assert_eq!(term.input_buffer, "hi"); +} + +#[test] +fn terminal_backspace_works() { + let mut term = TerminalProvider::new(); + term.handle_event(char_event('a')); + term.handle_event(char_event('b')); + term.handle_event(key_event(KeyCode::Backspace)); + assert_eq!(term.input_buffer, "a"); +} + +#[test] +fn terminal_submit_clears_buffer() { + let mut term = TerminalProvider::new(); + term.handle_event(char_event('l')); + term.handle_event(char_event('s')); + term.handle_event(key_event(KeyCode::Enter)); + assert_eq!(term.input_buffer, ""); +} + +#[test] +fn terminal_help_command() { + let mut term = TerminalProvider::new(); + for c in "help".chars() { term.handle_event(char_event(c)); } + let lines_before = term.lines.len(); + term.handle_event(key_event(KeyCode::Enter)); + let lines_after = term.lines.len(); + assert!(lines_after > lines_before); +} + +#[test] +fn terminal_history_navigation() { + let mut term = TerminalProvider::new(); + for c in "test".chars() { term.handle_event(char_event(c)); } + term.handle_event(key_event(KeyCode::Enter)); + term.handle_event(key_event(KeyCode::Up)); + assert_eq!(term.input_buffer, "test"); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..742c827 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod agent; +pub mod builtin; +pub mod plugin; +pub mod runtime; +pub mod ui; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f9aa1a7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,414 @@ +use std::collections::HashMap; +use std::io::{self, stdout}; + +use anyhow::Result; +use crossterm::{ + event::{self, Event, KeyCode, KeyModifiers}, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, +}; +use ratatui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout, Rect}, + style::Color, + widgets::Widget, + Terminal, +}; + +use everjoy::builtin; +use everjoy::runtime::{ + Capability, Command, EventResult, ModuleRegistry, PanelType, ProviderDesc, Registrar, +}; +use everjoy::ui; + +enum AppState { + Splash, + Main, +} + +struct App { + state: AppState, + layout: ui::LayoutManager, + focus: ui::focus::FocusManager, + menubar: ui::menubar::Menubar, + statusbar: ui::statusbar::StatusBar, + splash: ui::splash::SplashScreen, + registry: ModuleRegistry, + /// Current active provider id per panel. + active_providers: HashMap, +} + +impl App { + fn new() -> Self { + let mut registry = ModuleRegistry::default(); + + // Register built-in providers + registry.register_provider( + ProviderDesc { + id: "filetree".into(), + name: "File Tree".into(), + panel: PanelType::Sidebar, + capabilities: vec![Capability::Render, Capability::HandleEvent], + }, + Box::new(|| Box::new(builtin::filetree::FileTreeProvider::new())), + ); + registry.register_provider( + ProviderDesc { + id: "editor".into(), + name: "Editor".into(), + panel: PanelType::Main, + capabilities: vec![Capability::Render, Capability::HandleEvent], + }, + Box::new(|| Box::new(builtin::editor::EditorProvider::new())), + ); + registry.register_provider( + ProviderDesc { + id: "terminal".into(), + name: "Terminal".into(), + panel: PanelType::Main, + capabilities: vec![Capability::Render, Capability::HandleEvent], + }, + Box::new(|| Box::new(builtin::terminal::TerminalProvider::new())), + ); + registry.register_provider( + ProviderDesc { + id: "agentchat".into(), + name: "Agent Chat".into(), + panel: PanelType::Agent, + capabilities: vec![Capability::Render, Capability::HandleEvent], + }, + Box::new(|| Box::new(builtin::agentchat::AgentChatProvider::new())), + ); + + // Default active providers + let mut active_providers: HashMap = HashMap::new(); + active_providers.insert(PanelType::Sidebar, "filetree".into()); + active_providers.insert(PanelType::Main, "editor".into()); + active_providers.insert(PanelType::Agent, "agentchat".into()); + + // Ensure all default providers are instantiated + for id in active_providers.values() { + let _ = registry.ensure_instance(id); + } + + Self { + state: AppState::Splash, + layout: ui::LayoutManager::default(), + focus: ui::focus::FocusManager::default(), + menubar: ui::menubar::Menubar::default(), + statusbar: ui::statusbar::StatusBar::default(), + splash: ui::splash::SplashScreen, + registry, + active_providers, + } + } + + fn run(&mut self, terminal: &mut Terminal>) -> Result<()> { + loop { + self.update_statusbar(); + terminal.draw(|frame| self.render(frame.area(), frame.buffer_mut()))?; + + if event::poll(std::time::Duration::from_millis(16))? { + let ev = event::read()?; + if self.handle_event(ev) { + break; + } + } + } + Ok(()) + } + + fn update_statusbar(&mut self) { + // Pull state from the active Main provider if it's an editor + if let Some(id) = self.active_providers.get(&PanelType::Main) { + if let Some(inst) = self.registry.ensure_instance(id) { + if let Some(editor) = inst.as_any().downcast_ref::() { + self.statusbar.filename = editor.filename().to_string(); + self.statusbar.cursor_line = editor.cursor_line(); + self.statusbar.cursor_col = editor.cursor_col(); + if editor.is_modified() { + self.statusbar.filename.push_str(" [+]"); + } + } + } + } + + // Focus info + self.statusbar.focused_panel = match self.focus.panel { + PanelType::Sidebar => "Sidebar".into(), + PanelType::Main => "Main".into(), + PanelType::Agent => "Agent".into(), + }; + } + + fn render(&mut self, area: Rect, buf: &mut ratatui::buffer::Buffer) { + match self.state { + AppState::Splash => { + self.splash.render(area, buf); + } + AppState::Main => { + self.render_main(area, buf); + } + } + } + + fn render_main(&mut self, area: Rect, buf: &mut ratatui::buffer::Buffer) { + let vertical = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Min(0), + Constraint::Length(1), + ]) + .split(area); + + let menubar_area = vertical[0]; + let body_area = vertical[1]; + let status_area = vertical[2]; + + self.menubar.render(menubar_area, buf); + + let panels = self.layout.compute(body_area); + + let sidebar_title = match self.focus.panel { + PanelType::Sidebar => " Sidebar * ", + _ => " Sidebar ", + }; + let main_title = match self.focus.panel { + PanelType::Main => " Main * ", + _ => " Main ", + }; + let agent_title = match self.focus.panel { + PanelType::Agent => " Agent * ", + _ => " Agent ", + }; + + self.draw_panel_frame(panels.sidebar, buf, sidebar_title); + self.draw_panel_frame(panels.main, buf, main_title); + self.draw_panel_frame(panels.agent, buf, agent_title); + + let sidebar_inner = inner_rect(panels.sidebar); + let main_inner = inner_rect(panels.main); + let agent_inner = inner_rect(panels.agent); + + // Fill panel backgrounds + for inner in [sidebar_inner, main_inner, agent_inner] { + for y in inner.top()..inner.bottom() { + for x in inner.left()..inner.right() { + buf[(x, y)].set_bg(Color::Rgb(25, 25, 25)); + } + } + } + + self.render_provider(PanelType::Sidebar, sidebar_inner, buf); + self.render_provider(PanelType::Main, main_inner, buf); + self.render_provider(PanelType::Agent, agent_inner, buf); + + self.statusbar.render(status_area, buf); + } + + fn render_provider(&mut self, panel: PanelType, area: Rect, buf: &mut ratatui::buffer::Buffer) { + if let Some(id) = self.active_providers.get(&panel) { + if let Some(inst) = self.registry.ensure_instance(id) { + inst.render(area, buf); + } + } + } + + fn draw_panel_frame(&self, area: Rect, buf: &mut ratatui::buffer::Buffer, title: &str) { + use ratatui::style::{Color, Style}; + use ratatui::widgets::{Block, Borders}; + + let block = Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)); + block.render(area, buf); + } + + fn handle_event(&mut self, ev: Event) -> bool { + if let Event::Key(key) = &ev { + if key.code == KeyCode::Char('q') && key.modifiers.contains(KeyModifiers::CONTROL) { + return true; + } + } + + match self.state { + AppState::Splash => { + if let Event::Key(_) = ev { + self.state = AppState::Main; + } + false + } + AppState::Main => self.handle_main_event(ev), + } + } + + fn handle_main_event(&mut self, ev: Event) -> bool { + // Menubar + if self.menubar.is_active() { + if self.menubar.handle_event(&ev) { + return false; + } + } else if let Event::Key(key) = &ev { + if key.modifiers.contains(KeyModifiers::ALT) { + if self.menubar.handle_event(&ev) { + return false; + } + } + } + + // Focus shortcuts + if let Event::Key(key) = &ev { + if key.modifiers.contains(KeyModifiers::CONTROL) { + match key.code { + KeyCode::Char('j') => { + self.set_focus(PanelType::Sidebar); + return false; + } + KeyCode::Char('k') => { + self.set_focus(PanelType::Main); + return false; + } + KeyCode::Char('l') => { + self.set_focus(PanelType::Agent); + return false; + } + _ => {} + } + } + } + + // Route to focused provider + let focused = self.focus.panel; + let commands = if let Some(id) = self.active_providers.get(&focused) { + if let Some(inst) = self.registry.ensure_instance(id) { + let (result, cmds) = inst.handle_event(ev.clone()); + if matches!(result, EventResult::PassThrough) { + if let Event::Key(key) = &ev { + if key.modifiers.contains(KeyModifiers::ALT) { + let _ = self.menubar.handle_event(&ev); + } + } + } + cmds + } else { + Vec::new() + } + } else { + Vec::new() + }; + + for cmd in commands { + self.dispatch_command(cmd); + } + + false + } + + fn set_focus(&mut self, panel: PanelType) { + let old = self.focus.panel; + if old == panel { + return; + } + self.focus.focus_panel(panel); + + // Notify old provider + if let Some(id) = self.active_providers.get(&old) { + if let Some(inst) = self.registry.ensure_instance(id) { + inst.focus_changed(false); + } + } + // Notify new provider + if let Some(id) = self.active_providers.get(&panel) { + if let Some(inst) = self.registry.ensure_instance(id) { + inst.focus_changed(true); + } + } + } + + fn dispatch_command(&mut self, cmd: Command) { + match cmd { + Command::Present { + panel, + source, + mime_hint, + } => { + let guessed = source.mime_hint(); + let mime_str = mime_hint.as_deref().or(guessed.as_deref()); + if let Some((id, _score)) = + self.registry.find_best_provider(panel, &source, mime_str) + { + // Hide current provider on this panel if different + if let Some(old_id) = self.active_providers.get(&panel) { + if old_id != &id { + if let Some(inst) = self.registry.ensure_instance(old_id) { + inst.hide(); + } + } + } + self.active_providers.insert(panel, id.clone()); + if let Some(inst) = self.registry.ensure_instance(&id) { + inst.show(); + if self.focus.panel == panel { + inst.focus_changed(true); + } + } + } + } + Command::Launch { panel, tool_id, .. } => { + // Direct dispatch by provider id + if let Some(old_id) = self.active_providers.get(&panel) { + if old_id != &tool_id { + if let Some(inst) = self.registry.ensure_instance(old_id) { + inst.hide(); + } + } + } + self.active_providers.insert(panel, tool_id.clone()); + if let Some(inst) = self.registry.ensure_instance(&tool_id) { + inst.show(); + if self.focus.panel == panel { + inst.focus_changed(true); + } + } + } + Command::FocusPanel(panel) => { + self.set_focus(panel); + } + Command::ShowMessage { msg, level } => { + let color = match level { + everjoy::runtime::MessageLevel::Info => Color::Cyan, + everjoy::runtime::MessageLevel::Warn => Color::Yellow, + everjoy::runtime::MessageLevel::Error => Color::Red, + }; + self.statusbar.message = Some((msg, color)); + } + } + } +} + +fn inner_rect(area: Rect) -> Rect { + Rect { + x: area.x.saturating_add(1), + y: area.y.saturating_add(1), + width: area.width.saturating_sub(2), + height: area.height.saturating_sub(2), + } +} + +fn main() -> Result<()> { + enable_raw_mode()?; + stdout().execute(EnterAlternateScreen)?; + let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; + + let mut app = App::new(); + let result = app.run(&mut terminal); + + // Shutdown all providers before exit + app.registry.shutdown_all(); + + disable_raw_mode()?; + stdout().execute(LeaveAlternateScreen)?; + result +} diff --git a/src/plugin/loader.rs b/src/plugin/loader.rs new file mode 100644 index 0000000..511fb26 --- /dev/null +++ b/src/plugin/loader.rs @@ -0,0 +1,22 @@ +use std::path::{Path, PathBuf}; + +/// Loads external plugins (.wasm, .so, .dll). +pub struct ExtensionLoader { + plugin_dir: PathBuf, +} + +impl ExtensionLoader { + pub fn new(plugin_dir: PathBuf) -> Self { + Self { plugin_dir } + } + + pub fn scan_and_load(&self) -> anyhow::Result<()> { + // TODO: scan plugin_dir, load each file + Ok(()) + } + + fn load(&self, _path: &Path) -> anyhow::Result<()> { + // TODO: detect format, dispatch to wasm/dylib/static loader + Ok(()) + } +} diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs new file mode 100644 index 0000000..7728b4b --- /dev/null +++ b/src/plugin/mod.rs @@ -0,0 +1,4 @@ +pub mod loader; +pub mod panic; +pub mod surface; +pub mod translator; diff --git a/src/plugin/panic.rs b/src/plugin/panic.rs new file mode 100644 index 0000000..f2b167b --- /dev/null +++ b/src/plugin/panic.rs @@ -0,0 +1,14 @@ +/// Isolates panics from external plugins so they don't crash the runtime. +pub fn catch_plugin_panic(plugin_id: &str, f: F) -> Option +where + F: FnOnce() -> R + std::panic::UnwindSafe, +{ + match std::panic::catch_unwind(f) { + Ok(result) => Some(result), + Err(_) => { + eprintln!("Plugin {} panicked! Unloading.", plugin_id); + // TODO: unload plugin, render fallback + None + } + } +} diff --git a/src/plugin/surface.rs b/src/plugin/surface.rs new file mode 100644 index 0000000..d266c05 --- /dev/null +++ b/src/plugin/surface.rs @@ -0,0 +1,35 @@ +/// Cell grid for external plugin rendering. +/// Runtime allocates, plugin borrows during render() call. +#[repr(C)] +pub struct EjSurface { + pub width: u16, + pub height: u16, + pub cells: *mut EjCell, +} + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct EjCell { + pub ch: u32, + pub style: EjStyleId, +} + +pub type EjStyleId = u32; + +impl EjSurface { + pub fn new(width: u16, height: u16) -> Self { + let count = (width * height) as usize; + let cells = vec![EjCell { ch: 0, style: 0 }; count].into_boxed_slice(); + let ptr = Box::into_raw(cells) as *mut EjCell; + Self { width, height, cells: ptr } + } +} + +impl Drop for EjSurface { + fn drop(&mut self) { + unsafe { + let count = (self.width * self.height) as usize; + let _ = Vec::from_raw_parts(self.cells, count, count); + } + } +} diff --git a/src/plugin/translator.rs b/src/plugin/translator.rs new file mode 100644 index 0000000..181dde8 --- /dev/null +++ b/src/plugin/translator.rs @@ -0,0 +1,13 @@ +/// Translates C-ABI calls to Rust trait objects. +/// Wraps external plugins into Runtime-compatible types. +pub struct Translator; + +impl Translator { + pub fn new() -> Self { + Self + } + + // TODO: wrap EjProviderVTable into Box + // TODO: wrap EjTool into Box + // TODO: cell grid → Buffer blitting +} diff --git a/src/runtime/ctx.rs b/src/runtime/ctx.rs new file mode 100644 index 0000000..9c9f75f --- /dev/null +++ b/src/runtime/ctx.rs @@ -0,0 +1,9 @@ +//! ProviderCtx has been removed in favor of the Command pattern. +//! Providers return Commands from handle_event() instead of calling +//! into the system via a context trait. + +/// Shared services available to all providers. +pub trait RuntimeServices { + // TODO: fn config(&self) -> &Config; + // TODO: fn db(&self) -> &DbHandle; +} diff --git a/src/runtime/lifecycle.rs b/src/runtime/lifecycle.rs new file mode 100644 index 0000000..d4dd0da --- /dev/null +++ b/src/runtime/lifecycle.rs @@ -0,0 +1,12 @@ +/// Manages provider lifecycle: init, hide, show, shutdown. +pub struct LifecycleManager; + +impl LifecycleManager { + pub fn new() -> Self { + Self + } + + // TODO: track active provider instances per panel + // TODO: handle keep_alive_on_hide semantics + // TODO: atomic unload (shutdown all providers from a plugin) +} diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs new file mode 100644 index 0000000..87c92e8 --- /dev/null +++ b/src/runtime/mod.rs @@ -0,0 +1,9 @@ +pub mod ctx; +pub mod lifecycle; +pub mod registry; +pub mod traits; + +pub use ctx::RuntimeServices; +pub use lifecycle::LifecycleManager; +pub use registry::{ModuleRegistry, RegisteredProvider}; +pub use traits::*; diff --git a/src/runtime/registry.rs b/src/runtime/registry.rs new file mode 100644 index 0000000..36c32b4 --- /dev/null +++ b/src/runtime/registry.rs @@ -0,0 +1,134 @@ +use std::collections::HashMap; + +use ratatui::{buffer::Buffer, layout::Rect}; +use super::traits::*; + +/// Central registry for all module contributions. +pub struct ModuleRegistry { + pub panel_providers: HashMap, + pub tools: HashMap>, + pub menu_items: Vec>, + pub status_widgets: Vec>, +} + +pub struct RegisteredProvider { + pub desc: ProviderDesc, + pub factory: ProviderFactory, + pub instance: Option>, +} + +impl Default for ModuleRegistry { + fn default() -> Self { + Self { + panel_providers: HashMap::new(), + tools: HashMap::new(), + menu_items: Vec::new(), + status_widgets: Vec::new(), + } + } +} + +impl ModuleRegistry { + /// Get all providers registered for a given panel type. + pub fn providers_for_panel(&self, panel: PanelType) -> Vec<&RegisteredProvider> { + self.panel_providers + .values() + .filter(|rp| rp.desc.panel == panel) + .collect() + } + + /// Get a mutable registered provider by id. + pub fn provider_mut(&mut self, id: &str) -> Option<&mut RegisteredProvider> { + self.panel_providers.get_mut(id) + } + + /// Instantiate a provider by id if not already created. + pub fn ensure_instance(&mut self, id: &str) -> Option<&mut Box> { + let rp = self.panel_providers.get_mut(id)?; + if rp.instance.is_none() { + let factory = std::mem::replace(&mut rp.factory, Box::new(|| panic!("factory consumed"))); + let mut instance = (factory)(); + instance.init(); + rp.instance = Some(instance); + } + rp.instance.as_mut() + } + + /// Find the best provider for a source on a given panel. + /// Returns (provider_id, score). Score 0 means no match. + pub fn find_best_provider( + &self, + panel: PanelType, + source: &Source, + mime_hint: Option<&str>, + ) -> Option<(String, i32)> { + let mut best: Option<(String, i32)> = None; + for rp in self.providers_for_panel(panel) { + let score = if let Some(inst) = &rp.instance { + inst.can_open(source, mime_hint) + } else { + // We can't call can_open without an instance. + // Fallback: use a static heuristic based on desc/capabilities. + // For now, assume all registered providers on this panel + // might handle generic sources at a low score. + // A real implementation could store a static scorer. + 0 + }; + if score > 0 { + if best.as_ref().map(|(_, s)| score > *s).unwrap_or(true) { + best = Some((rp.desc.id.clone(), score)); + } + } + } + best + } + + /// Shutdown all provider instances. + pub fn shutdown_all(&mut self) { + for rp in self.panel_providers.values_mut() { + if let Some(mut inst) = rp.instance.take() { + inst.shutdown(); + } + } + } +} + +impl Registrar for ModuleRegistry { + fn register_provider( + &mut self, + desc: ProviderDesc, + factory: ProviderFactory, + ) { + let id = desc.id.clone(); + self.panel_providers.insert( + id, + RegisteredProvider { + desc, + factory, + instance: None, + }, + ); + } + + fn register_tool(&mut self, desc: ToolDesc, handler: Box) { + self.tools.insert(desc.name.clone(), handler); + } + + fn register_menu_item( + &mut self, + desc: MenuItemDesc, + handler: Box, + ) { + // TODO: wrap handler into a MenuItem trait object + let _ = (desc, handler); + } + + fn register_status_widget( + &mut self, + desc: StatusWidgetDesc, + render: Box, + ) { + // TODO: wrap render into a StatusWidget trait object + let _ = (desc, render); + } +} diff --git a/src/runtime/traits.rs b/src/runtime/traits.rs new file mode 100644 index 0000000..60f5bb4 --- /dev/null +++ b/src/runtime/traits.rs @@ -0,0 +1,333 @@ +use ratatui::{buffer::Buffer, layout::Rect}; +use std::collections::HashMap; +use std::path::PathBuf; + +// --------------------------------------------------------------------------- +// Plugin +// --------------------------------------------------------------------------- + +/// A plugin registers its contributions during bootstrap. +pub trait Plugin: Send { + fn bootstrap(registrar: &mut dyn Registrar); +} + +// --------------------------------------------------------------------------- +// Source — what is being presented/launched +// --------------------------------------------------------------------------- + +/// Where the content comes from. +#[derive(Clone, Debug)] +pub enum Source { + /// Local file or directory. + File { path: PathBuf }, + + /// Network resource. + Url { url: String }, + + /// In-memory blob. + Memory { name: String, data: Vec }, +} + +impl Source { + /// Guess MIME type from path extension or content. + pub fn mime_hint(&self) -> Option { + match self { + Source::File { path } => { + let ext = path.extension()?.to_str()?; + match ext { + "rs" => Some("text/rust".into()), + "toml" => Some("text/toml".into()), + "md" => Some("text/markdown".into()), + "json" => Some("application/json".into()), + "sh" | "bash" => Some("text/x-shellscript".into()), + "py" => Some("text/x-python".into()), + "js" => Some("text/javascript".into()), + "ts" => Some("text/typescript".into()), + "txt" => Some("text/plain".into()), + _ => Some("text/plain".into()), + } + } + Source::Url { url } => { + if url.ends_with(".md") { + Some("text/markdown".into()) + } else { + None + } + } + Source::Memory { name, .. } => { + if name.ends_with(".json") { + Some("application/json".into()) + } else { + Some("text/plain".into()) + } + } + } + } +} + +// --------------------------------------------------------------------------- +// Command — what a Provider wants the system to do +// --------------------------------------------------------------------------- + +/// Actions a Provider can request from the system. +#[derive(Clone, Debug)] +pub enum Command { + /// Present content in a panel. The system asks providers on that panel + /// who can handle this source, and picks the highest scorer. + Present { + panel: PanelType, + source: Source, + mime_hint: Option, + }, + + /// Launch a tool/session in a panel by id. Not a bid — direct dispatch. + Launch { + panel: PanelType, + tool_id: String, + params: serde_json::Value, + }, + + /// Switch focus to a panel. + FocusPanel(PanelType), + + /// Show a transient message in the status bar. + ShowMessage { msg: String, level: MessageLevel }, +} + +// --------------------------------------------------------------------------- +// PanelProvider +// --------------------------------------------------------------------------- + +/// Renders content into a panel (Sidebar, Main, or Agent). +pub trait PanelProvider: Send { + /// Downcast to Any for runtime type checking. + fn as_any(&self) -> &dyn std::any::Any { + panic!("as_any not implemented") + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + panic!("as_any_mut not implemented") + } + + /// Unique id of this provider instance. + fn id(&self) -> &str; + + /// Called once after the provider instance is created. + fn init(&mut self) {} + + /// Called every frame to render content into the given area. + fn render(&mut self, area: Rect, buf: &mut Buffer); + + /// Called when this provider's panel has focus and receives input. + /// Returns handled-or-not plus any commands to dispatch. + fn handle_event(&mut self, event: crossterm::event::Event) -> (EventResult, Vec); + + /// Called when focus enters or leaves this provider's panel. + fn focus_changed(&mut self, focused: bool); + + /// Called when the provider is about to be hidden (but not destroyed). + fn hide(&mut self); + + /// Called when the provider becomes visible again. + fn show(&mut self); + + /// Called before the provider instance is destroyed. + fn shutdown(&mut self); + + /// Score how well this provider can handle the given source. + /// Return 0 = cannot handle. Higher = better match. + fn can_open(&self, _source: &Source, _mime_hint: Option<&str>) -> i32 { + 0 + } + + /// Whether to keep this provider alive when hidden. + fn keep_alive_on_hide(&self) -> bool { + false + } +} + +// --------------------------------------------------------------------------- +// Tool +// --------------------------------------------------------------------------- + +/// Callable by Agents via LLM function calling. +pub trait Tool: Send { + fn name(&self) -> &str; + fn description(&self) -> &str; + fn schema(&self) -> ToolSchema; + fn execute(&self, input: ToolInput) -> ToolOutput; +} + +// --------------------------------------------------------------------------- +// Menubar / StatusWidget +// --------------------------------------------------------------------------- + +/// Menubar entry. +pub trait MenuItem: Send { + fn id(&self) -> &str; + fn label(&self) -> &str; + fn parent(&self) -> Option<&str>; + fn shortcut(&self) -> Option<&str>; + fn invoke(&self); +} + +/// Status bar widget. +pub trait StatusWidget: Send { + fn id(&self) -> &str; + fn position(&self) -> StatusPosition; + fn render(&self, area: Rect, buf: &mut Buffer); +} + +// --------------------------------------------------------------------------- +// Registrar +// --------------------------------------------------------------------------- + +/// Registration interface exposed during Plugin::bootstrap. +pub trait Registrar { + fn register_provider( + &mut self, + desc: ProviderDesc, + factory: ProviderFactory, + ); + + fn register_tool(&mut self, desc: ToolDesc, handler: Box); + + fn register_menu_item( + &mut self, + desc: MenuItemDesc, + handler: Box, + ); + + fn register_status_widget( + &mut self, + desc: StatusWidgetDesc, + render: Box, + ); +} + +// --------------------------------------------------------------------------- +// Supporting types +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EventResult { + /// Event was handled, stop propagation. + Consumed, + /// Event was not handled, pass to next handler. + PassThrough, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum PanelType { + Sidebar, + Main, + Agent, +} + +#[derive(Clone, Debug)] +pub struct ProviderDesc { + pub id: String, + pub name: String, + pub panel: PanelType, + pub capabilities: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Capability { + Render, + HandleEvent, + HandleDrag, + AsyncRender, +} + +pub type ProviderFactory = + Box Box + Send>; + +#[derive(Clone, Debug)] +pub struct ToolDesc { + pub name: String, + pub description: String, + pub parameters: Vec, +} + +#[derive(Clone, Debug)] +pub struct ToolParameter { + pub name: String, + pub param_type: ParamType, + pub required: bool, + pub description: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ParamType { + String, + Number, + Boolean, + Enum(Vec), +} + +#[derive(Clone, Debug)] +pub struct ToolInput { + pub args: HashMap, +} + +#[derive(Clone, Debug)] +pub struct ToolOutput { + pub success: bool, + pub content: String, + pub metadata: Option, +} + +impl ToolOutput { + pub fn ok(content: impl Into) -> Self { + Self { + success: true, + content: content.into(), + metadata: None, + } + } + + pub fn err(content: impl Into) -> Self { + Self { + success: false, + content: content.into(), + metadata: None, + } + } +} + +#[derive(Clone, Debug)] +pub struct ToolSchema { + // TODO: JSON Schema for LLM function calling +} + +#[derive(Clone, Debug)] +pub struct MenuItemDesc { + pub id: String, + pub label: String, + pub parent: Option, + pub shortcut: Option, + pub order: i32, +} + +#[derive(Clone, Debug)] +pub struct StatusWidgetDesc { + pub id: String, + pub position: StatusPosition, + pub min_width: u16, + pub order: i32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StatusPosition { + Left, + Right, + Center, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MessageLevel { + Info, + Warn, + Error, +} diff --git a/src/ui/focus.rs b/src/ui/focus.rs new file mode 100644 index 0000000..4d0cd59 --- /dev/null +++ b/src/ui/focus.rs @@ -0,0 +1,33 @@ +use crate::runtime::PanelType; + +/// Manages focus state across panels and widgets. +pub struct FocusManager { + pub panel: PanelType, + pub menubar_active: bool, +} + +impl Default for FocusManager { + fn default() -> Self { + Self { + panel: PanelType::Main, + menubar_active: false, + } + } +} + +impl FocusManager { + pub fn focus_panel(&mut self, panel: PanelType) { + self.panel = panel; + self.menubar_active = false; + } + + pub fn focus_menubar(&mut self) { + self.menubar_active = true; + } + + pub fn is_menubar_focused(&self) -> bool { + self.menubar_active + } + + // TODO: handle Tab cycling within panel widgets +} diff --git a/src/ui/layout.rs b/src/ui/layout.rs new file mode 100644 index 0000000..65ba985 --- /dev/null +++ b/src/ui/layout.rs @@ -0,0 +1,46 @@ +use ratatui::layout::{Constraint, Direction, Layout, Rect}; + +/// Manages panel proportions and handles drag-to-resize. +pub struct LayoutManager { + sidebar_ratio: f32, // 0.0 ~ 1.0 + agent_ratio: f32, // 0.0 ~ 1.0 +} + +impl Default for LayoutManager { + fn default() -> Self { + Self { + sidebar_ratio: 0.20, + agent_ratio: 0.28, + } + } +} + +impl LayoutManager { + pub fn compute(&self, area: Rect) -> Panels { + let main_ratio = 1.0 - self.sidebar_ratio - self.agent_ratio; + + let horizontal = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((self.sidebar_ratio * 100.0) as u16), + Constraint::Percentage((main_ratio * 100.0) as u16), + Constraint::Percentage((self.agent_ratio * 100.0) as u16), + ]) + .split(area); + + Panels { + sidebar: horizontal[0], + main: horizontal[1], + agent: horizontal[2], + } + } + + // TODO: persist ratios to config + // TODO: handle mouse drag on splitters +} + +pub struct Panels { + pub sidebar: Rect, + pub main: Rect, + pub agent: Rect, +} diff --git a/src/ui/menubar.rs b/src/ui/menubar.rs new file mode 100644 index 0000000..d09cd43 --- /dev/null +++ b/src/ui/menubar.rs @@ -0,0 +1,186 @@ +use crossterm::event::{Event, KeyCode, KeyModifiers}; +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Paragraph, Widget}, +}; + +/// Renders the top menubar with Alt+ shortcuts and tracks active state. +pub struct Menubar { + items: Vec, + active_index: Option, + active_submenu: Option, +} + +struct MenuEntry { + label: String, + shortcut: char, + submenu: Vec, +} + +impl Default for Menubar { + fn default() -> Self { + Self { + items: vec![ + MenuEntry { + label: "File".into(), + shortcut: 'F', + submenu: vec!["New File".into(), "Open...".into(), "Save".into(), "Save All".into(), "Exit".into()], + }, + MenuEntry { + label: "Edit".into(), + shortcut: 'E', + submenu: vec!["Undo".into(), "Redo".into(), "Cut".into(), "Copy".into(), "Paste".into()], + }, + MenuEntry { + label: "View".into(), + shortcut: 'V', + submenu: vec!["Sidebar".into(), "Terminal".into(), "Zoom In".into(), "Zoom Out".into()], + }, + MenuEntry { + label: "Agent".into(), + shortcut: 'A', + submenu: vec!["Run".into(), "Stop".into(), "Settings...".into()], + }, + MenuEntry { + label: "Help".into(), + shortcut: 'H', + submenu: vec!["Documentation".into(), "Shortcuts".into(), "About".into()], + }, + ], + active_index: None, + active_submenu: None, + } + } +} + +impl Menubar { + /// Handle menubar-specific key events. Returns true if consumed. + pub fn handle_event(&mut self, event: &Event) -> bool { + if let Event::Key(key) = event { + // Alt+letter to open menu + if key.modifiers.contains(KeyModifiers::ALT) { + if let KeyCode::Char(c) = key.code { + let upper = c.to_ascii_uppercase(); + if let Some(idx) = self.items.iter().position(|e| e.shortcut == upper) { + self.active_index = Some(idx); + self.active_submenu = Some(0); + return true; + } + } + } + + // Menu navigation when active + if self.active_index.is_some() { + match key.code { + KeyCode::Esc => { + self.active_index = None; + self.active_submenu = None; + return true; + } + KeyCode::Left => { + if let Some(idx) = self.active_index { + self.active_index = Some(idx.saturating_sub(1)); + self.active_submenu = Some(0); + } + return true; + } + KeyCode::Right => { + if let Some(idx) = self.active_index { + self.active_index = Some((idx + 1).min(self.items.len() - 1)); + self.active_submenu = Some(0); + } + return true; + } + KeyCode::Up => { + if let Some(sub) = self.active_submenu { + self.active_submenu = Some(sub.saturating_sub(1)); + } + return true; + } + KeyCode::Down => { + if let Some(idx) = self.active_index { + let max = self.items[idx].submenu.len().saturating_sub(1); + if let Some(sub) = self.active_submenu { + self.active_submenu = Some((sub + 1).min(max)); + } + } + return true; + } + KeyCode::Enter => { + // TODO: invoke menu action + self.active_index = None; + self.active_submenu = None; + return true; + } + _ => {} + } + } + } + false + } + + pub fn is_active(&self) -> bool { + self.active_index.is_some() + } + + pub fn deactivate(&mut self) { + self.active_index = None; + self.active_submenu = None; + } +} + +impl Widget for &Menubar { + fn render(self, area: Rect, buf: &mut Buffer) { + let spans: Vec = self + .items + .iter() + .enumerate() + .flat_map(|(i, entry)| { + let is_active = self.active_index == Some(i); + let mut chars = entry.label.chars(); + let first = chars.next().unwrap(); + + let first_style = if is_active { + Style::default() + .fg(Color::Black) + .bg(Color::White) + .add_modifier(Modifier::UNDERLINED) + .add_modifier(Modifier::BOLD) + } else { + Style::default() + .fg(Color::Rgb(204, 204, 204)) + .add_modifier(Modifier::UNDERLINED) + .add_modifier(Modifier::BOLD) + }; + let rest_style = if is_active { + Style::default() + .fg(Color::Black) + .bg(Color::White) + } else { + Style::default().fg(Color::Rgb(204, 204, 204)) + }; + + let parts = vec![ + Span::raw(" "), + Span::styled(first.to_string(), first_style), + Span::styled(chars.as_str().to_string(), rest_style), + ]; + + // Render submenu if active + if is_active { + // We can't render a dropdown in the same line as the menubar + // For now just highlight the active item + } + + parts + }) + .collect(); + + Paragraph::new(Line::from(spans)) + .style(Style::default().bg(Color::Rgb(45, 45, 45))) + .render(area, buf); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..e6c99c8 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,9 @@ +pub mod focus; +pub mod layout; +pub mod menubar; +pub mod router; +pub mod scheduler; +pub mod splash; +pub mod statusbar; + +pub use layout::LayoutManager; diff --git a/src/ui/router.rs b/src/ui/router.rs new file mode 100644 index 0000000..a37b02f --- /dev/null +++ b/src/ui/router.rs @@ -0,0 +1,20 @@ +use crossterm::event::Event; + +/// Routes input events through the 3-layer system: +/// 1. System emergency shortcuts +/// 2. Focused PanelProvider +/// 3. Global shortcuts / Menubar +pub struct EventRouter; + +impl EventRouter { + pub fn new() -> Self { + Self + } + + pub fn route(&mut self, event: Event) { + // TODO: Layer 1 — system shortcuts (Ctrl+C, Ctrl+Q) + // TODO: Layer 2 — focused provider + // TODO: Layer 3 — global shortcuts / menubar + let _ = event; + } +} diff --git a/src/ui/scheduler.rs b/src/ui/scheduler.rs new file mode 100644 index 0000000..42f42fa --- /dev/null +++ b/src/ui/scheduler.rs @@ -0,0 +1,30 @@ +use ratatui::{backend::Backend, Frame, Terminal}; + +/// Schedules and executes the per-frame render. +pub struct RenderScheduler; + +impl RenderScheduler { + pub fn new() -> Self { + Self + } + + pub fn render(&mut self, terminal: &mut Terminal) { + terminal + .draw(|frame| { + self.render_frame(frame); + }) + .expect("render failed"); + } + + fn render_frame(&self, frame: &mut Frame) { + let _area = frame.area(); + // TODO: + // 1. Draw splash if active + // 2. Otherwise draw main UI: + // a. Menubar + // b. Sidebar Provider + // c. Main Panel Provider + // d. Agent Panel Provider + // e. Status Bar + } +} diff --git a/src/ui/splash.rs b/src/ui/splash.rs new file mode 100644 index 0000000..38ee9c6 --- /dev/null +++ b/src/ui/splash.rs @@ -0,0 +1,78 @@ +use ratatui::{ + buffer::Buffer, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::Line, + widgets::{Paragraph, Widget}, +}; + +/// Splash screen shown on startup. +pub struct SplashScreen; + +impl Widget for &SplashScreen { + fn render(self, area: Rect, buf: &mut Buffer) { + // Background + for y in area.top()..area.bottom() { + for x in area.left()..area.right() { + buf[(x, y)].set_bg(Color::Rgb(20, 18, 15)); + } + } + + let vertical = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(20), + Constraint::Length(8), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Min(0), + ]) + .split(area); + + // ASCII art dog + let dog = Paragraph::new(vec![ + Line::from(" ████████ "), + Line::from(" ████░░░░░░████ "), + Line::from(" ████░░░░░░░░░░████ "), + Line::from(" ████░░░▓▓▓▓▓▓░░░████ "), + Line::from(" ████░░░▓░░░░░░▓░░░████ "), + Line::from(" ███░░░░▓░░▓▓░░▓░░░░███ "), + Line::from(" ███░░░░░▓░░░░░▓░░░░░███ "), + Line::from(" ███░░░░░▓▓▓▓▓▓░░░░░███ "), + ]) + .alignment(Alignment::Center) + .style(Style::default().fg(Color::Rgb(210, 170, 90))); + dog.render(vertical[1], buf); + + // Title + let title = Paragraph::new("EVERJOY") + .alignment(Alignment::Center) + .style( + Style::default() + .fg(Color::Rgb(210, 170, 90)) + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::UNDERLINED), + ); + title.render(vertical[2], buf); + + // Subtitle + let subtitle = Paragraph::new("An AI-native TUI workflow") + .alignment(Alignment::Center) + .style(Style::default().fg(Color::Rgb(150, 140, 130)).add_modifier(Modifier::ITALIC)); + subtitle.render(vertical[3], buf); + + // Version + let version = Paragraph::new("v0.1.0 — for Hoshu Chiu & Everjoy Chiu 🐕") + .alignment(Alignment::Center) + .style(Style::default().fg(Color::Rgb(120, 110, 100))); + version.render(vertical[4], buf); + + // Hint + let hint = Paragraph::new("◀ Press any key to start ▶") + .alignment(Alignment::Center) + .style(Style::default().fg(Color::Rgb(180, 180, 180)).add_modifier(Modifier::BOLD)); + hint.render(vertical[5], buf); + } +} diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs new file mode 100644 index 0000000..8a684ed --- /dev/null +++ b/src/ui/statusbar.rs @@ -0,0 +1,65 @@ +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Paragraph, Widget}, +}; + +/// Dynamic status bar showing editor state, focus, and agent status. +pub struct StatusBar { + pub filename: String, + pub file_type: String, + pub encoding: String, + pub cursor_line: usize, + pub cursor_col: usize, + pub agent_status: String, + pub focused_panel: String, + pub message: Option<(String, Color)>, +} + +impl Default for StatusBar { + fn default() -> Self { + Self { + filename: "main.rs".into(), + file_type: "Rust".into(), + encoding: "UTF-8".into(), + cursor_line: 1, + cursor_col: 1, + agent_status: "idle".into(), + focused_panel: "Main".into(), + message: None, + } + } +} + +impl Widget for &StatusBar { + fn render(self, area: Rect, buf: &mut Buffer) { + let left = format!( + " {} | {} | {} | Ln {}, Col {} ", + self.filename, self.file_type, self.encoding, self.cursor_line, self.cursor_col + ); + let right = format!(" [{}] | {} ", self.agent_status, self.focused_panel); + + let left_width = left.len() as u16; + let right_width = right.len() as u16; + let mid_width = area.width.saturating_sub(left_width + right_width); + + let mid = if let Some((msg, color)) = &self.message { + let padded = format!(" {:^width$} ", msg, width = mid_width as usize); + Span::styled(padded, Style::default().fg(*color).add_modifier(ratatui::style::Modifier::BOLD)) + } else { + Span::raw(" ".repeat(mid_width as usize)) + }; + + let line = Line::from(vec![ + Span::styled(left, Style::default().fg(Color::Rgb(204, 204, 204))), + mid, + Span::styled(right, Style::default().fg(Color::Rgb(204, 204, 204))), + ]); + + Paragraph::new(line) + .style(Style::default().bg(Color::Rgb(30, 60, 90))) + .render(area, buf); + } +}