dtui-3.0.0/.cargo_vcs_info.json0000644000000001610000000000100117770ustar { "git": { "sha1": "fb7e5425376335c7bc2c3933af400a75f5a3eb37", "dirty": true }, "path_in_vcs": "" }dtui-3.0.0/.config/config.json5000064400000000000000000000015411046102023000143420ustar 00000000000000{ "keybindings": { "All": { "q": "Quit", // Quit the application "": "Suspend", // Suspend the application "": "NextFocus", // Next focus "": "Down", "j": "Down", "": "Up", "k": "Up", "?": "Help", }, "Services": { "": "GetService" // Get service selected }, "Objects": { "": {"InvokeDbus": "CallMethod"}, "g": {"InvokeDbus": "GetProperty"}, "s": {"InvokeDbus": "SetProperty"}, "": "DownTree", "l": "DownTree", "": "UpTree", "h": "UpTree", }, "Call": { "": "CallActiveMethod", // Call the ongoing method in the call view "i": {"EditorMode": "Insert"}, "": {"EditorMode": "Normal"}, }, "Help":{ "": {"Focus": "Services"} } } }dtui-3.0.0/.gitignore000064400000000000000000000000101046102023000125500ustar 00000000000000/target dtui-3.0.0/Cargo.lock0000644000002000210000000000100077470ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.60.2", ] [[package]] name = "arraydeque" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-channel" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "pin-project-lite", "slab", ] [[package]] name = "async-io" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ "autocfg", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix 1.1.2", "slab", "windows-sys 0.61.1", ] [[package]] name = "async-lock" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ "event-listener", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-process" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", "event-listener", "futures-lite", "rustix 1.1.2", ] [[package]] name = "async-recursion" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "async-signal" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", "rustix 1.1.2", "signal-hook-registry", "slab", "windows-sys 0.61.1", ] [[package]] name = "async-task" version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[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 2.0.106", ] [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-link", ] [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" dependencies = [ "serde", ] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "blocking" version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", "async-task", "futures-io", "futures-lite", "piper", ] [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[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.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chumsky" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" dependencies = [ "hashbrown 0.14.5", "stacker", ] [[package]] name = "clap" version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "clap_lex" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "color-eyre" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" dependencies = [ "backtrace", "color-spantrace", "eyre", "indenter", "once_cell", "owo-colors", "tracing-error", ] [[package]] name = "color-spantrace" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" dependencies = [ "once_cell", "owo-colors", "tracing-core", "tracing-error", ] [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "compact_str" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" dependencies = [ "castaway", "cfg-if", "itoa", "rustversion", "ryu", "static_assertions", ] [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "config" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" dependencies = [ "async-trait", "convert_case", "json5", "nom", "pathdiff", "ron", "rust-ini", "serde", "serde_json", "toml", "yaml-rust2", ] [[package]] name = "const-random" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" dependencies = [ "const-random-macro", ] [[package]] name = "const-random-macro" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ "getrandom 0.2.16", "once_cell", "tiny-keccak", ] [[package]] name = "convert_case" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" dependencies = [ "unicode-segmentation", ] [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "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", "futures-core", "mio", "parking_lot", "rustix 0.38.44", "serde", "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 = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "darling" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", "syn 2.0.106", ] [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", "syn 2.0.106", ] [[package]] name = "derive_deref" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcdbcee2d9941369faba772587a565f4f534e42cb8d17e5295871de730163b2b" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "directories" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.61.1", ] [[package]] name = "dlv-list" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" dependencies = [ "const-random", ] [[package]] name = "dtui" version = "3.0.0" dependencies = [ "async-recursion", "chumsky", "clap", "color-eyre", "config", "crossterm", "derive_deref", "directories", "futures", "itertools 0.11.0", "json5", "lazy_static", "libc", "ratatui", "serde", "signal-hook", "strip-ansi-escapes", "strum 0.27.2", "tokio", "tokio-util", "tracing", "tracing-error", "tracing-journald", "tracing-subscriber", "tui-textarea", "tui-tree-widget", "tui-widget-list", "zbus", "zbus_names", "zbus_xml", ] [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[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 = "endi" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enumflags2" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[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.1", ] [[package]] name = "event-listener" version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", ] [[package]] name = "eyre" version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" [[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 = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", "wasi 0.14.7+wasi-0.2.4", ] [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", ] [[package]] name = "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.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "hashlink" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ "hashbrown 0.14.5", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indenter" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown 0.16.0", ] [[package]] name = "indoc" version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "instability" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" dependencies = [ "darling", "indoc", "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "io-uring" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ "bitflags", "cfg-if", "libc", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[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.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "json5" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" dependencies = [ "pest", "pest_derive", "serde", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libredox" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", ] [[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.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "lock_api" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[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 = "matchers" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ "regex-automata", ] [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags", "cfg-if", "cfg_aliases", "libc", "memoffset", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "nu-ansi-term" version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "object" version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-multimap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", "hashbrown 0.14.5", ] [[package]] name = "ordered-stream" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "owo-colors" version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pest" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" dependencies = [ "pest", "pest_generator", ] [[package]] name = "pest_generator" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "pest_meta" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" dependencies = [ "pest", "sha2", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", "fastrand", "futures-io", ] [[package]] name = "polling" version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix 1.1.2", "windows-sys 0.61.1", ] [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit 0.23.6", ] [[package]] name = "proc-macro2" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "psm" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" dependencies = [ "cc", ] [[package]] name = "quick-xml" version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", "serde", ] [[package]] name = "quote" version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 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 = "ratatui" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ "bitflags", "cassowary", "compact_str", "crossterm", "indoc", "instability", "itertools 0.13.0", "lru", "paste", "strum 0.26.3", "unicode-segmentation", "unicode-truncate", "unicode-width 0.2.0", ] [[package]] name = "redox_syscall" version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] name = "redox_users" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", "thiserror", ] [[package]] name = "regex-automata" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "ron" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64", "bitflags", "serde", "serde_derive", ] [[package]] name = "rust-ini" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" dependencies = [ "cfg-if", "ordered-multimap", ] [[package]] name = "rustc-demangle" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys 0.11.0", "windows-sys 0.61.1", ] [[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.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_bytes" version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", "serde_core", ] [[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 2.0.106", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "serde_repr" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "serde_spanned" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[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.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "stacker" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" dependencies = [ "cc", "cfg-if", "libc", "psm", "windows-sys 0.59.0", ] [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strip-ansi-escapes" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" dependencies = [ "vte", ] [[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 0.26.4", ] [[package]] name = "strum" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros 0.27.2", ] [[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 2.0.106", ] [[package]] name = "strum_macros" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix 1.1.2", "windows-sys 0.61.1", ] [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "thread_local" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", ] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", ] [[package]] name = "tokio" version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "slab", "socket2", "tokio-macros", "tracing", "windows-sys 0.59.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "tokio-util" version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" 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 0.6.11", "toml_edit 0.22.27", ] [[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_datetime" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" dependencies = [ "serde_core", ] [[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 0.6.11", "toml_write", "winnow", ] [[package]] name = "toml_edit" version = "0.23.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" dependencies = [ "indexmap", "toml_datetime 0.7.2", "toml_parser", "winnow", ] [[package]] name = "toml_parser" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" dependencies = [ "winnow", ] [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "tracing-core" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-error" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" dependencies = [ "tracing", "tracing-subscriber", ] [[package]] name = "tracing-journald" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0b4143302cf1022dac868d521e36e8b27691f72c84b3311750d5188ebba657" dependencies = [ "libc", "tracing-core", "tracing-subscriber", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-serde" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" dependencies = [ "serde", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex-automata", "serde", "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", "tracing-serde", ] [[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 = "tui-tree-widget" version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c14c4488e071617f5b5922222193cdf6725835e492c6229557af85d3c1a4e903" dependencies = [ "ratatui", "unicode-width 0.2.0", ] [[package]] name = "tui-widget-list" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a417c8ac65119a0e5b8627d0ce492731b95affb29ec8602d9e72646bda5ecf6" dependencies = [ "ratatui", ] [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "ucd-trie" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uds_windows" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ "memoffset", "tempfile", "winapi", ] [[package]] name = "unicode-ident" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-truncate" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools 0.13.0", "unicode-segmentation", "unicode-width 0.1.14", ] [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vte" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" dependencies = [ "memchr", ] [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ "wasip2", ] [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[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.4", ] [[package]] name = "windows-sys" version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" dependencies = [ "windows-link", ] [[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.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" dependencies = [ "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "yaml-rust2" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" dependencies = [ "arraydeque", "encoding_rs", "hashlink", ] [[package]] name = "zbus" version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" dependencies = [ "async-broadcast", "async-executor", "async-io", "async-lock", "async-process", "async-recursion", "async-task", "async-trait", "blocking", "enumflags2", "event-listener", "futures-core", "futures-lite", "hex", "nix", "ordered-stream", "serde", "serde_repr", "tokio", "tracing", "uds_windows", "windows-sys 0.60.2", "winnow", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.106", "zbus_names", "zvariant", "zvariant_utils", ] [[package]] name = "zbus_names" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", "winnow", "zvariant", ] [[package]] name = "zbus_xml" version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29" dependencies = [ "quick-xml", "serde", "static_assertions", "zbus_names", "zvariant", ] [[package]] name = "zerocopy" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "zvariant" version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" dependencies = [ "endi", "enumflags2", "serde", "serde_bytes", "winnow", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.106", "zvariant_utils", ] [[package]] name = "zvariant_utils" version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" dependencies = [ "proc-macro2", "quote", "serde", "syn 2.0.106", "winnow", ] dtui-3.0.0/Cargo.toml0000644000000052670000000000100100110ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" rust-version = "1.88" name = "dtui" version = "3.0.0" authors = ["Troels Hoffmeyer "] build = false exclude = [ ".github/*", "images/*", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false default-run = "dtui" description = "dBus TUI for introspecting your current dbus session/system" readme = "README.md" keywords = [ "tui", "dbus", ] categories = ["command-line-utilities"] license = "MIT" repository = "https://github.com/Troels51/dtui" [[bin]] name = "dtui" path = "src/bin/dtui/main.rs" [[example]] name = "tree" path = "examples/tree/main.rs" [dependencies.async-recursion] version = "1.1.1" [dependencies.chumsky] version = "0.9.3" [dependencies.clap] version = "4.5.48" features = ["derive"] [dependencies.color-eyre] version = "0.6.5" [dependencies.config] version = "0.14.1" [dependencies.crossterm] version = "0.28.1" features = [ "serde", "event-stream", ] [dependencies.derive_deref] version = "1.1.1" [dependencies.directories] version = "6.0.0" [dependencies.futures] version = "0.3.31" [dependencies.itertools] version = "0.11.0" [dependencies.json5] version = "0.4.1" [dependencies.lazy_static] version = "1.5.0" [dependencies.libc] version = "0.2.176" [dependencies.ratatui] version = "0.29" features = ["macros"] [dependencies.serde] version = "1.0.228" features = ["derive"] [dependencies.signal-hook] version = "0.3.18" [dependencies.strip-ansi-escapes] version = "0.2.1" [dependencies.strum] version = "0.27.2" features = ["derive"] [dependencies.tokio] version = "1.47" features = ["full"] [dependencies.tokio-util] version = "0.7.16" [dependencies.tracing] version = "0.1.41" [dependencies.tracing-error] version = "0.2.1" [dependencies.tracing-journald] version = "0.3.1" [dependencies.tracing-subscriber] version = "0.3.20" features = [ "env-filter", "json", "fmt", ] [dependencies.tui-textarea] version = "0.7" [dependencies.tui-tree-widget] version = "0.23" [dependencies.tui-widget-list] version = "0.13.2" [dependencies.zbus] version = "5.11.0" features = [ "tokio", "serde_bytes", ] [dependencies.zbus_names] version = "4.2.0" [dependencies.zbus_xml] version = "5.0.2" dtui-3.0.0/Cargo.toml.orig000064400000000000000000000025741046102023000134700ustar 00000000000000[package] name = "dtui" version = "3.0.0" edition = "2024" default-run = "dtui" authors = ["Troels Hoffmeyer "] description = "dBus TUI for introspecting your current dbus session/system" license = "MIT" repository = "https://github.com/Troels51/dtui" rust-version = "1.88" keywords = ["tui", "dbus"] categories = ["command-line-utilities"] exclude = [ ".github/*", "images/*", ] [dependencies] tui-tree-widget = "0.23" crossterm = { version = "0.28.1", features = ["serde", "event-stream"] } tokio = { version = "1.47", features = ["full"] } async-recursion = "1.1.1" itertools = "0.11.0" clap = { version = "4.5.48", features = ["derive"] } ratatui = { version = "0.29", features = ["macros"] } tracing-error = "0.2.1" tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json", "fmt"] } tracing-journald = "0.3.1" tui-textarea = "0.7" chumsky = "0.9.3" zbus = {version = "5.11.0", features = ["tokio", "serde_bytes"]} zbus_names = "4.2.0" zbus_xml = "5.0.2" color-eyre = "0.6.5" serde = { version = "1.0.228", features = ["derive"] } strum = { version = "0.27.2", features = ["derive"] } libc = "0.2.176" tokio-util = "0.7.16" derive_deref = "1.1.1" lazy_static = "1.5.0" directories = "6.0.0" json5 = "0.4.1" config = "0.14.1" futures = "0.3.31" signal-hook = "0.3.18" strip-ansi-escapes = "0.2.1" tui-widget-list = "0.13.2" dtui-3.0.0/LICENSE000064400000000000000000000020611046102023000115750ustar 00000000000000MIT License Copyright (c) 2024 Troels Hoffmeyer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dtui-3.0.0/README.md000064400000000000000000000106011046102023000120460ustar 00000000000000# dtui [![build](https://github.com/Troels51/dtui/actions/workflows/build.yml/badge.svg)](https://github.com/Troels51/dtui/actions/workflows/build.yml) ![Crates.io Version](https://img.shields.io/crates/v/dtui?link=https%3A%2F%2Fcrates.io%2Fcrates%2Fdtui) A small TUI for d-termining the state of your dbus. It will show you the current services running and allow you to introspect objects and their interfaces in those services ![Example](/images/dtui.png) ## Build ### From Source To build install Rust and cargo, then run build ```sh cargo build ``` To run from cargo ```sh cargo run --bin dtui ``` ## Basic Usage ### Views & Navigation There are 4 views, Services, Objects, Results and Call view. - The Service view will show the services avaliable on your bus. - The Object view will show object on the service selected. - The Result view will show results from communicating with Dbus, i.e method calls or properties. - The Call view will show the current active Method call or Property Set, with each of the arguments for a call being a text input. There is a general navigation, and view specicic navigation ### General navigation | Key | Action | Description | | :--- | :--- | :--- | | Arrows/h j k l | **Move Cursor** | Navigate menus, lists, and tables. | | Tab | **Next view** | Switch focus to next view. | | ? | **Help** | Show this help. | | q | **Quit** | Quit application. | --- ### Services View navigation | Key | Action | Description | | :--- | :--- | :--- | | Enter | **Select service** | Select service and get its objects. | --- ### Objects View navigation | Key | Action | Description | | :--- | :--- | :--- | | Right/Left / h/j | **Expand/Collapse** | Expand/collapse a node. | | Enter | **Call method / Get property** | Call a Method, or Get a property that is selected. | | s | **Set property** | Set a property that is selected. | | g | **Get property** | Get a property that is selected. | --- ### Call View navigation | Key | Action | Description | | :--- | :--- | :--- | | i | **Enter insert mode** | Enter "Insert mode" which will allow you to insert text into the text fields. | | Esc | **Enter normal mode** | Enter normal mode, allowing normal interactions such as navigation or calling the method. | | Enter | **Call active** | Call the active method or set property based on the argument inputs given. | --- ## Parsing The parsing for the arguments in the Call view are based on the DBus type signature from the argument in the method/property, you can see that type in the bottom of the text field. The argument text field will turn green when the text can be parsed. | D-Bus Type | Input Format | Separator | Example | | :--- | :--- | :--- | :--- | | **Basic Types** | **Value** (e.g., `5`, `true`, `"path"`) | N/A | `5` for `i`, `true` for `b` | | **Array** (`a...`) | Elements enclosed by **`[` and `]`** | Comma (`,`) | `["first", "second"]` | | **Structure** (`(...)`) | Elements enclosed by **`(` and `)`** | Comma (`,`) | `("text", 123)` for `(su)` | | **Dictionary** (`a{...}`) | Key-value pairs enclosed by **`{` and `}`** | Key/Value: Colon (`:`)
Pairs: Comma (`,`) | `{"key": "value", "key2": "value2"}` | | **Variant** (`v`) | **Signature** followed by `->` and **Value** | `->` | `"u"->5` (means `u32` with value `5`) | ----- ### Basic Types | Signature | Example Input | D-Bus Value | | :--- | :--- | :--- | | `y` (byte/u8) | `255` | 255 | | `i` (integer/i32) | `-42` | -42 | | `d` (double/f64) | `3.1415` | 3.1415 | | `b` (boolean) | `true` or `false` | True or False | | `s` (string) | `"Hello World"` | "Hello World" | | `o` (object path) | `"/org/example/path"` | ObjectPath | | `g` (signature) | `"as"` | Signature value | ----- ### Complex Type Examples | Signature | Example Input | Breakdown | | :--- | :--- | :--- | | `a{si}` | `{"count": 5, "max": 10}` | A dictionary (array of entries) where keys are **string** (`s`) and values are **integer** (`i`). | | `(ssu)` | `("user_name", "john", 42)` | A structure with three elements: **string**, **string**, **u32**. | | `a(is)` | `[(1, "a"), (2, "b")]` | An array of structures, where each structure has an **integer** and a **string**. | | `v` | `"as"->["one", "two"]` | A variant containing an array of strings (`as`) with the values `["one", "two"]`. | **Note:** File Descriptors (`h`) cannot be parsed from the input string and will result in an error. --- ### ArchLinux [AUR dtui package](https://aur.archlinux.org/packages/dtui) dtui-3.0.0/TODO.md000064400000000000000000000010731046102023000116610ustar 00000000000000# TODO ## Features Result view prettier, or bigger (✓) Maybe make a line for service everytime you change service Maybe only show the result, but make it expandable if you want to see other things Property set, use Call view for this ✓ Show errors ✓ Show insert mode ✓ Blink when Call view is not used correctly. Do timer ✓ Help view. Write a good help Documentation for format Signals Show read/writeable annotations ## Refactorings Error handling and reporting update vs update_from_dbus https://github.com/achristmascarl/rainfrogdtui-3.0.0/examples/tree/main.rs000064400000000000000000000054471046102023000146520ustar 00000000000000use std::io::BufReader; use async_recursion::async_recursion; use zbus::fdo::DBusProxy; use zbus::names::OwnedBusName; use zbus::zvariant::ObjectPath; use zbus::{Connection, Result}; use zbus_xml::Node; #[async_recursion] async fn print_all_interfaces( connection: &Connection, service: &OwnedBusName, path: ObjectPath<'async_recursion>, indent: usize, ) -> std::result::Result<(), zbus::Error> { println!("{:indent$}{} ", "", path.as_str(), indent = indent); let introspectable_proxy = zbus::fdo::IntrospectableProxy::builder(connection) .destination(service)? .path(path.clone())? .build() .await?; let introspect_xml = introspectable_proxy.introspect().await?; let introspect = Node::from_reader(BufReader::new(introspect_xml.as_bytes())).unwrap(); println!("{:indent$}Interfaces: ", "", indent = indent + 4); for interface in introspect.interfaces() { println!("{:indent$}{} ", "", interface.name(), indent = indent + 8); println!("{:indent$}Methods: ", "", indent = indent + 12); for method in interface.methods() { println!("{:indent$}{} ", "", method.name(), indent = indent + 16); } println!("{:indent$}Signals: ", "", indent = indent + 12); for signal in interface.signals() { println!("{:indent$}{} ", "", signal.name(), indent = indent + 16); } println!("{:indent$}Properties: ", "", indent = indent + 12); for property in interface.properties() { println!("{:indent$}{} ", "", property.name(), indent = indent + 16); } println!("{:indent$}Annotations: ", "", indent = indent + 12); for annotation in interface.annotations() { println!("{:indent$}{} ", "", annotation.name(), indent = indent + 16); } } for node in introspect.nodes() { let node_name = node.name().unwrap(); let path_name = if path.as_str().ends_with('/') { path.as_str().to_string() + node_name } else { path.as_str().to_string() + "/" + node_name }; let sub_path = ObjectPath::try_from(path_name)?; print_all_interfaces(connection, service, sub_path, indent).await?; } Ok(()) } #[tokio::main] async fn main() -> Result<()> { let connection = Connection::session().await?; let dbusproxy = DBusProxy::new(&connection).await?; let reply = dbusproxy.list_names().await?; let _service = reply.first().unwrap(); for service in reply { if service.as_str().contains(':') { continue; } println!("Service: {}", service.as_str()); let path_name = "/".to_string(); let path = ObjectPath::try_from(path_name)?; print_all_interfaces(&connection, &service, path, 4).await?; } Ok(()) } dtui-3.0.0/src/bin/dtui/TODO000064400000000000000000000003621046102023000135660ustar 00000000000000TODO: Move into a component system Events should be handled in the components, which then sends events? Move the popup into a component underneath the objects view Implement properties gets/sets Show properties in the component underneathdtui-3.0.0/src/bin/dtui/action.rs000064400000000000000000000050371046102023000147250ustar 00000000000000use serde::{Deserialize, Serialize}; use strum::Display; use zbus_names::OwnedBusName; use crate::{ app::Focus, stateful_tree::{OwnedMethod, OwnedProperty}, }; #[derive(Default, Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize, Copy)] pub enum EditorMode { #[default] Normal, Insert, } #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize, Copy)] pub enum DbusInvocationAction { CallMethod, GetProperty, SetProperty, SubscribeSignal, } #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] pub enum Action { Tick, Render, Resize(u16, u16), Suspend, Resume, Quit, ClearScreen, Error(String), Help, NextFocus, Focus(Focus), EditorMode(EditorMode), Up, Down, DownTree, // Go further into an object tree UpTree, // Go up an object tree GetService, InvokeDbus(DbusInvocationAction), // Call method, get property, set property, listen to signal. TODO: Rename StartDbusInvocation(Invocation), // Call view CallActiveMethod, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Invocation { pub(crate) service: OwnedBusName, pub(crate) object: zbus::zvariant::OwnedObjectPath, pub(crate) interface: zbus_names::OwnedInterfaceName, pub(crate) invocation_description: InvokableDbusMember, } impl Invocation { pub(crate) fn nr_args(&self) -> usize { match &self.invocation_description { InvokableDbusMember::Method { method } => method.args().len(), InvokableDbusMember::Property { .. } => 1, InvokableDbusMember::Signal { .. } => 1, } } pub(crate) fn input_count(&self) -> usize { match &self.invocation_description { InvokableDbusMember::Method { method } => method .args() .iter() .filter(|arg| match arg.direction() { Some(direction) => match direction { zbus_xml::ArgDirection::In => true, zbus_xml::ArgDirection::Out => false, }, None => false, }) .count(), InvokableDbusMember::Property { .. } => 1, InvokableDbusMember::Signal { .. } => 1, } } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum InvokableDbusMember { Method { method: OwnedMethod }, Property { property: OwnedProperty }, Signal { name: zbus_names::OwnedMemberName }, } dtui-3.0.0/src/bin/dtui/app.rs000064400000000000000000000353651046102023000142370ustar 00000000000000use color_eyre::Result; use crossterm::event::KeyEvent; use ratatui::{ layout::{Constraint, Direction, Flex, Layout}, prelude::Rect, }; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::info; use zbus::{Connection, conn}; use crate::{ Args, BusType, action::{Action, EditorMode}, components::{Component, Components}, config::Config, dbus_handler::DbusActorHandle, messages::AppMessage, tui::{Event, Tui}, }; pub struct App { config: Config, tick_rate: f64, frame_rate: f64, components: Components, should_quit: bool, should_suspend: bool, focus: Focus, last_tick_key_events: Vec, action_tx: mpsc::UnboundedSender, action_rx: mpsc::UnboundedReceiver, dbus_handler: DbusActorHandle, dbus_receiver: mpsc::UnboundedReceiver, editor_mode: crate::action::EditorMode, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Focus { #[default] Services, Objects, Call, Help, All, // For keybindings or interactions that are always active } impl Focus { fn next(&self) -> Focus { match self { Focus::Services => Focus::Objects, Focus::Objects => Focus::Call, Focus::Call => Focus::Services, Focus::All => Focus::Services, Focus::Help => Focus::Services, } } } impl App { pub async fn new(tick_rate: f64, frame_rate: f64, args: Args) -> Result { let (action_tx, action_rx) = mpsc::unbounded_channel(); // Create Dbus actor let mut connection = match args.bus { BusType::System => Connection::system().await?, BusType::Session => Connection::session().await?, }; if let Some(address) = args.address { connection = conn::Builder::address(address.as_str())?.build().await?; } let (dbus_handler_sender, dbus_receiver) = mpsc::unbounded_channel(); let dbus_handler = DbusActorHandle::new(dbus_handler_sender, connection); Ok(Self { tick_rate, frame_rate, components: Components::new(), should_quit: false, should_suspend: false, config: Config::new()?, focus: Focus::Services, last_tick_key_events: Vec::new(), action_tx, action_rx, dbus_handler, dbus_receiver, editor_mode: EditorMode::Normal, }) } pub async fn run(&mut self) -> Result<()> { let mut tui = Tui::new()? // .mouse(true) // uncomment this line to enable mouse support .tick_rate(self.tick_rate) .frame_rate(self.frame_rate); tui.enter()?; self.components .register_action_handler(self.action_tx.clone())?; self.components .register_dbus_actor_handler(self.dbus_handler.clone())?; self.components .register_config_handler(self.config.clone())?; self.components.init(tui.size()?)?; let _ = self.action_tx.send(Action::Focus(self.focus)); let action_tx = self.action_tx.clone(); self.dbus_handler.request_services().await; loop { self.handle_events(&mut tui).await?; self.handle_actions(&mut tui).await?; self.handle_dbus_actions(&mut tui)?; if self.should_suspend { tui.suspend()?; action_tx.send(Action::Resume)?; action_tx.send(Action::ClearScreen)?; // tui.mouse(true); tui.enter()?; } else if self.should_quit { tui.stop()?; break; } } tui.exit()?; Ok(()) } async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> { let Some(event) = tui.next_event().await else { return Ok(()); }; let action_tx = self.action_tx.clone(); match event { Event::Quit => action_tx.send(Action::Quit)?, Event::Tick => action_tx.send(Action::Tick)?, Event::Render => action_tx.send(Action::Render)?, Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?, Event::Key(key) => self.handle_key_event(key)?, _ => {} } if let Some(action) = self .components .service_view .handle_events(Some(event.clone()))? { action_tx.send(action)?; } if let Some(action) = self .components .object_view .handle_events(Some(event.clone()))? { action_tx.send(action)?; } if let Some(action) = self .components .results_view .handle_events(Some(event.clone()))? { action_tx.send(action)?; } if let Some(action) = self .components .call_view .handle_events(Some(event.clone()))? { action_tx.send(action)?; } if let Some(action) = self .components .bottom_text .handle_events(Some(event.clone()))? { action_tx.send(action)?; } if let Some(action) = self .components .help_view .handle_events(Some(event.clone()))? { action_tx.send(action)?; } Ok(()) } fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> { let action_tx = self.action_tx.clone(); let Some(generic_keymap) = self.config.keybindings.get(&Focus::All) else { return Ok(()); }; let Some(focus_keymap) = self.config.keybindings.get(&self.focus) else { return Ok(()); }; if self.editor_mode == EditorMode::Insert { if let Some(action) = focus_keymap.get(&vec![key]) && let Action::EditorMode(..) = action { action_tx.send(action.clone())? } return Ok(()); } for keymap in [generic_keymap, focus_keymap] { match keymap.get(&vec![key]) { Some(action) => { info!("Got action: {action:?}"); action_tx.send(action.clone())?; } _ => { // If the key was not handled as a single key action, // then consider it for multi-key combinations. self.last_tick_key_events.push(key); // Check for multi-key combinations if let Some(action) = keymap.get(&self.last_tick_key_events) { info!("Multi-key action: {action:?}"); action_tx.send(action.clone())?; } } } } let _ = self.components.handle_key_event(key); Ok(()) } async fn handle_actions(&mut self, tui: &mut Tui) -> Result<()> { while let Ok(action) = self.action_rx.try_recv() { match action { Action::Tick => { self.last_tick_key_events.drain(..); } Action::Quit => self.should_quit = true, Action::Suspend => self.should_suspend = true, Action::Resume => self.should_suspend = false, Action::ClearScreen => tui.terminal.clear()?, Action::Resize(w, h) => self.handle_resize(tui, w, h)?, Action::Render => self.render(tui)?, Action::NextFocus => { let next_focus = self.focus.next(); info!("next focus {:?}", next_focus); let _ = self.action_tx.send(Action::Focus(next_focus)); } Action::Focus(focus) => { info!("Setting focus {:?}", focus); self.focus = focus; } Action::EditorMode(mode) => { // It only makes sense to change the editor mode when the call view is in focus if self.focus == Focus::Call { self.editor_mode = mode; } } Action::Help => { info!("Help requested"); let _ = self.action_tx.send(Action::Focus(Focus::Help)); } _ => {} } if let Some(action) = self.components.service_view.update(action.clone()).await? { self.action_tx.send(action)? }; if let Some(action) = self.components.object_view.update(action.clone()).await? { self.action_tx.send(action)? }; if let Some(action) = self.components.results_view.update(action.clone()).await? { self.action_tx.send(action)? }; if let Some(action) = self.components.call_view.update(action.clone()).await? { self.action_tx.send(action)? }; if let Some(action) = self.components.bottom_text.update(action.clone()).await? { self.action_tx.send(action)? }; if let Some(action) = self.components.help_view.update(action.clone()).await? { self.action_tx.send(action)? }; } Ok(()) } fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> { tui.resize(Rect::new(0, 0, w, h))?; self.render(tui)?; Ok(()) } fn render(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { /* +----------------+----------------+ | | | | | | | | | | | | | Services | Objects | | | | | | | | | | | | | +----------------+----------------+ | | | | | | | Result log | Call | | | | | | | +---------------------------------+ | Bottom help text | +---------------------------------+ */ let vertical_split = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(5), Constraint::Min(5), Constraint::Max(2)]) .split(frame.area()); let service_object_split = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(25), Constraint::Percentage(75)].as_ref()) .split(vertical_split[0]); let result_call_split = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(vertical_split[1]); if let Err(err) = self .components .service_view .draw(frame, service_object_split[0]) { let _ = self .action_tx .send(Action::Error(format!("Failed to draw: {:?}", err))); } if let Err(err) = self .components .object_view .draw(frame, service_object_split[1]) { let _ = self .action_tx .send(Action::Error(format!("Failed to draw: {:?}", err))); } if let Err(err) = self .components .results_view .draw(frame, result_call_split[0]) { let _ = self .action_tx .send(Action::Error(format!("Failed to draw: {:?}", err))); } if let Err(err) = self.components.call_view.draw(frame, result_call_split[1]) { let _ = self .action_tx .send(Action::Error(format!("Failed to draw: {:?}", err))); } if let Err(err) = self.components.bottom_text.draw(frame, vertical_split[2]) { let _ = self .action_tx .send(Action::Error(format!("Failed to draw: {:?}", err))); } // We show the Help view overlaid on the other components as a popup if self.focus == Focus::Help { let pop_up = centered_area(frame.area(), 75, 75); if let Err(err) = self.components.help_view.draw(frame, pop_up) { let _ = self .action_tx .send(Action::Error(format!("Failed to draw: {:?}", err))); } } })?; Ok(()) } fn handle_dbus_actions(&mut self, _tui: &mut Tui) -> Result<()> { while let Ok(action) = self.dbus_receiver.try_recv() { if let Some(action) = self .components .service_view .update_from_dbus(action.clone())? { self.action_tx.send(action)? }; if let Some(action) = self .components .object_view .update_from_dbus(action.clone())? { self.action_tx.send(action)? }; if let Some(action) = self .components .bottom_text .update_from_dbus(action.clone())? { self.action_tx.send(action)? }; if let Some(action) = self .components .results_view .update_from_dbus(action.clone())? { self.action_tx.send(action)? }; if let Some(action) = self.components.call_view.update_from_dbus(action.clone())? { self.action_tx.send(action)? }; } Ok(()) } } fn centered_area(area: Rect, percent_y: u16, percent_x: u16) -> Rect { let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center); let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center); let [area] = vertical.areas(area); let [area] = horizontal.areas(area); area } dtui-3.0.0/src/bin/dtui/components/bottom_text.rs000064400000000000000000000100031046102023000201720ustar 00000000000000use color_eyre::Result; use ratatui::{prelude::*, widgets::*}; use tokio::sync::mpsc::UnboundedSender; use super::Component; use crate::{ action::{Action, DbusInvocationAction, EditorMode}, app::Focus, config::Config, }; #[derive(Default)] pub struct BottomText { command_tx: Option>, config: Config, help_text: String, } impl BottomText { pub fn new() -> Self { Self::default() } fn get_action_key(&self, focus: Focus, action: Action) -> String { self.config .keybindings .get_key_from_action(focus, action) .and_then(|key_events| key_events.first()) // This takes the first key possible, so won't show all possibilities .map_or("none".to_string(), |key_event| key_event.code.to_string()) } } impl Component for BottomText { fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.command_tx = Some(tx); Ok(()) } fn register_config_handler(&mut self, config: Config) -> Result<()> { self.config = config; Ok(()) } async fn update(&mut self, action: Action) -> Result> { if let Action::Focus(focus) = action { let next_focus_key = self.get_action_key(Focus::All, Action::NextFocus); let quit_key = self.get_action_key(Focus::All, Action::Quit); let normal_mode = self.get_action_key(Focus::Call, Action::EditorMode(EditorMode::Normal)); let insert_mode = self.get_action_key(Focus::Call, Action::EditorMode(EditorMode::Insert)); let help_key = self.get_action_key(Focus::All, Action::Help); self.help_text = match focus { crate::app::Focus::Services => { format!( "Change focus: {} | Navigation: ← ↓ ↑ → | Get Service: {} | Quit: {} | Help: {}", next_focus_key, self.get_action_key(focus, Action::GetService), quit_key, help_key ) } crate::app::Focus::Objects => { format!( "Change focus: {} | Navigation: ← ↓ ↑ → | Call method: {} | Get property {} | Set property {} | Quit: {} | Help: {}", next_focus_key, self.get_action_key( focus, Action::InvokeDbus(DbusInvocationAction::CallMethod) ), self.get_action_key( focus, Action::InvokeDbus(DbusInvocationAction::GetProperty) ), self.get_action_key( focus, Action::InvokeDbus(DbusInvocationAction::SetProperty) ), quit_key, help_key ) } crate::app::Focus::Call => { format!( "Change focus: {} | Navigation: ← ↓ ↑ → | Call Method: {} | Quit: {}, InsertMode: {}, NormalMode: {} | Help: {}", next_focus_key, self.get_action_key(focus, Action::CallActiveMethod), quit_key, insert_mode, normal_mode, help_key ) } crate::app::Focus::All => String::new(), crate::app::Focus::Help => String::new(), }; } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let bottom_text = Span::raw(self.help_text.clone()); let helper_paragraph = Paragraph::new(bottom_text).alignment(Alignment::Center); frame.render_widget(helper_paragraph, area); Ok(()) } } dtui-3.0.0/src/bin/dtui/components/call_view.rs000064400000000000000000000321721046102023000176020ustar 00000000000000use std::{ iter::repeat_n, str::FromStr, time::{Duration, SystemTime}, }; use chumsky::Parser; use color_eyre::Result; use ratatui::{prelude::*, widgets::*}; use tokio::sync::mpsc::UnboundedSender; use tracing::info; use tui_textarea::CursorMove; use super::Component; use crate::{ action::{Action, EditorMode, Invocation, InvokableDbusMember}, app::Focus, config::Config, dbus_handler::DbusActorHandle, messages::InvocationResponse, other::active_area_border_color, parser::get_parser, }; pub struct MethodArgVisual { pub text_area: tui_textarea::TextArea<'static>, pub parser: Box, Error = chumsky::error::Simple>>, pub is_input: bool, // Is this Arg an input or output } // Encapsulates the information about the ongoing call struct OngoingCallInfo { invocation: Invocation, method_arg_vis: Vec, selected: usize, } impl OngoingCallInfo { fn new(call: Invocation, _area: Size) -> Option { let mut call_info = OngoingCallInfo { invocation: call, method_arg_vis: vec![], selected: 0, }; match &call_info.invocation.invocation_description { InvokableDbusMember::Method { method } => { // First time init of text areas for arg in method.args().iter() { let mut text_area = tui_textarea::TextArea::default(); let inout: String = if let Some(direction) = arg.direction() { match direction { zbus_xml::ArgDirection::In => "input".to_string(), zbus_xml::ArgDirection::Out => "output".to_string(), } } else { "".to_string() }; text_area.set_cursor_line_style(Style::default()); text_area.set_cursor_style(Style::default()); text_area.set_block( Block::default() .borders(Borders::ALL) .title(format!("name: {} | {}", arg.name().unwrap(), inout)) .title_bottom(format!("type: {}", arg.ty().to_string())), ); let parser = get_parser( zbus::zvariant::Signature::from_str(arg.ty().to_string().as_str()) .expect("The type description for the method we got was not good"), ); let input = match arg.direction().unwrap_or(zbus_xml::ArgDirection::In) { zbus_xml::ArgDirection::In => true, zbus_xml::ArgDirection::Out => false, }; call_info.method_arg_vis.push(MethodArgVisual { text_area, parser: Box::new(parser), is_input: input, }); } Some(call_info) } InvokableDbusMember::Property { property } => { let mut text_area = tui_textarea::TextArea::default(); text_area.set_cursor_line_style(Style::default()); text_area.set_cursor_style(Style::default()); text_area.set_block( Block::default() .borders(Borders::ALL) .title(format!("name: {} | {}", property.name(), "input")) .title_bottom(format!("type: {}", property.ty().to_string())), ); let parser = Box::new(get_parser( zbus::zvariant::Signature::from_str(property.ty().to_string().as_str()) .expect("The type description for the method we got was not good"), )); call_info.method_arg_vis.push(MethodArgVisual { text_area, parser, is_input: true, }); Some(call_info) } InvokableDbusMember::Signal { .. } => todo!(), } } async fn call(&self, mut values: Vec, actor: &DbusActorHandle) { match &self.invocation.invocation_description { InvokableDbusMember::Method { method } => { actor .call_method( self.invocation.service.clone(), self.invocation.object.clone(), self.invocation.interface.clone(), method.name().clone(), values, ) .await } InvokableDbusMember::Property { property } => { actor .set_property( self.invocation.service.clone(), self.invocation.object.clone(), self.invocation.interface.clone(), property.name().clone(), values .pop() .expect("Properties can only have one value when setting"), ) .await } InvokableDbusMember::Signal { .. } => todo!(), } } } #[derive(Default)] pub struct CallView { command_tx: Option>, config: Config, active: bool, ongoing: Option, dbus_actor_handle: Option, editor_mode: EditorMode, area: Size, blink_start: Option, } impl CallView { pub fn new() -> Self { Self::default() } fn draw_inner(frame: &mut Frame<'_>, area: Rect, ongoing: &mut OngoingCallInfo, active: bool) { let nr_args = ongoing.invocation.nr_args(); let single_line_layout = Layout::vertical(repeat_n(Constraint::Length(3), nr_args).chain([Constraint::Min(1)])); let segments = single_line_layout.split(area); for (i, input) in ongoing.method_arg_vis.iter_mut().enumerate() { let emphasis = if i == ongoing.selected && active { let method_arg: String = input.text_area.lines()[0].clone(); let parsed = input.parser.parse(method_arg); match parsed { Ok(_) => Style::default().fg(Color::Green), Err(_) => Style::default().fg(Color::Red), } } else { Style::default() }; input.text_area.set_block( input .text_area .block() .unwrap() .clone() .border_style(emphasis), ); input.text_area.set_cursor_line_style(emphasis); frame.render_widget(&input.text_area, segments[i]); } } fn blink_error(&mut self) { info!("Blinking error"); self.blink_start = Some(SystemTime::now()); } } impl Component for CallView { fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.command_tx = Some(tx); Ok(()) } fn register_config_handler(&mut self, config: Config) -> Result<()> { self.config = config; Ok(()) } fn register_dbus_actor_handle(&mut self, dbus_actor_handle: DbusActorHandle) -> Result<()> { self.dbus_actor_handle = Some(dbus_actor_handle); Ok(()) } async fn update(&mut self, action: Action) -> Result> { if let Action::Focus(focus) = action { self.active = focus == Focus::Call; } if self.active { match action { Action::Down => { if let Some(ongoing) = &mut self.ongoing { let input_count = ongoing.invocation.input_count(); ongoing.selected = std::cmp::min(input_count.saturating_sub(1), ongoing.selected + 1); } } Action::Up => { if let Some(ongoing) = &mut self.ongoing { ongoing.selected = ongoing.selected.saturating_sub(1); } } Action::CallActiveMethod => { if let Some(ongoing) = &mut self.ongoing { info!("Calling active method"); let parses = ongoing .method_arg_vis .iter() .filter(|input| input.is_input) .map(|input| input.parser.parse(input.text_area.lines()[0].clone())); if parses.clone().all( |result: Result< zbus::zvariant::Value<'static>, Vec>, >| Result::is_ok(&result), ) { let values: Vec = parses .map(|value| { // We know that they are all Ok, so unwrap is fine here zbus::zvariant::OwnedValue::try_from(value.unwrap()).unwrap() }) .collect(); ongoing.call(values, self.dbus_actor_handle.as_ref().expect("Cannot call method without a dbus actor handle, init should be called first")).await; } else { // Alert user that call cannot be made if arguments cannot be parsed } } } Action::EditorMode(mode) => { self.editor_mode = mode; self.blink_start = None; } _ => (), } } // Handle irregardless of active if let Action::StartDbusInvocation(invocation) = action { self.ongoing = OngoingCallInfo::new(invocation, self.area); } Ok(None) } fn handle_key_event(&mut self, key: crossterm::event::KeyEvent) -> Result> { if self.editor_mode == EditorMode::Insert { // Ignore certain keys as they will just confuse match key.code { crossterm::event::KeyCode::Enter | crossterm::event::KeyCode::PageUp | crossterm::event::KeyCode::PageDown | crossterm::event::KeyCode::Tab | crossterm::event::KeyCode::BackTab | crossterm::event::KeyCode::Null | crossterm::event::KeyCode::Esc => { self.blink_error(); return Ok(None); } _ => (), } if let Some(ongoing) = &mut self.ongoing { ongoing.method_arg_vis[ongoing.selected] .text_area .input(key); } } Ok(None) } fn update_from_dbus( &mut self, dbus_action: crate::messages::AppMessage, ) -> Result> { if let crate::messages::AppMessage::InvocationResponse(InvocationResponse { method_name: _, message, .. }) = dbus_action && let Ok(value) = message.body().deserialize::() && let Some(ref mut ongoing) = self.ongoing { for (index, output_field) in ongoing .method_arg_vis .iter_mut() .filter(|field| !field.is_input) .enumerate() { output_field.text_area.move_cursor(CursorMove::Head); output_field.text_area.delete_line_by_end(); // The way to clear a text area output_field .text_area .insert_str(format!("{}", value.fields()[index])); } } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let titel_style = match self .blink_start .map(|time| time + Duration::from_millis(500) > SystemTime::now()) { Some(true) => Style::new().red(), _ => Style::new(), }; let view_titel = Span::from("Call - "); let mode_titel = Span::styled(format!("Mode: {}", self.editor_mode), titel_style); let block = Block::default() .borders(Borders::ALL) .title(vec![view_titel, mode_titel]) .border_type(BorderType::Rounded) .border_style(Style::default().fg(active_area_border_color(self.active))); let inner = block.inner(area); if let Some(ref mut ongoing) = self.ongoing { CallView::draw_inner(frame, inner, ongoing, self.active); } frame.render_widget(block, area); Ok(()) } fn init(&mut self, area: Size) -> Result<()> { self.area = area; Ok(()) } } dtui-3.0.0/src/bin/dtui/components/help_view.rs000064400000000000000000000473151046102023000176240ustar 00000000000000use color_eyre::Result; use ratatui::{prelude::*, widgets::*}; use tokio::sync::mpsc::UnboundedSender; use super::Component; use crate::{action::Action, config::Config}; #[derive(Default)] pub struct HelpView { command_tx: Option>, config: Config, help_text: Vec>, vertical_scroll: usize, } impl HelpView { pub fn new() -> Self { let mut new = Self::default(); new.help_text = build_help_text_lines(); new } fn scroll_up(&mut self) { self.vertical_scroll = self.vertical_scroll.saturating_sub(1); } fn scroll_down(&mut self) { self.vertical_scroll = self.vertical_scroll.saturating_add(1); } } impl Component for HelpView { fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.command_tx = Some(tx); Ok(()) } fn register_config_handler(&mut self, config: Config) -> Result<()> { self.config = config; Ok(()) } async fn update(&mut self, action: Action) -> Result> { match action { Action::Up => self.scroll_up(), Action::Down => self.scroll_down(), _ => (), } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { frame.render_widget(Clear, area); let helper_paragraph = Paragraph::new(self.help_text.clone()) .alignment(Alignment::Left) .scroll((self.vertical_scroll as u16, 0)); let popup_block = Block::bordered() .title("Help") .border_type(BorderType::Rounded); helper_paragraph.render(popup_block.inner(area), frame.buffer_mut()); let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) .begin_symbol(Some("↑")) .end_symbol(Some("↓")) .thumb_style(Color::LightBlue); let mut scrollbar_state = ScrollbarState::new(self.help_text.len()).position(self.vertical_scroll); frame.render_widget(popup_block, area); frame.render_stateful_widget( scrollbar, area.inner(Margin { vertical: 1, horizontal: 0, }), &mut scrollbar_state, ); Ok(()) } } /// Translates the markdown content into a vector of styled `ratatui::text::Line`. fn build_help_text_lines() -> Vec> { // Define styles for different elements to maintain consistency. let title_style = Style::new().bold().underlined(); let heading_style = Style::new().bold(); let key_style = Style::new(); let code_style = Style::new().bg(Color::Rgb(60, 60, 60)); let sig_style = Style::default(); let value_style = Style::default(); vec![ Line::from(Span::styled("Basic Usage", title_style)), Line::from(""), Line::from(Span::styled("Views & Navigation", heading_style)), Line::from("There are 4 views: Services, Objects, Results, and Call view."), Line::from("- The Service view will show the services available on your bus."), Line::from("- The Object view will show objects on the selected service."), Line::from("- The Result view shows results from D-Bus, like method calls or properties."), Line::from("- The Call view shows the active Method call or Property Set."), Line::from(""), // --- General Navigation Table --- Line::from(Span::styled("General Navigation", heading_style)), Line::from( "┌──────────────────┬──────────────────┬────────────────────────────────────────┐", ), Line::from( "│ Key │ Action │ Description │", ), Line::from( "├──────────────────┼──────────────────┼────────────────────────────────────────┤", ), Line::from(vec![ "│ ".into(), Span::styled("Arrows/h j k l", key_style), " │ ".into(), "Move Cursor".into(), " │ ".into(), "Navigate menus, lists, and tables.".into(), " │".into(), ]), Line::from(vec![ "│ ".into(), Span::styled("Tab", key_style), " │ ".into(), "Next view".into(), " │ ".into(), "Switch focus to the next view.".into(), " │".into(), ]), Line::from(vec![ "│ ".into(), Span::styled("?", key_style), " │ ".into(), "Help".into(), " │ ".into(), "Show this help screen.".into(), " │".into(), ]), Line::from(vec![ "│ ".into(), Span::styled("q", key_style), " │ ".into(), "Quit".into(), " │ ".into(), "Quit the application.".into(), " │".into(), ]), Line::from( "└──────────────────┴──────────────────┴────────────────────────────────────────┘", ), Line::from(""), // --- Services --- Line::from(Span::styled("Services View Navigation", heading_style)), Line::from( "┌──────────────────┬──────────────────┬────────────────────────────────────────┐", ), Line::from( "│ Key │ Action │ Description │", ), Line::from( "├──────────────────┼──────────────────┼────────────────────────────────────────┤", ), Line::from(vec![ "│ ".into(), Span::styled("Enter", key_style), " │ ".into(), "Select".into(), " │ ".into(), "Select a service and get its objects.".into(), " │".into(), ]), Line::from( "└──────────────────┴──────────────────┴────────────────────────────────────────┘", ), Line::from(""), // --- Services --- Line::from(Span::styled("Objects View Navigation", heading_style)), Line::from( "┌──────────────────┬──────────────────┬────────────────────────────────────────┐", ), Line::from( "│ Key │ Action │ Description │", ), Line::from( "├──────────────────┼──────────────────┼────────────────────────────────────────┤", ), Line::from(vec![ "│ ".into(), Span::styled("Right/Left or l/h", key_style), "│ ".into(), "Expand/Collapse".into(), " │ ".into(), "Expand/Collapse a node.".into(), " │".into(), ]), Line::from(vec![ "│ ".into(), Span::styled("Enter", key_style), " │ ".into(), "Call".into(), " │ ".into(), "Call a method or get a property.".into(), " │".into(), ]), Line::from(vec![ "│ ".into(), Span::styled("s", key_style), " │ ".into(), "Set Property".into(), " │ ".into(), "Set a selected property.".into(), " │".into(), ]), Line::from(vec![ "│ ".into(), Span::styled("g", key_style), " │ ".into(), "Get Property".into(), " │ ".into(), "Get a selected property.".into(), " │".into(), ]), Line::from( "└──────────────────┴──────────────────┴────────────────────────────────────────┘", ), Line::from(""), // --- Call view --- Line::from(Span::styled("Call View Navigation", heading_style)), Line::from( "┌──────────────────┬──────────────────┬────────────────────────────────────────┐", ), Line::from( "│ Key │ Action │ Description │", ), Line::from( "├──────────────────┼──────────────────┼────────────────────────────────────────┤", ), Line::from(vec![ "│ ".into(), Span::styled("i", key_style), " │ ".into(), "Insert mode".into(), " │ ".into(), "Insert mode for text fields".into(), " │".into(), ]), Line::from(vec![ "│ ".into(), Span::styled("Esc", key_style), " │ ".into(), "Normal Mode".into(), " │ ".into(), "Normal mode to navigate or call".into(), " │".into(), ]), Line::from(vec![ "│ ".into(), Span::styled("Enter", key_style), " │ ".into(), "Call".into(), " │ ".into(), "Call the method or set property.".into(), " │".into(), ]), Line::from( "└──────────────────┴──────────────────┴────────────────────────────────────────┘", ), Line::from(""), Line::from("──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────".dim()), Line::from(""), // --- Parsing Section --- Line::from(Span::styled("Parsing", title_style)), Line::from("Arguments in the Call view are parsed based on their D-Bus type signature."), Line::from("A text field turns green when the input is valid."), Line::from(""), Line::from(Span::styled("D-Bus Type Signatures", heading_style)), Line::from( "┌─────────────────┬──────────────────────────────────┬─────────────────────────────┐", ), Line::from( "│ D-Bus Type │ Input Format │ Example │", ), Line::from( "├─────────────────┼──────────────────────────────────┼─────────────────────────────┤", ), Line::from(vec![ "│ Basic Types │ Value (e.g., ".into(), Span::styled("5", code_style), ", ".into(), Span::styled("true", code_style), ") │ ".into(), Span::styled("5", code_style), " for ".into(), Span::styled("i", code_style), ", ".into(), Span::styled("true", code_style), " for ".into(), Span::styled("b", code_style), Span::raw(" │"), ]), Line::from(vec![ "│ Array ".into(), Span::styled("a...", code_style), " │ Elements in ".into(), Span::styled("[]", code_style), ", separated by ".into(), Span::styled(",", code_style), " │ ".into(), Span::styled("[\"a\", \"b\"]", code_style), Span::raw(" │"), ]), Line::from(vec![ "│ Struct ".into(), Span::styled("(...)", code_style), " │ Elements in ".into(), Span::styled("()", code_style), ", separated by ".into(), Span::styled(",", code_style), " │ ".into(), Span::styled("(\"x\", 1)", code_style), " for ".into(), Span::styled("(su)", code_style), Span::raw(" │"), ]), Line::from(vec![ "│ Dictionary ".into(), Span::styled("a{}", code_style), " │ Key-value pairs in ".into(), Span::styled("{}", code_style), " │ ".into(), Span::styled("{\"k\": \"v\"}", code_style), Span::raw(" │"), ]), Line::from(vec![ "│ Variant ".into(), Span::styled("v", code_style), " │ ".into(), Span::styled("sig->val", code_style), " │ ".into(), Span::styled("\"u\"->5", code_style), Span::raw(" (u32 with value 5) │"), ]), Line::from( "└─────────────────┴──────────────────────────────────┴─────────────────────────────┘", ), Line::from(""), // --- Basic types --- Line::from(Span::styled("D-Bus Type Signatures", heading_style)), Line::from( "┌─────────────────┬──────────────────────────────────┬─────────────────────────────┐", ), Line::from( "│ D-Bus Type │ Input Format │ Example │", ), Line::from( "├─────────────────┼──────────────────────────────────┼─────────────────────────────┤", ), Line::from(vec![ Span::raw("│ "), Span::styled("`y` byte/u8", sig_style), Span::raw(" │ "), Span::styled("`255`", value_style), Span::raw(" │ "), Span::styled("255", value_style), Span::raw(" │"), ]), Line::from(vec![ Span::raw("│ "), Span::styled("`i` integer/i32", sig_style), Span::raw(" │ "), Span::styled("`-42`", value_style), Span::raw(" │ "), Span::styled("-42", value_style), Span::raw(" │"), ]), Line::from(vec![ Span::raw("│ "), Span::styled("`d` double/f64", sig_style), Span::raw(" │ "), Span::styled("`3.1415`", value_style), Span::raw(" │ "), Span::styled("3.1415", value_style), Span::raw(" │"), ]), Line::from(vec![ Span::raw("│ "), Span::styled("`b` boolean", sig_style), Span::raw(" │ "), Span::styled("`true` or `false`", value_style), Span::raw(" │ "), Span::styled("True or False", value_style), Span::raw(" │"), ]), Line::from(vec![ Span::raw("│ "), Span::styled("`s` string", sig_style), Span::raw(" │ "), Span::styled("`\"Hello World\"`", value_style), Span::raw(" │ "), Span::styled("\"Hello World\"", value_style), Span::raw(" │"), ]), Line::from(vec![ Span::raw("│ "), Span::styled("`o` object path", sig_style), Span::raw(" │ "), Span::styled("`\"/org/example/path\"`", value_style), Span::raw(" │ "), Span::styled("ObjectPath", value_style), Span::raw(" │"), ]), Line::from(vec![ Span::raw("│ "), Span::styled("`g` signature", sig_style), Span::raw(" │ "), Span::styled("`\"as\"`", value_style), Span::raw(" │ "), Span::styled("Signature value", value_style), Span::raw(" │"), ]), Line::from( "└─────────────────┴──────────────────────────────────┴─────────────────────────────┘", ), Line::from(""), // --- Complex Type Examples --- Line::from(Span::styled("Complex Type Examples", heading_style)), Line::from(vec![ "• ".into(), Span::styled("a{si}", code_style), ": A dictionary mapping strings to integers. e.g., ".into(), Span::styled("{\"count\": 5}", code_style), ]), Line::from(vec![ "• ".into(), Span::styled("(ssu)", code_style), ": A struct of two strings and a u32. e.g., ".into(), Span::styled("(\"user\", \"john\", 42)", code_style), ]), Line::from(vec![ "• ".into(), Span::styled("a(is)", code_style), ": An array of (integer, string) structs. e.g., ".into(), Span::styled("[(1, \"a\"), (2, \"b\")]", code_style), ]), Line::from(""), Line::from(vec![ "Note: File Descriptors (".into(), Span::styled("h", code_style), ") cannot be parsed from strings.".into(), ]), ] } dtui-3.0.0/src/bin/dtui/components/objects_view.rs000064400000000000000000000176761046102023000203340ustar 00000000000000use color_eyre::Result; use ratatui::{prelude::*, widgets::*}; use tokio::sync::mpsc::UnboundedSender; use tracing::info; use tui_tree_widget::Tree; use zbus::zvariant::OwnedObjectPath; use zbus_names::{OwnedBusName, OwnedInterfaceName, OwnedMemberName}; use super::Component; use crate::{ action::{Action, Invocation}, app::Focus, config::Config, dbus_handler::DbusActorHandle, other::active_area_border_color, stateful_tree::{self, StatefulTree}, }; #[derive(Default)] pub struct ObjectsView { command_tx: Option>, config: Config, objects: StatefulTree, originating_service: Option, dbus_actor_handle: Option, active: bool, } impl ObjectsView { pub fn new() -> Self { Self::default() } } impl Component for ObjectsView { fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.command_tx = Some(tx); Ok(()) } fn register_config_handler(&mut self, config: Config) -> Result<()> { self.config = config; Ok(()) } fn register_dbus_actor_handle(&mut self, dbus_actor_handle: DbusActorHandle) -> Result<()> { self.dbus_actor_handle = Some(dbus_actor_handle); Ok(()) } async fn update(&mut self, action: Action) -> Result> { if let Action::Focus(focus) = action { self.active = focus == Focus::Objects; } if self.active { match action { Action::Up => { self.objects.up(); } Action::Down => { self.objects.down(); } Action::DownTree => { self.objects.right(); } Action::UpTree => { self.objects.left(); } Action::InvokeDbus(dbus_action) => { info!( "Invoking dbus_action {} on {:?}", dbus_action, self.objects.state.selected() ); if let Some(originating_service) = &self.originating_service { let selected = self.objects.state.selected(); let invokable = extract_invokable(originating_service.clone(), selected); if let Some(invokable) = invokable { match (dbus_action, &invokable.invocation_description) { ( crate::action::DbusInvocationAction::CallMethod, crate::action::InvokableDbusMember::Method { .. }, ) | ( crate::action::DbusInvocationAction::SetProperty, crate::action::InvokableDbusMember::Property { .. }, ) => { return Ok(Some(Action::StartDbusInvocation(invokable))); } ( crate::action::DbusInvocationAction::GetProperty, crate::action::InvokableDbusMember::Property { property }, ) | ( crate::action::DbusInvocationAction::CallMethod, crate::action::InvokableDbusMember::Property { property }, ) => { let dbus_actor = self .dbus_actor_handle .clone() .expect("Component needs dbus handle"); dbus_actor .get_property( invokable.service, invokable.object, invokable.interface, property.name.as_str().to_string(), ) .await; return Ok(None); } _ => (), } } } } _ => {} } } Ok(None) } fn update_from_dbus( &mut self, dbus_action: crate::messages::AppMessage, ) -> Result> { match dbus_action { crate::messages::AppMessage::Objects((service_name, objects)) => { info!("Got objects from service: {}", service_name); self.originating_service = Some(service_name); self.objects = StatefulTree::from_nodes(objects); } crate::messages::AppMessage::Services(_owned_bus_names) => (), crate::messages::AppMessage::InvocationResponse { .. } => {} crate::messages::AppMessage::Error(_dbus_error) => {} } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let objects_view = Tree::new(&self.objects.items) .unwrap() .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(active_area_border_color(self.active))) .title("Objects"), ) .highlight_style(Style::default().add_modifier(Modifier::BOLD)) .highlight_symbol(">> "); frame.render_stateful_widget(objects_view, area, &mut self.objects.state); Ok(()) } } fn extract_invokable( current_service: OwnedBusName, selected: &[crate::stateful_tree::DbusIdentifier], ) -> Option { let mut selected_iter = selected.iter(); if let Some(stateful_tree::DbusIdentifier::Object(path)) = selected_iter.next() && let Some(stateful_tree::DbusIdentifier::Interface(interface)) = selected_iter.next() && let Some(stateful_tree::DbusIdentifier::Member(member_type)) = selected_iter.next() { let path = OwnedObjectPath::try_from(path.clone()).unwrap(); let interface = OwnedInterfaceName::try_from(interface.clone()).unwrap(); match member_type { stateful_tree::MemberTypes::Methods => { if let Some(stateful_tree::DbusIdentifier::Method(method)) = selected_iter.next() { Some(crate::action::InvokableDbusMember::Method { method: method.clone(), }) } else { None } } stateful_tree::MemberTypes::Properties => { if let Some(stateful_tree::DbusIdentifier::Property(property)) = selected_iter.next() { Some(crate::action::InvokableDbusMember::Property { property: property.to_owned(), }) } else { None } } stateful_tree::MemberTypes::Signals => { if let Some(stateful_tree::DbusIdentifier::Signal(signal)) = selected_iter.next() { Some(crate::action::InvokableDbusMember::Signal { name: OwnedMemberName::try_from(signal.clone()).unwrap(), }) } else { None } } } .map(|invokable| Invocation { service: current_service, object: path, interface, invocation_description: invokable, }) } else { None } } dtui-3.0.0/src/bin/dtui/components/result_view.rs000064400000000000000000000071221046102023000202020ustar 00000000000000use color_eyre::Result; use itertools::Itertools; use ratatui::{prelude::*, widgets::*}; use tokio::sync::mpsc::UnboundedSender; use tui_widget_list::ListBuilder; use super::Component; use crate::{ action::Action, config::Config, messages::AppMessage, other::active_area_border_color, }; #[derive(Default)] pub struct ResultsView { command_tx: Option>, config: Config, active: bool, list_state: tui_widget_list::ListState, results: Vec, } impl ResultsView { pub fn new() -> Self { Self::default() } } impl Component for ResultsView { fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.command_tx = Some(tx); Ok(()) } fn register_config_handler(&mut self, config: Config) -> Result<()> { self.config = config; Ok(()) } async fn update(&mut self, action: Action) -> Result> { if self.active { match action { Action::Up => {} Action::Down => {} Action::DownTree => {} Action::UpTree => {} _ => {} } } else { // Handle action when not in focus } Ok(None) } fn update_from_dbus( &mut self, dbus_action: crate::messages::AppMessage, ) -> Result> { self.results.push(dbus_action); Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let block = Block::default() .borders(Borders::ALL) .title("Results") .border_type(BorderType::Rounded) .border_style(Style::default().fg(active_area_border_color(self.active))); let inner = block.inner(area); frame.render_widget(block, area); let builder = ListBuilder::new(|context| { let widget = self.results.get(context.index).unwrap(); (widget, (widget.characters() / context.cross_axis_size) + 1) // We want the number of times the amount of characters can fit into the cross_axis }); let list = tui_widget_list::ListView::new(builder, self.results.len()); list.render(inner, frame.buffer_mut(), &mut self.list_state); Ok(()) } } fn dbus_result_to_string(message: &zbus::Message) -> String { if let Ok(message) = message.body().deserialize::() { message .fields() .iter() .map(|field| field.to_string()) .join(",") } else { "".to_string() } } const RESULT_STYLE: Style = Style::new(); const ERROR_STYLE: Style = Style::new().fg(Color::Red); impl Widget for &AppMessage { fn render(self, area: Rect, buf: &mut Buffer) where Self: Sized, { match self { AppMessage::Objects(object) => { Paragraph::new(format!("Service: {}", &object.0)).style(RESULT_STYLE) } AppMessage::Services(..) => { Paragraph::new("All services read".to_string()).style(RESULT_STYLE) } AppMessage::InvocationResponse(invocation_response) => { Paragraph::new(dbus_result_to_string(&invocation_response.message).to_string()) .style(RESULT_STYLE) } AppMessage::Error(dbus_error) => { Paragraph::new(format!("Error! {}", dbus_error.message)).style(ERROR_STYLE) } } .add_modifier(Modifier::BOLD) .wrap(Wrap { trim: false }) .render(area, buf); } } dtui-3.0.0/src/bin/dtui/components/services_view.rs000064400000000000000000000067641046102023000205220ustar 00000000000000use color_eyre::Result; use ratatui::{prelude::*, widgets::*}; use tokio::sync::mpsc::UnboundedSender; use tracing::info; use zbus_names::OwnedBusName; use super::Component; use crate::{ action::Action, app::Focus, config::Config, dbus_handler::DbusActorHandle, messages::InvocationResponse, other::active_area_border_color, stateful_list::StatefulList, }; #[derive(Default)] pub struct ServicesView { command_tx: Option>, config: Config, services: StatefulList, active: bool, dbus_actor_handle: Option, } impl ServicesView { pub fn new() -> Self { Self::default() } } impl Component for ServicesView { fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.command_tx = Some(tx); Ok(()) } fn register_config_handler(&mut self, config: Config) -> Result<()> { self.config = config; Ok(()) } fn register_dbus_actor_handle(&mut self, dbus_actor_handle: DbusActorHandle) -> Result<()> { self.dbus_actor_handle = Some(dbus_actor_handle); Ok(()) } async fn update(&mut self, action: Action) -> Result> { if let Action::Focus(focus) = action { self.active = focus == Focus::Services; } if self.active { match action { Action::Down => { self.services.next(); } Action::Up => { self.services.previous(); } Action::GetService => { if let Some(selected_index) = self.services.state.selected() { let item = self.services.items[selected_index].clone(); if let Some(handle) = &self.dbus_actor_handle { handle.request_objects_from(item).await } } } _ => {} } } Ok(None) } fn update_from_dbus( &mut self, dbus_action: crate::messages::AppMessage, ) -> Result> { match dbus_action { crate::messages::AppMessage::Objects(_) => {} crate::messages::AppMessage::Services(owned_bus_names) => { info!("services view got services"); self.services = StatefulList::with_items(owned_bus_names); } crate::messages::AppMessage::InvocationResponse(InvocationResponse { .. }) => {} crate::messages::AppMessage::Error(..) => {} } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let items: Vec = self .services .items .iter() .map(|i| { let lines = Span::from(i.as_str()); ListItem::new(lines).style(Style::default()) }) .collect(); let items = List::new(items) .block( Block::default() .borders(Borders::ALL) .title("Services") .border_type(BorderType::Rounded) .border_style(Style::default().fg(active_area_border_color(self.active))), ) .highlight_style(Style::default().add_modifier(Modifier::BOLD)) .highlight_symbol(">> "); frame.render_stateful_widget(items, area, &mut self.services.state); Ok(()) } } dtui-3.0.0/src/bin/dtui/components.rs000064400000000000000000000175401046102023000156370ustar 00000000000000use bottom_text::BottomText; use color_eyre::Result; use crossterm::event::{KeyEvent, MouseEvent}; use objects_view::ObjectsView; use ratatui::{ Frame, layout::{Rect, Size}, }; use services_view::ServicesView; use tokio::sync::mpsc::UnboundedSender; use crate::{ action::Action, components::{call_view::CallView, help_view::HelpView, result_view::ResultsView}, config::Config, dbus_handler::DbusActorHandle, messages::AppMessage, tui::Event, }; pub mod bottom_text; pub mod call_view; pub mod help_view; pub mod objects_view; pub mod result_view; pub mod services_view; pub struct Components { pub service_view: ServicesView, pub object_view: ObjectsView, pub bottom_text: BottomText, pub results_view: ResultsView, pub call_view: CallView, pub help_view: HelpView, } impl Components { pub fn new() -> Self { Components { service_view: ServicesView::new(), object_view: ObjectsView::new(), bottom_text: BottomText::new(), results_view: ResultsView::new(), call_view: CallView::new(), help_view: HelpView::new(), } } pub fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.service_view.register_action_handler(tx.clone())?; self.object_view.register_action_handler(tx.clone())?; self.results_view.register_action_handler(tx.clone())?; self.call_view.register_action_handler(tx.clone())?; self.help_view.register_action_handler(tx.clone())?; self.bottom_text.register_action_handler(tx)?; Ok(()) } pub fn register_config_handler(&mut self, config: Config) -> Result<()> { self.service_view.register_config_handler(config.clone())?; self.object_view.register_config_handler(config.clone())?; self.results_view.register_config_handler(config.clone())?; self.call_view.register_config_handler(config.clone())?; self.help_view.register_config_handler(config.clone())?; self.bottom_text.register_config_handler(config)?; Ok(()) } pub fn register_dbus_actor_handler(&mut self, dbus_handle: DbusActorHandle) -> Result<()> { self.service_view .register_dbus_actor_handle(dbus_handle.clone())?; self.object_view .register_dbus_actor_handle(dbus_handle.clone())?; self.results_view .register_dbus_actor_handle(dbus_handle.clone())?; self.call_view .register_dbus_actor_handle(dbus_handle.clone())?; Ok(()) } pub fn init(&mut self, size: ratatui::prelude::Size) -> Result<()> { self.service_view.init(size)?; self.object_view.init(size)?; self.results_view.init(size)?; self.call_view.init(size)?; self.help_view.init(size)?; self.bottom_text.init(size)?; Ok(()) } pub fn handle_key_event(&mut self, key: KeyEvent) -> Result> { let actions = [ self.call_view.handle_key_event(key)?, self.service_view.handle_key_event(key)?, self.object_view.handle_key_event(key)?, self.bottom_text.handle_key_event(key)?, self.results_view.handle_key_event(key)?, self.help_view.handle_key_event(key)?, ]; Ok(actions.into_iter().flatten().collect()) // filter out nones } } /// `Component` is a trait that represents a visual and interactive element of the user interface. /// /// Implementors of this trait can be registered with the main application loop and will be able to /// receive events, update state, and be rendered on the screen. pub trait Component { /// Register an action handler that can send actions for processing if necessary. /// /// # Arguments /// /// * `tx` - An unbounded sender that can send actions. /// /// # Returns /// /// * `Result<()>` - An Ok result or an error. fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { let _ = tx; // to appease clippy Ok(()) } /// Register a configuration handler that provides configuration settings if necessary. /// /// # Arguments /// /// * `config` - Configuration settings. /// /// # Returns /// /// * `Result<()>` - An Ok result or an error. fn register_config_handler(&mut self, config: Config) -> Result<()> { let _ = config; // to appease clippy Ok(()) } /// Register a dbus_actor_handle that provides access to dbus /// /// # Arguments /// /// * `dbus_actor_handle` - DbusActorHandle that enables dbus access. /// /// # Returns /// /// * `Result<()>` - An Ok result or an error. fn register_dbus_actor_handle(&mut self, dbus_actor_handle: DbusActorHandle) -> Result<()> { let _ = dbus_actor_handle; // to appease clippy Ok(()) } /// Initialize the component with a specified area if necessary. /// /// # Arguments /// /// * `area` - Rectangular area to initialize the component within. /// /// # Returns /// /// * `Result<()>` - An Ok result or an error. fn init(&mut self, area: Size) -> Result<()> { let _ = area; // to appease clippy Ok(()) } /// Handle incoming events and produce actions if necessary. /// /// # Arguments /// /// * `event` - An optional event to be processed. /// /// # Returns /// /// * `Result>` - An action to be processed or none. fn handle_events(&mut self, event: Option) -> Result> { let action = match event { Some(Event::Key(key_event)) => self.handle_key_event(key_event)?, Some(Event::Mouse(mouse_event)) => self.handle_mouse_event(mouse_event)?, _ => None, }; Ok(action) } /// Handle key events and produce actions if necessary. /// /// # Arguments /// /// * `key` - A key event to be processed. /// /// # Returns /// /// * `Result>` - An action to be processed or none. fn handle_key_event(&mut self, key: KeyEvent) -> Result> { let _ = key; // to appease clippy Ok(None) } /// Handle mouse events and produce actions if necessary. /// /// # Arguments /// /// * `mouse` - A mouse event to be processed. /// /// # Returns /// /// * `Result>` - An action to be processed or none. fn handle_mouse_event(&mut self, mouse: MouseEvent) -> Result> { let _ = mouse; // to appease clippy Ok(None) } /// Update the state of the component based on a received action. (REQUIRED) /// /// # Arguments /// /// * `action` - An action that may modify the state of the component. /// /// # Returns /// /// * `Result>` - An action to be processed or none. async fn update(&mut self, action: Action) -> Result> { let _ = action; // to appease clippy Ok(None) } /// Update the state of the component based on a received dbus action. /// /// # Arguments /// /// * `action` - A Dbus action that may modify the state of the component. /// /// # Returns /// /// * `Result>` - An action to be processed or none. /// fn update_from_dbus(&mut self, dbus_action: AppMessage) -> Result> { let _ = dbus_action; // to appease clippy Ok(None) } /// Render the component on the screen. (REQUIRED) /// /// # Arguments /// /// * `f` - A frame used for rendering. /// * `area` - The area in which the component should be drawn. /// /// # Returns /// /// * `Result<()>` - An Ok result or an error. fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()>; } dtui-3.0.0/src/bin/dtui/config.rs000064400000000000000000000335541046102023000147220ustar 00000000000000#![allow(dead_code)] // Remove this once you start using the code use std::{collections::HashMap, env, path::PathBuf}; use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use derive_deref::{Deref, DerefMut}; use directories::ProjectDirs; use lazy_static::lazy_static; use ratatui::style::{Color, Modifier, Style}; use serde::{Deserialize, de::Deserializer}; use tracing::error; use crate::{action::Action, app::Focus}; const CONFIG: &str = include_str!("../../../.config/config.json5"); #[derive(Clone, Debug, Deserialize, Default)] pub struct AppConfig { #[serde(default)] pub data_dir: PathBuf, #[serde(default)] pub config_dir: PathBuf, } #[derive(Clone, Debug, Default, Deserialize)] pub struct Config { #[serde(default, flatten)] pub config: AppConfig, #[serde(default)] pub keybindings: KeyBindings, #[serde(default)] pub styles: Styles, } lazy_static! { pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); pub static ref DATA_FOLDER: Option = env::var(format!("{}_DATA", PROJECT_NAME.clone())) .ok() .map(PathBuf::from); pub static ref CONFIG_FOLDER: Option = env::var(format!("{}_CONFIG", PROJECT_NAME.clone())) .ok() .map(PathBuf::from); } impl Config { pub fn new() -> Result { let default_config: Config = json5::from_str(CONFIG).unwrap(); let data_dir = get_data_dir(); let config_dir = get_config_dir(); let mut builder = config::Config::builder() .set_default("data_dir", data_dir.to_str().unwrap())? .set_default("config_dir", config_dir.to_str().unwrap())?; let config_files = [ ("config.json5", config::FileFormat::Json5), ("config.json", config::FileFormat::Json), ("config.yaml", config::FileFormat::Yaml), ("config.toml", config::FileFormat::Toml), ("config.ini", config::FileFormat::Ini), ]; let mut found_config = false; for (file, format) in &config_files { let source = config::File::from(config_dir.join(file)) .format(*format) .required(false); builder = builder.add_source(source); if config_dir.join(file).exists() { found_config = true } } if !found_config { error!("No configuration file found. Application may not behave as expected"); } let mut cfg: Self = builder.build()?.try_deserialize()?; for (mode, default_bindings) in default_config.keybindings.iter() { let user_bindings = cfg.keybindings.entry(*mode).or_default(); for (key, cmd) in default_bindings.iter() { user_bindings .entry(key.clone()) .or_insert_with(|| cmd.clone()); } } for (mode, default_styles) in default_config.styles.iter() { let user_styles = cfg.styles.entry(*mode).or_default(); for (style_key, style) in default_styles.iter() { user_styles.entry(style_key.clone()).or_insert(*style); } } Ok(cfg) } } pub fn get_data_dir() -> PathBuf { if let Some(s) = DATA_FOLDER.clone() { s } else if let Some(proj_dirs) = project_directory() { proj_dirs.data_local_dir().to_path_buf() } else { PathBuf::from(".").join(".data") } } pub fn get_config_dir() -> PathBuf { if let Some(s) = CONFIG_FOLDER.clone() { s } else if let Some(proj_dirs) = project_directory() { proj_dirs.config_local_dir().to_path_buf() } else { PathBuf::from(".").join(".config") } } fn project_directory() -> Option { ProjectDirs::from("com", "troels51", env!("CARGO_PKG_NAME")) } #[derive(Clone, Debug, Default, Deref, DerefMut)] pub struct KeyBindings(pub HashMap, Action>>); impl<'de> Deserialize<'de> for KeyBindings { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let parsed_map = HashMap::>::deserialize(deserializer)?; let keybindings = parsed_map .into_iter() .map(|(mode, inner_map)| { let converted_inner_map = inner_map .into_iter() .map(|(key_str, cmd)| (parse_key_sequence(&key_str).unwrap(), cmd)) .collect(); (mode, converted_inner_map) }) .collect(); Ok(KeyBindings(keybindings)) } } impl KeyBindings { pub fn get_key_from_action(&self, focus: Focus, action: Action) -> Option<&Vec> { self.get(&focus).and_then(|m| { m.iter() .find(|(_, value)| **value == action) .map(|(key, _)| key) }) } } fn parse_key_event(raw: &str) -> Result { let raw_lower = raw.to_ascii_lowercase(); let (remaining, modifiers) = extract_modifiers(&raw_lower); parse_key_code_with_modifiers(remaining, modifiers) } fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) { let mut modifiers = KeyModifiers::empty(); let mut current = raw; loop { match current { rest if rest.starts_with("ctrl-") => { modifiers.insert(KeyModifiers::CONTROL); current = &rest[5..]; } rest if rest.starts_with("alt-") => { modifiers.insert(KeyModifiers::ALT); current = &rest[4..]; } rest if rest.starts_with("shift-") => { modifiers.insert(KeyModifiers::SHIFT); current = &rest[6..]; } _ => break, // break out of the loop if no known prefix is detected }; } (current, modifiers) } fn parse_key_code_with_modifiers( raw: &str, mut modifiers: KeyModifiers, ) -> Result { let c = match raw { "esc" => KeyCode::Esc, "enter" => KeyCode::Enter, "left" => KeyCode::Left, "right" => KeyCode::Right, "up" => KeyCode::Up, "down" => KeyCode::Down, "home" => KeyCode::Home, "end" => KeyCode::End, "pageup" => KeyCode::PageUp, "pagedown" => KeyCode::PageDown, "backtab" => { modifiers.insert(KeyModifiers::SHIFT); KeyCode::BackTab } "backspace" => KeyCode::Backspace, "delete" => KeyCode::Delete, "insert" => KeyCode::Insert, "f1" => KeyCode::F(1), "f2" => KeyCode::F(2), "f3" => KeyCode::F(3), "f4" => KeyCode::F(4), "f5" => KeyCode::F(5), "f6" => KeyCode::F(6), "f7" => KeyCode::F(7), "f8" => KeyCode::F(8), "f9" => KeyCode::F(9), "f10" => KeyCode::F(10), "f11" => KeyCode::F(11), "f12" => KeyCode::F(12), "space" => KeyCode::Char(' '), "hyphen" => KeyCode::Char('-'), "minus" => KeyCode::Char('-'), "tab" => KeyCode::Tab, c if c.len() == 1 => { let mut c = c.chars().next().unwrap(); if modifiers.contains(KeyModifiers::SHIFT) { c = c.to_ascii_uppercase(); } KeyCode::Char(c) } _ => return Err(format!("Unable to parse {raw}")), }; Ok(KeyEvent::new(c, modifiers)) } pub fn key_event_to_string(key_event: &KeyEvent) -> String { let char; let key_code = match key_event.code { KeyCode::Backspace => "backspace", KeyCode::Enter => "enter", KeyCode::Left => "left", KeyCode::Right => "right", KeyCode::Up => "up", KeyCode::Down => "down", KeyCode::Home => "home", KeyCode::End => "end", KeyCode::PageUp => "pageup", KeyCode::PageDown => "pagedown", KeyCode::Tab => "tab", KeyCode::BackTab => "backtab", KeyCode::Delete => "delete", KeyCode::Insert => "insert", KeyCode::F(c) => { char = format!("f({c})"); &char } KeyCode::Char(' ') => "space", KeyCode::Char(c) => { char = c.to_string(); &char } KeyCode::Esc => "esc", KeyCode::Null => "", KeyCode::CapsLock => "", KeyCode::Menu => "", KeyCode::ScrollLock => "", KeyCode::Media(_) => "", KeyCode::NumLock => "", KeyCode::PrintScreen => "", KeyCode::Pause => "", KeyCode::KeypadBegin => "", KeyCode::Modifier(_) => "", }; let mut modifiers = Vec::with_capacity(3); if key_event.modifiers.intersects(KeyModifiers::CONTROL) { modifiers.push("ctrl"); } if key_event.modifiers.intersects(KeyModifiers::SHIFT) { modifiers.push("shift"); } if key_event.modifiers.intersects(KeyModifiers::ALT) { modifiers.push("alt"); } let mut key = modifiers.join("-"); if !key.is_empty() { key.push('-'); } key.push_str(key_code); key } pub fn parse_key_sequence(raw: &str) -> Result, String> { if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() { return Err(format!("Unable to parse `{}`", raw)); } let raw = if !raw.contains("><") { let raw = raw.strip_prefix('<').unwrap_or(raw); raw.strip_prefix('>').unwrap_or(raw) } else { raw }; let sequences = raw .split("><") .map(|seq| { if let Some(s) = seq.strip_prefix('<') { s } else if let Some(s) = seq.strip_suffix('>') { s } else { seq } }) .collect::>(); sequences.into_iter().map(parse_key_event).collect() } #[derive(Clone, Debug, Default, Deref, DerefMut)] pub struct Styles(pub HashMap>); impl<'de> Deserialize<'de> for Styles { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let parsed_map = HashMap::>::deserialize(deserializer)?; let styles = parsed_map .into_iter() .map(|(mode, inner_map)| { let converted_inner_map = inner_map .into_iter() .map(|(str, style)| (str, parse_style(&style))) .collect(); (mode, converted_inner_map) }) .collect(); Ok(Styles(styles)) } } pub fn parse_style(line: &str) -> Style { let (foreground, background) = line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len())); let foreground = process_color_string(foreground); let background = process_color_string(&background.replace("on ", "")); let mut style = Style::default(); if let Some(fg) = parse_color(&foreground.0) { style = style.fg(fg); } if let Some(bg) = parse_color(&background.0) { style = style.bg(bg); } style = style.add_modifier(foreground.1 | background.1); style } fn process_color_string(color_str: &str) -> (String, Modifier) { let color = color_str .replace("grey", "gray") .replace("bright ", "") .replace("bold ", "") .replace("underline ", "") .replace("inverse ", ""); let mut modifiers = Modifier::empty(); if color_str.contains("underline") { modifiers |= Modifier::UNDERLINED; } if color_str.contains("bold") { modifiers |= Modifier::BOLD; } if color_str.contains("inverse") { modifiers |= Modifier::REVERSED; } (color, modifiers) } fn parse_color(s: &str) -> Option { let s = s.trim_start(); let s = s.trim_end(); if s.contains("bright color") { let s = s.trim_start_matches("bright "); let c = s .trim_start_matches("color") .parse::() .unwrap_or_default(); Some(Color::Indexed(c.wrapping_shl(8))) } else if s.contains("color") { let c = s .trim_start_matches("color") .parse::() .unwrap_or_default(); Some(Color::Indexed(c)) } else if s.contains("gray") { let c = 232 + s.trim_start_matches("gray") .parse::() .unwrap_or_default(); Some(Color::Indexed(c)) } else if s.contains("rgb") { let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8; let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8; let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8; let c = 16 + red * 36 + green * 6 + blue; Some(Color::Indexed(c)) } else if s == "bold black" { Some(Color::Indexed(8)) } else if s == "bold red" { Some(Color::Indexed(9)) } else if s == "bold green" { Some(Color::Indexed(10)) } else if s == "bold yellow" { Some(Color::Indexed(11)) } else if s == "bold blue" { Some(Color::Indexed(12)) } else if s == "bold magenta" { Some(Color::Indexed(13)) } else if s == "bold cyan" { Some(Color::Indexed(14)) } else if s == "bold white" { Some(Color::Indexed(15)) } else if s == "black" { Some(Color::Indexed(0)) } else if s == "red" { Some(Color::Indexed(1)) } else if s == "green" { Some(Color::Indexed(2)) } else if s == "yellow" { Some(Color::Indexed(3)) } else if s == "blue" { Some(Color::Indexed(4)) } else if s == "magenta" { Some(Color::Indexed(5)) } else if s == "cyan" { Some(Color::Indexed(6)) } else if s == "white" { Some(Color::Indexed(7)) } else { None } } dtui-3.0.0/src/bin/dtui/dbus_handler.rs000064400000000000000000000203451046102023000161010ustar 00000000000000use std::{collections::HashMap, error::Error, io::BufReader}; use async_recursion::async_recursion; use tokio::sync::mpsc::{self, Receiver, UnboundedSender}; use zbus::{ Connection, names::{OwnedBusName, OwnedInterfaceName, OwnedMemberName}, zvariant::{ObjectPath, OwnedValue, Str, StructureBuilder}, }; use zbus_xml::Node; use crate::messages::{AppMessage, DbusError, DbusMessage, InvocationResponse}; pub struct DbusActor { app_sender: UnboundedSender, app_receiver: Receiver, connection: Connection, } impl DbusActor { pub fn new( app_sender: UnboundedSender, app_receiver: Receiver, connection: Connection, ) -> Self { Self { app_sender, app_receiver, connection, } } async fn get_node( &self, service_name: &OwnedBusName, path: &ObjectPath<'_>, ) -> Result, Box> { let introspectable_proxy = zbus::fdo::IntrospectableProxy::builder(&self.connection) .destination(service_name)? .path(path.clone())? .build() .await?; let introspect_xml: String = introspectable_proxy.introspect().await?; let introspect = Node::from_reader(BufReader::new(introspect_xml.as_bytes()))?; Ok(introspect) } #[async_recursion] async fn get_sub_nodes( &self, service_name: &OwnedBusName, path: &ObjectPath<'async_recursion>, ) -> Result>, Box> { let node = self.get_node(service_name, path).await?; let mut result = HashMap::new(); for sub_node in node.nodes() { if let Some(name) = sub_node.name() { let path_name = if path.as_str().ends_with('/') { path.as_str().to_string() + name } else { path.as_str().to_string() + "/" + name }; let sub_path = ObjectPath::try_from(path_name)?; result.extend(self.get_sub_nodes(service_name, &sub_path).await?); } } result.insert(path.to_string(), node); Ok(result) } pub async fn handle_message(&mut self, msg: DbusMessage) { tracing::info!("Handle message {:?}", msg); match msg { DbusMessage::GetObjects(service_name) => { let path_name = "/".to_string(); let path = ObjectPath::try_from(path_name).expect("/ is always a valid path"); if let Ok(nodes) = self.get_sub_nodes(&service_name, &path).await { self.app_sender .send(AppMessage::Objects((service_name, nodes))) .expect("channel dead"); } } DbusMessage::ServiceRequest() => { let proxy = zbus::fdo::DBusProxy::new(&self.connection) .await .expect("Could not create DbusProxy"); if let Ok(names) = proxy.list_names().await { let _ = self.app_sender.send(AppMessage::Services(names)); } } DbusMessage::MethodCallRequest(service, object_path, interface, method, values) => { let mut body = StructureBuilder::new(); let is_empty = values.is_empty(); for v in values { body.push_value(v.into()); } let method_call_response = if !is_empty { self.connection .call_method( Some(service.clone()), object_path.clone(), Some(interface.clone()), method.clone(), &body.build().unwrap(), ) .await } else { self.connection .call_method( Some(service.clone()), object_path.clone(), Some(interface.clone()), method.clone(), &(), ) .await }; match method_call_response { Ok(message) => { let _ = self.app_sender.send(AppMessage::InvocationResponse( InvocationResponse { service, object_path, method_name: method, interface, message, }, )); } Err(e) => { tracing::info!("Method call error {}", e); let _ = self.app_sender.send(AppMessage::Error(DbusError { message: e.to_string(), })); } }; } } } } async fn run_actor(mut actor: DbusActor) { while let Some(msg) = actor.app_receiver.recv().await { actor.handle_message(msg).await } } #[derive(Clone, Debug)] pub struct DbusActorHandle { sender: mpsc::Sender, } impl DbusActorHandle { pub fn new(app_sender: UnboundedSender, connection: Connection) -> Self { let (sender, receiver) = mpsc::channel(8); let actor = DbusActor::new(app_sender, receiver, connection); tokio::spawn(run_actor(actor)); Self { sender } } pub async fn request_objects_from(&self, object: OwnedBusName) { let msg = DbusMessage::GetObjects(object); let _ = self.sender.send(msg).await; } pub async fn request_services(&self) { let msg = DbusMessage::ServiceRequest(); let _ = self.sender.send(msg).await; } pub async fn call_method( &self, service: OwnedBusName, object: zbus::zvariant::OwnedObjectPath, interface: OwnedInterfaceName, method: OwnedMemberName, values: Vec, ) { let msg = DbusMessage::MethodCallRequest(service, object, interface, method, values); let _ = self.sender.send(msg).await; } pub async fn get_property( &self, service: OwnedBusName, object: zbus::zvariant::OwnedObjectPath, interface: OwnedInterfaceName, property_name: String, ) { let property_interface = OwnedInterfaceName::try_from("org.freedesktop.DBus.Properties") .expect("org.freedesktop.Dbus.Properties is valid interface name"); let method = OwnedMemberName::try_from("Get").expect("Get is a valid Method"); let mut values: Vec = Vec::new(); values.push( OwnedValue::try_from(interface.clone()).expect("OwnedInterfaceName is valid value"), ); values.push(OwnedValue::from(Str::from(property_name))); let msg = DbusMessage::MethodCallRequest(service, object, property_interface, method, values); let _ = self.sender.send(msg).await; } pub async fn set_property( &self, service: OwnedBusName, object: zbus::zvariant::OwnedObjectPath, interface: OwnedInterfaceName, property_name: zbus_names::OwnedPropertyName, value: OwnedValue, ) { let property_interface = OwnedInterfaceName::try_from("org.freedesktop.DBus.Properties") .expect("org.freedesktop.Dbus.Properties is valid interface name"); let method = OwnedMemberName::try_from("Set").expect("Set is a valid Method"); let mut values: Vec = Vec::new(); values.push( OwnedValue::try_from(interface.clone()).expect("OwnedInterfaceName is valid value"), ); values.push(OwnedValue::from(Str::from(property_name))); values.push(value); let msg = DbusMessage::MethodCallRequest(service, object, property_interface, method, values); let _ = self.sender.send(msg).await; } } dtui-3.0.0/src/bin/dtui/error.rs000064400000000000000000000016071046102023000146000ustar 00000000000000use std::env; use color_eyre::Result; use tracing::error; pub fn init() -> Result<()> { let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() .panic_section(format!( "This is a bug. Consider reporting it at {}", env!("CARGO_PKG_REPOSITORY") )) .capture_span_trace_by_default(false) .display_location_section(false) .display_env_section(false) .into_hooks(); eyre_hook.install()?; std::panic::set_hook(Box::new(move |panic_info| { if let Ok(mut t) = crate::tui::Tui::new() && let Err(r) = t.exit() { error!("Unable to exit Terminal: {:?}", r); } let msg = format!("{}", panic_hook.panic_report(panic_info)); error!("Error: {}", strip_ansi_escapes::strip_str(msg)); std::process::exit(libc::EXIT_FAILURE); })); Ok(()) } dtui-3.0.0/src/bin/dtui/logging.rs000064400000000000000000000025521046102023000150750ustar 00000000000000use color_eyre::Result; use tracing_error::ErrorLayer; use tracing_subscriber::{EnvFilter, fmt, prelude::*}; use crate::config; lazy_static::lazy_static! { pub static ref LOG_ENV: String = format!("{}_LOG_LEVEL", config::PROJECT_NAME.clone()); pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); } pub fn init() -> Result<()> { let directory = config::get_data_dir(); std::fs::create_dir_all(directory.clone())?; let log_path = directory.join(LOG_FILE.clone()); let log_file = std::fs::File::create(log_path)?; let env_filter = EnvFilter::builder().with_default_directive(tracing::Level::INFO.into()); // If the `RUST_LOG` environment variable is set, use that as the default, otherwise use the // value of the `LOG_ENV` environment variable. If the `LOG_ENV` environment variable contains // errors, then this will return an error. let env_filter = env_filter .try_from_env() .or_else(|_| env_filter.with_env_var(LOG_ENV.clone()).from_env())?; let file_subscriber = fmt::layer() .with_file(true) .with_line_number(true) .with_writer(log_file) .with_target(false) .with_ansi(false) .with_filter(env_filter); tracing_subscriber::registry() .with(file_subscriber) .with(ErrorLayer::default()) .try_init()?; Ok(()) } dtui-3.0.0/src/bin/dtui/main.rs000064400000000000000000000022561046102023000143740ustar 00000000000000mod action; mod app; mod components; mod config; pub mod dbus_handler; mod error; mod logging; pub mod messages; mod other; pub mod parser; pub mod stateful_list; pub mod stateful_tree; mod tui; use clap::{ArgGroup, Parser, ValueEnum, command}; use std::error::Error; use tracing::level_filters::LevelFilter; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum BusType { System, Session, } #[derive(Parser)] #[command(author, version, about, long_about = None)] #[clap(group(ArgGroup::new("bus_or_address").args(&["bus", "address"])))] pub struct Args { //Which bus to connect to #[clap(default_value_t = BusType::System)] #[arg(value_enum)] pub bus: BusType, //Address of potentially remote connection #[clap(long)] pub address: Option, #[clap(default_value_t = LevelFilter::OFF)] pub debug_level: LevelFilter, } #[tokio::main] async fn main() -> Result<(), Box> { let args = Args::parse(); logging::init()?; error::init()?; let app = app::App::new(10.0, 60.0, args).await; match app { Ok(mut app) => app.run().await?, Err(e) => println!("{}", e), } Ok(()) } dtui-3.0.0/src/bin/dtui/messages.rs000064400000000000000000000031251046102023000152530ustar 00000000000000use std::collections::HashMap; use chumsky::chain::Chain; use zbus::{ names::{OwnedBusName, OwnedInterfaceName, OwnedMemberName}, zvariant::{OwnedObjectPath, OwnedValue}, }; use zbus_xml::Node; #[derive(Debug)] pub enum DbusMessage { GetObjects(OwnedBusName), ServiceRequest(), MethodCallRequest( OwnedBusName, OwnedObjectPath, OwnedInterfaceName, OwnedMemberName, Vec, ), } #[derive(Debug, Clone)] pub struct InvocationResponse { pub service: OwnedBusName, pub object_path: OwnedObjectPath, pub interface: OwnedInterfaceName, pub method_name: OwnedMemberName, pub message: zbus::Message, } #[derive(Debug, Clone)] pub struct DbusError { pub message: String, } /// Message from the Dbus Actor to the App. /// TODO: Needs better name, or it needs to be refactored into Action #[derive(Debug, Clone)] pub enum AppMessage { Objects((OwnedBusName, HashMap>)), // Service name + Map of (Object names, node) Services(Vec), InvocationResponse(InvocationResponse), Error(DbusError), } impl AppMessage { pub fn characters(&self) -> u16 { match self { AppMessage::Objects(object) => object.0.len() as u16, AppMessage::Services(owned_bus_names) => owned_bus_names.first().unwrap().len() as u16, AppMessage::InvocationResponse(invocation_response) => { invocation_response.message.body().len() as u16 } AppMessage::Error(dbus_error) => dbus_error.message.len() as u16, } } } dtui-3.0.0/src/bin/dtui/other.rs000064400000000000000000000003341046102023000145640ustar 00000000000000use ratatui::style::Color; // TODO: Rename this module to something else pub fn active_area_border_color(is_active: bool) -> Color { if is_active { Color::LightBlue } else { Color::White } } dtui-3.0.0/src/bin/dtui/parser.rs000064400000000000000000000506331046102023000147460ustar 00000000000000use chumsky::prelude::*; use std::{collections::HashMap, str::FromStr, u32}; use zbus::zvariant::{self, ObjectPath, Signature, StructureBuilder}; /// Create a parser from a Signature. /// The language that this parses is a human readable version of the dbus format. /// Arrays are delimited by [], with values seperated by "," /// Structure are delimited by (), with values seperated by "," /// Dictionaries are delimited by {}, and with keys and values seperated by ":" and pairs seperated by "," /// /// # Examples /// ["first", "second"] is a array with 2 string elements with a signature of "as" /// {"first": 1, "second": 2} is a dictionary with string as key type and key type of some number, it's signature is "a{su}" /// /// ``` /// let signature = Signature::from_str("as").unwrap(); /// let result = get_parser(signature).parse("[\"first\", \"second\"]"); /// assert_eq!(result, Ok(zvariant::Value::Array(vec!["first", "second"].into()))); /// ``` pub fn get_parser( signature: Signature, ) -> impl Parser, Error = Simple> { match signature { zvariant::Signature::Unit => todo!(), zvariant::Signature::U8 => parser_u8().boxed(), zvariant::Signature::Bool => parser_bool().boxed(), zvariant::Signature::I16 => parser_i16().boxed(), zvariant::Signature::U16 => parser_u16().boxed(), zvariant::Signature::I32 => parser_i32().boxed(), zvariant::Signature::U32 => parser_u32().boxed(), zvariant::Signature::I64 => parser_i64().boxed(), zvariant::Signature::U64 => parser_u64().boxed(), zvariant::Signature::F64 => parser_f64().boxed(), zvariant::Signature::Str => parser_string().boxed(), zvariant::Signature::Signature => parser_signature().boxed(), zvariant::Signature::ObjectPath => parser_object_path().boxed(), zvariant::Signature::Variant => parser_variant().boxed(), zvariant::Signature::Fd => parser_fd().boxed(), zvariant::Signature::Array(child) => parser_array(child.signature().clone()).boxed(), zvariant::Signature::Dict { key, value } => { parser_dict(key.signature().clone(), value.signature().clone()).boxed() } zvariant::Signature::Structure(fields) => parser_struct(fields).boxed(), } } fn parser_variant<'a>() -> impl Parser, Error = Simple> { parser_signature() .boxed() .then_ignore(just("->")) .then_with(|s| match s { zvariant::Value::Signature(signature) => { get_parser(signature).map(|variant| zvariant::Value::Value(Box::new(variant))) } _ => unreachable!(), }) } // fn parser_struct<'a>( structure: zvariant::signature::Fields, ) -> impl Parser, Error = Simple> { let mut element_parsers = structure .iter() .map(|signature: &zbus::zvariant::Signature| get_parser(signature.clone())); let mut full_parser = just('(').map(|_| Vec::>::new()).boxed(); // The map is there to get types to match as the chain in the loop needs the parser to output a Vec full_parser = full_parser.chain(element_parsers.next().unwrap()).boxed(); // The first doesnt get a ',' the rest do for element_parser in element_parsers { full_parser = full_parser .then_ignore(just(",").padded()) .chain(element_parser) .boxed(); } full_parser = full_parser.then_ignore(just(')').padded()).boxed(); full_parser.map(|fields| { let mut builder = StructureBuilder::new(); for field in fields { builder.push_value(field); } zvariant::Value::Structure(builder.build().unwrap()) }) } fn parser_dict<'a>( key_type: Signature, value_type: Signature, ) -> impl Parser, Error = Simple> { let key_parser = get_parser(key_type.clone()); let value_parser = get_parser(value_type.clone()); let member_parser = key_parser .then_ignore(just(":").padded()) .then(value_parser) .boxed(); member_parser .clone() .chain(just(',').padded().ignore_then(member_parser).repeated()) .or_not() .flatten() .delimited_by(just('{').padded(), just('}').padded()) .collect::, zvariant::Value<'_>>>() .map( move |m: HashMap, zvariant::Value<'_>>| { let mut dict = zvariant::Dict::new(&key_type, &value_type); for (k, v) in m { dict.append(k, v).expect("Could not append to key value pair, this should not happen if types are correct"); } zvariant::Value::Dict(dict) }, ) } fn parser_array<'a>( signature: Signature, ) -> impl Parser, Error = Simple> { let element_parser = get_parser(signature.clone()).boxed(); element_parser .clone() .chain( just(',') .padded() .ignore_then(element_parser.clone()) .repeated(), ) .or_not() .flatten() .delimited_by(just('['), just(']')) .map(move |v: Vec>| { let mut array: zvariant::Array<'_> = zvariant::Array::new(&signature); for element in v { array .append(element) .expect("The type was somehow incorrect in an inner array"); } zvariant::Value::Array(array) }) } // TODO: Can these be made generic, not sure how as they are generic over the Value type which is enums // TODO: Validation on sizes of numbers fn parser_u8() -> impl Parser, Error = Simple> { text::digits(10) .labelled("u8") .map(|s: String| zvariant::Value::U8(s.parse().unwrap())) .padded() } fn parser_u16() -> impl Parser, Error = Simple> { text::digits(10) .labelled("u16") .map(|s: String| zvariant::Value::U16(s.parse().unwrap())) .padded() } fn parser_i16() -> impl Parser, Error = Simple> { just('-') .or_not() .chain::(text::digits(10)) .collect::() .map(|s: String| zvariant::Value::I16(s.parse().unwrap())) .labelled("i16") .padded() } fn parser_u32() -> impl Parser, Error = Simple> { text::digits(10) .labelled("u32") .map(|s: String| zvariant::Value::U32(s.parse().unwrap())) .padded() } fn parser_i32() -> impl Parser, Error = Simple> { just('-') .or_not() .chain::(text::digits(10)) .collect::() .map(|s: String| zvariant::Value::I32(s.parse().unwrap())) .padded() } fn parser_u64() -> impl Parser, Error = Simple> { text::digits(10) .labelled("u64") .map(|s: String| zvariant::Value::U64(s.parse().unwrap())) .padded() } fn parser_i64() -> impl Parser, Error = Simple> { just('-') .or_not() .chain::(text::digits(10)) .collect::() .labelled("i64") .map(|s: String| zvariant::Value::I64(s.parse().unwrap())) .padded() } fn parser_f64() -> impl Parser, Error = Simple> { just('-') .or_not() .chain::(text::digits(10)) .chain::(just('.').chain(text::digits(10)).or_not().flatten()) .collect::() .labelled("f64") .map(|s: String| zvariant::Value::F64(s.parse().unwrap())) .padded() } fn parser_bool() -> impl Parser, Error = Simple> { just("true") .map(|_| zvariant::Value::Bool(true)) .or(just("false").map(|_| zvariant::Value::Bool(false))) .labelled("bool") .padded() } fn parser_string() -> impl Parser, Error = Simple> { let escape = just('\\').ignore_then( just('\\') .or(just('/')) .or(just('"')) .or(just('b').to('\x08')) .or(just('f').to('\x0C')) .or(just('n').to('\n')) .or(just('r').to('\r')) .or(just('t').to('\t')) .or(just('u').ignore_then( filter(|c: &char| c.is_ascii_hexdigit()) .repeated() .exactly(4) .collect::() .validate(|digits, span, emit| { char::from_u32(u32::from_str_radix(&digits, 16).unwrap()).unwrap_or_else( || { emit(Simple::custom(span, "invalid unicode character")); '\u{FFFD}' // unicode replacement character }, ) }), )), ); let string = just('"') .ignore_then(filter(|c| *c != '\\' && *c != '"').or(escape).repeated()) .then_ignore(just('"')) .collect::() .map(|s| zvariant::Value::Str(s.into())) .labelled("string"); string .recover_with(skip_then_retry_until(['}', ']'])) .padded() } fn parser_signature() -> impl Parser, Error = Simple> { let escape = just('\\').ignore_then( just('\\') .or(just('/')) .or(just('"')) .or(just('b').to('\x08')) .or(just('f').to('\x0C')) .or(just('n').to('\n')) .or(just('r').to('\r')) .or(just('t').to('\t')) .or(just('u').ignore_then( filter(|c: &char| c.is_ascii_hexdigit()) .repeated() .exactly(4) .collect::() .validate(|digits, span, emit| { char::from_u32(u32::from_str_radix(&digits, 16).unwrap()).unwrap_or_else( || { emit(Simple::custom(span, "invalid unicode character")); '\u{FFFD}' // unicode replacement character }, ) }), )), ); let string = just('"') .ignore_then(filter(|c| *c != '\\' && *c != '"').or(escape).repeated()) .then_ignore(just('"')) .collect::() .try_map(|digits, span| { if let Ok(signature) = Signature::from_str(digits.as_str()) { Ok(zvariant::Value::Signature(signature)) } else { Err(Simple::custom( span, "Could not parse signature from string value", )) } }) .labelled("signature"); string .recover_with(skip_then_retry_until(['}', ']'])) .padded() } fn parser_object_path() -> impl Parser, Error = Simple> { let escape = just('\\').ignore_then( just('\\') .or(just('/')) .or(just('"')) .or(just('b').to('\x08')) .or(just('f').to('\x0C')) .or(just('n').to('\n')) .or(just('r').to('\r')) .or(just('t').to('\t')) .or(just('u').ignore_then( filter(|c: &char| c.is_ascii_hexdigit()) .repeated() .exactly(4) .collect::() .validate(|digits, span, emit| { char::from_u32(u32::from_str_radix(&digits, 16).unwrap()).unwrap_or_else( || { emit(Simple::custom(span, "invalid unicode character")); '\u{FFFD}' // unicode replacement character }, ) }), )), ); let string = just('"') .ignore_then(filter(|c| *c != '\\' && *c != '"').or(escape).repeated()) .then_ignore(just('"')) .collect::() .try_map(|digits, span| { if let Ok(path) = ObjectPath::try_from(digits) { Ok(zvariant::Value::ObjectPath(path)) } else { Err(Simple::custom( span, "Could not parse object path from string value", )) } }) .labelled("object_path"); string .recover_with(skip_then_retry_until(['}', ']'])) .padded() } fn parser_fd() -> impl Parser, Error = Simple> { empty().try_map(|(), span| Err(Simple::custom(span, "Cannot parse file descriptors"))) } #[cfg(test)] fn test_generic_signature(src: &'static str, signature: &'static str, value: zvariant::Value) { use std::str::FromStr; let signature = Signature::from_str(signature).unwrap(); println!("{}", signature); let result = get_parser(signature).parse(src.trim()); dbg!(&result); dbg!(&value); assert_eq!(result, Ok(value)); } #[test] fn test_numbers() { test_generic_signature("5", "y", zvariant::Value::U8(5)); test_generic_signature("5", "n", zvariant::Value::I16(5)); test_generic_signature("-5", "n", zvariant::Value::I16(-5)); test_generic_signature("5", "q", zvariant::Value::U16(5)); test_generic_signature("5", "i", zvariant::Value::I32(5)); test_generic_signature("-5", "i", zvariant::Value::I32(-5)); test_generic_signature("5", "u", zvariant::Value::U32(5)); test_generic_signature("5", "x", zvariant::Value::I64(5)); test_generic_signature("-5", "x", zvariant::Value::I64(-5)); test_generic_signature("5", "t", zvariant::Value::U64(5)); } #[test] fn test_float() { test_generic_signature("5.0", "d", zvariant::Value::F64(5.0)); } #[test] fn test_string() { test_generic_signature(r#""asd""#, "s", zvariant::Value::Str("asd".into())); } #[test] fn test_signature() { test_generic_signature( r#""s""#, "g", zvariant::Value::Signature(Signature::try_from("s").unwrap()), ); test_generic_signature( r#""(ss)""#, "g", zvariant::Value::Signature(Signature::try_from("(ss)").unwrap()), ); test_generic_signature( r#""as""#, "g", zvariant::Value::Signature(Signature::try_from("as").unwrap()), ); use std::str::FromStr; let signature = Signature::from_str("g").unwrap(); let result = get_parser(signature).parse("k"); // k is not a valid signature assert!(result.is_err()); } #[test] fn test_object_path() { test_generic_signature( r#""/""#, "o", zvariant::Value::ObjectPath(ObjectPath::try_from("/").unwrap()), ); test_generic_signature( r#""/test""#, "o", zvariant::Value::ObjectPath(ObjectPath::try_from("/test").unwrap()), ); test_generic_signature( r#""/test/test2""#, "o", zvariant::Value::ObjectPath(ObjectPath::try_from("/test/test2").unwrap()), ); use std::str::FromStr; let signature = Signature::from_str("o").unwrap(); let result = get_parser(signature).parse("k"); // k is not a valid object path assert!(result.is_err()); let signature = Signature::from_str("o").unwrap(); let result = get_parser(signature).parse("//"); // // is not a valid object path assert!(result.is_err()); } #[test] fn test_bool() { test_generic_signature("true", "b", zvariant::Value::Bool(true)); test_generic_signature("false", "b", zvariant::Value::Bool(false)); } #[test] fn test_array() { test_generic_signature( "[1,2,3,4]", "ai", zvariant::Value::Array(vec![1, 2, 3, 4].into()), ); // One element test_generic_signature("[1]", "ai", zvariant::Value::Array(vec![1].into())); // array of array let expected_signature = Signature::from_str("ai").unwrap(); let mut expected = zvariant::Array::new(&expected_signature); expected .append(zvariant::Value::Array(vec![1].into())) .unwrap(); expected .append(zvariant::Value::Array(vec![2].into())) .unwrap(); expected .append(zvariant::Value::Array(vec![3].into())) .unwrap(); expected .append(zvariant::Value::Array(vec![4].into())) .unwrap(); test_generic_signature("[[1],[2],[3],[4]]", "aai", zvariant::Value::Array(expected)); // array of strings test_generic_signature( r#"["a","b","c","d"]"#, "as", zvariant::Value::Array(vec!["a", "b", "c", "d"].into()), ); // Array of structs fn one_element_struct(s: String) -> zvariant::Value<'static> { zvariant::Value::Structure( zvariant::StructureBuilder::new() .add_field(Into::::into(s)) .build() .unwrap(), ) } let struct_signature = Signature::Structure(zvariant::signature::Fields::Static { fields: &[&Signature::Str], }); let mut array = zvariant::Array::new(&struct_signature); let _ = array.append(one_element_struct("a".to_string())); let _ = array.append(one_element_struct("b".to_string())); let _ = array.append(one_element_struct("c".to_string())); let _ = array.append(one_element_struct("d".to_string())); test_generic_signature(r#"[("a"),("b"),("c"),("d")]"#, "a(s)", array.into()); // Array of dicts let mut array = zvariant::Array::new(&Signature::Dict { key: Signature::Str.into(), value: Signature::Str.into(), }); let _ = array.append(zvariant::Value::Dict( HashMap::from([("a", "1"), ("b", "2")]).into(), )); let _ = array.append(zvariant::Value::Dict( HashMap::from([("c", "3"), ("d", "4")]).into(), )); test_generic_signature( r#"[{"a": "1", "b":"2"}, {"c": "3", "d":"4"}]"#, "aa{ss}", array.into(), ); } #[test] fn test_dict() { test_generic_signature( r#"{"a": "b", "c":"d"}"#, "a{ss}", zvariant::Value::Dict(HashMap::from([("a", "b"), ("c", "d")]).into()), ) } #[test] fn test_struct() { test_generic_signature( r#"("5", 1)"#, "(si)", zvariant::Value::Structure(zvariant::Structure::from(("5", 1))), ); test_generic_signature( r#"("5", 1, 2, 3, 4, 5)"#, "(siiiii)", zvariant::Value::Structure(zvariant::Structure::from(("5", 1, 2, 3, 4, 5))), ); // One element structs let one_elemenent_structure = zvariant::StructureBuilder::new() .add_field(Into::::into("a")) .build() .unwrap(); test_generic_signature( r#"("a")"#, "(s)", zvariant::Value::Structure(one_elemenent_structure), ); // Struct with array let one_elemenent_structure = zvariant::StructureBuilder::new() .add_field(Into::::into(vec!["a"])) .build() .unwrap(); test_generic_signature( r#"(["a"])"#, "(as)", zvariant::Value::Structure(zvariant::Structure::from(one_elemenent_structure)), ); } #[test] fn test_variant() { test_generic_signature( r#""u"->5"#, "v", zvariant::Value::Value(Box::new(zvariant::Value::U32(5))), ); } // Test from examples in help view #[test] fn test_dict_from_examples() { test_generic_signature( r#"{"count": 5, "max": 10}"#, "a{si}", zvariant::Value::Dict(HashMap::from([("count", 5i32), ("max", 10i32)]).into()), ) } #[test] fn test_struct_from_examples() { test_generic_signature( r#"("user_name", "john", 42)"#, "(ssu)", zvariant::Value::Structure(zvariant::Structure::from(("user_name", "john", 42u32))), ) } #[test] fn test_array_of_struct_from_examples() { let struct_signature = Signature::Structure(zvariant::signature::Fields::Static { fields: &[&Signature::I32, &Signature::Str], }); let mut array = zvariant::Array::new(&struct_signature); let _ = array.append(zvariant::Value::Structure(zvariant::Structure::from(( 1, "a", )))); let _ = array.append(zvariant::Value::Structure(zvariant::Structure::from(( 2, "b", )))); test_generic_signature(r#"[(1,"a"),(2,"b")]"#, "a(is)", array.into()) } dtui-3.0.0/src/bin/dtui/stateful_list.rs000064400000000000000000000022741046102023000163320ustar 00000000000000use ratatui::widgets::ListState; pub struct StatefulList { pub state: ListState, pub items: Vec, } impl Default for StatefulList { fn default() -> Self { Self { state: Default::default(), items: Default::default(), } } } impl StatefulList { pub fn with_items(items: Vec) -> StatefulList { StatefulList { state: ListState::default(), items, } } pub fn next(&mut self) { let i = match self.state.selected() { Some(i) => { if i >= self.items.len() - 1 { 0 } else { i + 1 } } None => 0, }; self.state.select(Some(i)); } pub fn previous(&mut self) { let i = match self.state.selected() { Some(i) => { if i == 0 { self.items.len() - 1 } else { i - 1 } } None => 0, }; self.state.select(Some(i)); } pub fn unselect(&mut self) { self.state.select(None); } } dtui-3.0.0/src/bin/dtui/stateful_tree.rs000064400000000000000000000230471046102023000163170ustar 00000000000000use std::collections::HashMap; use itertools::Itertools; use serde::{Deserialize, Serialize}; use tui_tree_widget::{TreeItem, TreeState}; use zbus_names::{OwnedMemberName, OwnedPropertyName}; use zbus_xml::{Annotation, Arg, ArgDirection, Method, Node, Property}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum MemberTypes { Methods, Properties, Signals, } // This enum encodes where we are in the GUI tree // > Objects // > Interfaces // > Member (Aka one of Method/Property/Signal) // > Methods/Properties/Signals (The actual list of the methods) #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum DbusIdentifier { Object(String), // ObjectPath Interface(String), // InterfaceName Member(MemberTypes), // Can be Method, Properties, Signals Method(OwnedMethod), // zbus_name::MemberName Property(OwnedProperty), // zbus_name::PropertyName Signal(String), // zbus_name::MemberName } // OwnedMethod and OwnedProperty is zbus_xml::Method/Property but owned //TODO: Consider moving getting this or something similar into zbus_xml #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct OwnedMethod { pub(crate) name: OwnedMemberName, pub(crate) args: Vec, pub(crate) annotations: Vec, } impl From> for OwnedMethod { fn from(value: Method<'_>) -> Self { OwnedMethod { name: value.name().to_owned().into(), args: value.args().to_owned(), annotations: value.annotations().to_owned(), } } } impl OwnedMethod { pub fn name(&self) -> &OwnedMemberName { &self.name } pub fn args(&self) -> &Vec { &self.args } pub fn annotations(&self) -> &Vec { &self.annotations } } // Rely on PartialEq impl Eq for OwnedMethod {} impl std::hash::Hash for OwnedMethod { fn hash(&self, state: &mut H) { self.name().hash(state); } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct OwnedProperty { pub(crate) name: OwnedPropertyName, pub(crate) ty: zbus_xml::Signature, pub(crate) access: zbus_xml::PropertyAccess, pub(crate) annotations: Vec, } impl From> for OwnedProperty { fn from(value: Property<'_>) -> Self { Self { name: value.name().to_owned().into(), ty: value.ty().to_owned(), access: value.access().to_owned(), annotations: value.annotations().to_owned(), } } } impl OwnedProperty { pub fn name(&self) -> &OwnedPropertyName { &self.name } pub fn ty(&self) -> &zbus_xml::Signature { &self.ty } pub fn access(&self) -> &zbus_xml::PropertyAccess { &self.access } pub fn annotations(&self) -> &Vec { &self.annotations } } // Rely on PartialEq impl Eq for OwnedProperty {} impl std::hash::Hash for OwnedProperty { fn hash(&self, state: &mut H) { self.name().hash(state); } } impl Default for DbusIdentifier { fn default() -> Self { DbusIdentifier::Object("/".to_string()) } } #[derive(Debug)] pub struct StatefulTree { pub state: TreeState, pub items: Vec>, } impl Default for StatefulTree { fn default() -> Self { Self::new() } } impl StatefulTree { pub fn new() -> Self { Self { state: TreeState::default(), items: Vec::new(), } } pub fn from_nodes(nodes: HashMap>) -> Self { let items = nodes .iter() .sorted_by(|a, b| a.0.cmp(b.0)) .map(|(object_name, node)| -> TreeItem { let children = node_to_treeitems(node); TreeItem::new( DbusIdentifier::Object(object_name.clone()), object_name.clone(), children, ) .unwrap() }) .collect(); Self { state: TreeState::default(), items, } } pub fn down(&mut self) { self.state.key_down(); } pub fn up(&mut self) { self.state.key_up(); } pub fn left(&mut self) { self.state.key_left(); } pub fn right(&mut self) { self.state.key_right(); } pub fn toggle(&mut self) { self.state.toggle_selected(); } } fn node_to_treeitems(node: &zbus_xml::Node<'static>) -> Vec> { let children: Vec> = node .interfaces() .iter() .map(|interface| { let methods: Vec> = interface .methods() .iter() .cloned() .map(|method| { let inputs: Vec = method .args() .iter() .filter(|arg| arg.direction().is_some_and(|s| s == ArgDirection::In)) .map(|arg| { format!( "{}: {}", arg.name().unwrap_or_default(), arg.ty().to_string() ) }) .collect(); let outputs: Vec = method .args() .iter() .filter(|arg| arg.direction().is_some_and(|s| s == ArgDirection::Out)) .map(|arg| { format!( "{}: {}", arg.name().unwrap_or_default(), arg.ty().to_string() ) }) .collect(); let return_arrow = if outputs.is_empty() { "" } else { "=>" }; // If we dont return anything, the arrow shouldnt be there let leaf_string: String = format!( "{}({}) {} {}", method.name(), inputs.join(", "), return_arrow, outputs.join(", ") ); TreeItem::new_leaf(DbusIdentifier::Method(method.into()), leaf_string) }) .collect(); let properties: Vec> = interface .properties() .iter() .map(|property| { TreeItem::new_leaf( DbusIdentifier::Property(property.clone().into()), format!("{}: {}", property.name(), property.ty().to_string()), ) }) .collect(); let signals: Vec> = interface .signals() .iter() .map(|signal| { // Signals can only have input parameters let inputs: Vec = signal .args() .iter() .filter(|arg| arg.direction().is_some_and(|s| s == ArgDirection::In)) .map(|arg| { format!( "{}: {}", arg.name().unwrap_or_default(), arg.ty().to_string() ) }) .collect(); let leaf_string: String = format!("{}({})", signal.name(), inputs.join(", ")); TreeItem::new_leaf( DbusIdentifier::Signal(signal.name().to_string()), leaf_string, ) }) .collect(); // let annotations: Vec = interface // .annotations() // .iter() // .map(|annotation| { // TreeItem::new_leaf(annotation.name().to_string()) // }) // .collect(); let methods_tree = TreeItem::new( DbusIdentifier::Member(MemberTypes::Methods), "Methods", methods, ) .expect("Methods should have different ids"); let properties_tree = TreeItem::new( DbusIdentifier::Member(MemberTypes::Properties), "Properties", properties, ) .expect("Properties should have different ids"); let signals_tree = TreeItem::new( DbusIdentifier::Member(MemberTypes::Signals), "Signals", signals, ) .expect("Signals should have different ids"); // let annotations_tree = // TreeItem::new("Annotations", annotations); // TODO: Annotations are used differently, so i dont want to waste space with it TreeItem::new( DbusIdentifier::Interface(interface.name().to_string()), interface.name().to_string(), vec![ methods_tree, properties_tree, signals_tree, // annotations_tree, ], ) .unwrap() }) .collect(); children } dtui-3.0.0/src/bin/dtui/tui.rs000064400000000000000000000153111046102023000142450ustar 00000000000000#![allow(dead_code)] // Remove this once you start using the code use std::{ io::{Stdout, stdout}, ops::{Deref, DerefMut}, time::Duration, }; use color_eyre::Result; use crossterm::{ cursor, event::{ DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, Event as CrosstermEvent, EventStream, KeyEvent, KeyEventKind, MouseEvent, }, terminal::{EnterAlternateScreen, LeaveAlternateScreen}, }; use futures::{FutureExt, StreamExt}; use ratatui::backend::CrosstermBackend as Backend; use serde::{Deserialize, Serialize}; use tokio::{ sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, task::JoinHandle, time::interval, }; use tokio_util::sync::CancellationToken; use tracing::error; #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Event { Init, Quit, Error, Closed, Tick, Render, FocusGained, FocusLost, Paste(String), Key(KeyEvent), Mouse(MouseEvent), Resize(u16, u16), } pub struct Tui { pub terminal: ratatui::Terminal>, pub task: JoinHandle<()>, pub cancellation_token: CancellationToken, pub event_rx: UnboundedReceiver, pub event_tx: UnboundedSender, pub frame_rate: f64, pub tick_rate: f64, pub mouse: bool, pub paste: bool, } impl Tui { pub fn new() -> Result { let (event_tx, event_rx) = mpsc::unbounded_channel(); Ok(Self { terminal: ratatui::Terminal::new(Backend::new(stdout()))?, task: tokio::spawn(async {}), cancellation_token: CancellationToken::new(), event_rx, event_tx, frame_rate: 60.0, tick_rate: 4.0, mouse: false, paste: false, }) } pub fn tick_rate(mut self, tick_rate: f64) -> Self { self.tick_rate = tick_rate; self } pub fn frame_rate(mut self, frame_rate: f64) -> Self { self.frame_rate = frame_rate; self } pub fn mouse(mut self, mouse: bool) -> Self { self.mouse = mouse; self } pub fn paste(mut self, paste: bool) -> Self { self.paste = paste; self } pub fn start(&mut self) { self.cancel(); // Cancel any existing task self.cancellation_token = CancellationToken::new(); let event_loop = Self::event_loop( self.event_tx.clone(), self.cancellation_token.clone(), self.tick_rate, self.frame_rate, ); self.task = tokio::spawn(async { event_loop.await; }); } async fn event_loop( event_tx: UnboundedSender, cancellation_token: CancellationToken, tick_rate: f64, frame_rate: f64, ) { let mut event_stream = EventStream::new(); let mut tick_interval = interval(Duration::from_secs_f64(1.0 / tick_rate)); let mut render_interval = interval(Duration::from_secs_f64(1.0 / frame_rate)); // if this fails, then it's likely a bug in the calling code event_tx .send(Event::Init) .expect("failed to send init event"); loop { let event = tokio::select! { _ = cancellation_token.cancelled() => { break; } _ = tick_interval.tick() => Event::Tick, _ = render_interval.tick() => Event::Render, crossterm_event = event_stream.next().fuse() => match crossterm_event { Some(Ok(event)) => match event { CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => Event::Key(key), CrosstermEvent::Mouse(mouse) => Event::Mouse(mouse), CrosstermEvent::Resize(x, y) => Event::Resize(x, y), CrosstermEvent::FocusLost => Event::FocusLost, CrosstermEvent::FocusGained => Event::FocusGained, CrosstermEvent::Paste(s) => Event::Paste(s), _ => continue, // ignore other events } Some(Err(_)) => Event::Error, None => break, // the event stream has stopped and will not produce any more events }, }; if event_tx.send(event).is_err() { // the receiver has been dropped, so there's no point in continuing the loop break; } } cancellation_token.cancel(); } pub fn stop(&self) -> Result<()> { self.cancel(); let mut counter = 0; while !self.task.is_finished() { std::thread::sleep(Duration::from_millis(1)); counter += 1; if counter > 50 { self.task.abort(); } if counter > 100 { error!("Failed to abort task in 100 milliseconds for unknown reason"); break; } } Ok(()) } pub fn enter(&mut self) -> Result<()> { crossterm::terminal::enable_raw_mode()?; crossterm::execute!(stdout(), EnterAlternateScreen, cursor::Hide)?; if self.mouse { crossterm::execute!(stdout(), EnableMouseCapture)?; } if self.paste { crossterm::execute!(stdout(), EnableBracketedPaste)?; } self.start(); Ok(()) } pub fn exit(&mut self) -> Result<()> { self.stop()?; if crossterm::terminal::is_raw_mode_enabled()? { self.flush()?; if self.paste { crossterm::execute!(stdout(), DisableBracketedPaste)?; } if self.mouse { crossterm::execute!(stdout(), DisableMouseCapture)?; } crossterm::execute!(stdout(), LeaveAlternateScreen, cursor::Show)?; crossterm::terminal::disable_raw_mode()?; } Ok(()) } pub fn cancel(&self) { self.cancellation_token.cancel(); } pub fn suspend(&mut self) -> Result<()> { self.exit()?; #[cfg(not(windows))] signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; Ok(()) } pub fn resume(&mut self) -> Result<()> { self.enter()?; Ok(()) } pub async fn next_event(&mut self) -> Option { self.event_rx.recv().await } } impl Deref for Tui { type Target = ratatui::Terminal>; fn deref(&self) -> &Self::Target { &self.terminal } } impl DerefMut for Tui { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.terminal } } impl Drop for Tui { fn drop(&mut self) { self.exit().unwrap(); } }