diff --git a/Cargo.lock b/Cargo.lock index a7716c1..ae0860a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,27 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.83" @@ -155,7 +176,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.10.0", ] [[package]] @@ -164,10 +185,10 @@ version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -182,12 +203,92 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.4.1", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.48", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "dirs" version = "5.0.1" @@ -209,6 +310,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -221,6 +344,18 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "getrandom" version = "0.2.12" @@ -242,13 +377,24 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashlink" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown", + "hashbrown 0.14.3", ] [[package]] @@ -257,6 +403,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "iana-time-zone" version = "0.1.59" @@ -280,6 +432,47 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "instability" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b829f37dead9dc39df40c2d3376c179fdfd2ac771f53f55d3c30dc096a3c0c6e" +dependencies = [ + "darling", + "indoc", + "pretty_assertions", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + [[package]] name = "js-sys" version = "0.3.67" @@ -291,9 +484,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.152" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "libredox" @@ -303,7 +496,7 @@ checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ "bitflags 2.4.1", "libc", - "redox_syscall", + "redox_syscall 0.4.1", ] [[package]] @@ -323,6 +516,22 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -332,6 +541,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -353,6 +583,46 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "papergrid" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b0f8def1f117e13c895f3eda65a7b5650688da29d6ad04635f61bc7b92eebd" +dependencies = [ + "bytecount", + "fnv", + "unicode-width 0.2.0", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.7", + "smallvec", + "windows-targets 0.52.0", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pkg-config" version = "0.3.28" @@ -365,11 +635,43 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -413,6 +715,27 @@ dependencies = [ "getrandom", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.4.1", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -422,6 +745,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.4.1", +] + [[package]] name = "redox_users" version = "0.4.4" @@ -447,6 +779,37 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustix" +version = "0.38.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "shellexpand" version = "3.1.0" @@ -456,6 +819,36 @@ dependencies = [ "dirs", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simpledns" version = "0.1.0" @@ -463,8 +856,10 @@ dependencies = [ "chrono", "clap", "rand", + "ratatui", "rusqlite", "shellexpand", + "tabled", "yaml-rust", ] @@ -474,12 +869,57 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2593d31f82ead8df961d8bd23a64c2ccf2eb5dd34b0a34bfb4dd54011c72009e" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.48", +] + +[[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.48" @@ -491,6 +931,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tabled" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6709222f3973137427ce50559cd564dc187a95b9cfe01613d2f4e93610e510a" +dependencies = [ + "papergrid", + "tabled_derive", +] + +[[package]] +name = "tabled_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "931be476627d4c54070a1f3a9739ccbfec9b36b39815106a20cce2243bbcefe1" +dependencies = [ + "heck 0.4.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "thiserror" version = "1.0.56" @@ -508,7 +971,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -517,6 +980,35 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +[[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", + "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.1" @@ -562,7 +1054,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -584,7 +1076,7 @@ checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -595,6 +1087,28 @@ version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +[[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-core" version = "0.52.0" @@ -745,6 +1259,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.32" @@ -762,5 +1282,5 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] diff --git a/Cargo.toml b/Cargo.toml index e9be1a7..c030d2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,19 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = "0.8.5" -yaml-rust = "0.4" -clap = { version = "4.4.16", features = ["derive"] } chrono = "0.4.31" +clap = { version = "4.4.16", features = ["derive"] } +rand = "0.8.5" +ratatui = { version = "0.29.0", optional = true } rusqlite = { version = "0.30.0", features = ["bundled"] } shellexpand = "3.1.0" +tabled = "0.17.0" +yaml-rust = "0.4" + +[features] +default = ["log_info", "log_warn", "log_error"] +log_info = [] +log_debug = [] +log_warn = [] +log_error = [] +tui = ["dep:ratatui"] diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..db86508 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,213 @@ +use std::error::Error; +use std::io::{stdin, stdout, Write}; +use std::net::Ipv4Addr; + +use std::str::FromStr; + +use tabled::{builder::Builder, settings::Style}; + +use crate::{log_info, log_debug}; +use crate::{dns_packet::{DnsQueryType, DnsRecord, DnsRecordA, DnsRecordAAAA, DnsRecordCNAME, DnsRecordDROP, DnsRecordMX, DnsRecordNS, DnsRecordPreamble}, settings::DnsSettings, simple_database::SimpleDatabase, RecordArgs, RecordFilters}; + +pub fn add_record(args: RecordArgs, settings: DnsSettings) -> Result<(), Box> { + let domain = args.domain.unwrap(); + let query_type = args.query_type.unwrap().into(); + let preamble = DnsRecordPreamble::build(domain, query_type, args.class, args.ttl); + let record = match query_type { + DnsQueryType::Unknown(_) => panic!("Impossible state"), + DnsQueryType::A => DnsRecord::A(DnsRecordA::new(preamble, Ipv4Addr::from_str(args.ip.unwrap().as_str()).expect("Couldn't parse ipv4 address"))), + DnsQueryType::NS => DnsRecord::NS(DnsRecordNS::new(preamble, args.host.unwrap())), + DnsQueryType::CNAME => DnsRecord::CNAME(DnsRecordCNAME::new(preamble, args.host.unwrap())), + DnsQueryType::MX => DnsRecord::MX(DnsRecordMX::new(preamble, args.priority.unwrap(), args.host.unwrap())), + DnsQueryType::AAAA => DnsRecord::AAAA(DnsRecordAAAA::new(preamble, Ipv4Addr::from_str(args.ip.unwrap().as_str()).expect("Couldn't parse ipv4 address"))), + DnsQueryType::DROP => DnsRecord::DROP(DnsRecordDROP::new(preamble)), + }; + let database = SimpleDatabase::new(settings.database_file); + database.insert_record(record.clone(), false)?; + log_info!("Successfully added record: {:?}", record); + Ok(()) +} + +pub fn add_record_interactive(settings: DnsSettings) -> Result<(), Box> { + let domain = get_input("Domain: ", None, "A domain is required.", |x| !x.is_empty()); + let query_type = DnsQueryType::from_string(get_input("Record Type: ", + None, + "A record type is required [A, NS, CNAME, MX, AAAA, DROP]", + |x| ["A", "NS", "CNAME", "MX", "AAAA", "DROP"].contains(&x.to_uppercase().as_str())).as_str()); + let class = get_input("Class [default 1]: ", + Some("1".to_string()), + "A valid u16 must be supplied.", + |x| !x.is_empty() && x.parse::().is_ok()).parse::().unwrap(); + let ttl = get_input("TTL [default 300]: ", + Some("300".to_string()), + "A valid u32 must be supplied.", + |x| !x.is_empty() && x.parse::().is_ok()).parse::().unwrap(); + let preamble = DnsRecordPreamble::build(domain, query_type, class, ttl); + let record = match query_type { + DnsQueryType::Unknown(_) => panic!("Impossible state"), + DnsQueryType::A => { + let ip = get_input("IP: ", None, "A valid ip address is required.", |x| Ipv4Addr::from_str(x.as_str()).is_ok()); + DnsRecord::A(DnsRecordA::new(preamble, Ipv4Addr::from_str(ip.as_str()).unwrap())) + } + DnsQueryType::NS => { + let host = get_input("Host: ", None, "A host is required.", |x| !x.is_empty()); + DnsRecord::NS(DnsRecordNS::new(preamble, host)) + } + DnsQueryType::CNAME => { + let host = get_input("Host: ", None, "A host is required.", |x| !x.is_empty()); + DnsRecord::CNAME(DnsRecordCNAME::new(preamble, host)) + } + DnsQueryType::MX => { + let host = get_input("Host: ", None, "A host is required.", |x| !x.is_empty()); + let priority = get_input("Priority: ", None, "A valid u16 priority is required.", |x| !x.is_empty() && x.parse::().is_ok()).parse::().unwrap(); + DnsRecord::MX(DnsRecordMX::new(preamble, priority, host)) + } + DnsQueryType::AAAA => { + let ip = get_input("IP: ", None, "A valid ip address is required.", |x| Ipv4Addr::from_str(x.as_str()).is_ok()); + DnsRecord::AAAA(DnsRecordAAAA::new(preamble, Ipv4Addr::from_str(ip.as_str()).unwrap())) + } + DnsQueryType::DROP => DnsRecord::DROP(DnsRecordDROP::new(preamble)) + }; + let database = SimpleDatabase::new(settings.database_file); + database.insert_record(record.clone(), false)?; + log_info!("Successfully added record: {:?}", record); + Ok(()) +} + +fn get_input(message: &str, default_value: Option, error_message: &str, validate: fn(String) -> bool) -> String { + loop { + let mut s = String::new(); + print!("{}", message); + let _ = stdout().flush(); + let _ = stdin().read_line(&mut s).expect("Did not enter a correct string"); + s = s.trim_matches(&['\r', '\n', ' ', '\t']).to_string(); + if validate(s.clone()) { + return s; + } else if let Some(value) = default_value.clone() { + return value; + } + println!("{}", error_message); + } +} + +fn print_table(records: Vec) { + let mut builder = Builder::new(); + builder.push_record(["Type", "Domain", "Host/IP", "Priority", "TTL", "Class"]); + for record in records { + builder.push_record(match record { + DnsRecord::Unknown(dns_record_unknown) => [ + dns_record_unknown.preamble.query_type.into(), + dns_record_unknown.preamble.domain, + "".to_owned(), + "".to_owned(), + dns_record_unknown.preamble.ttl.to_string(), + dns_record_unknown.preamble.class.to_string() + ], + DnsRecord::A(dns_record_a) => [ + dns_record_a.preamble.query_type.into(), + dns_record_a.preamble.domain, + dns_record_a.ip.to_string(), + "".to_owned(), + dns_record_a.preamble.ttl.to_string(), + dns_record_a.preamble.class.to_string() + ], + DnsRecord::NS(dns_record_ns) => [ + dns_record_ns.preamble.query_type.into(), + dns_record_ns.preamble.domain, + dns_record_ns.host, + "".to_owned(), + dns_record_ns.preamble.ttl.to_string(), + dns_record_ns.preamble.class.to_string() + ], + DnsRecord::CNAME(dns_record_cname) => [ + dns_record_cname.preamble.query_type.into(), + dns_record_cname.preamble.domain, + dns_record_cname.host, + "".to_owned(), + dns_record_cname.preamble.ttl.to_string(), + dns_record_cname.preamble.class.to_string() + ], + DnsRecord::MX(dns_record_mx) => [ + dns_record_mx.preamble.query_type.into(), + dns_record_mx.preamble.domain, + dns_record_mx.host, + dns_record_mx.priority.to_string(), + dns_record_mx.preamble.ttl.to_string(), + dns_record_mx.preamble.class.to_string() + ], + DnsRecord::AAAA(dns_record_aaaa) => [ + dns_record_aaaa.preamble.query_type.into(), + dns_record_aaaa.preamble.domain, + dns_record_aaaa.ip.to_string(), + "".to_owned(), + dns_record_aaaa.preamble.ttl.to_string(), + dns_record_aaaa.preamble.class.to_string() + ], + DnsRecord::DROP(dns_record_drop) => [ + dns_record_drop.preamble.query_type.into(), + dns_record_drop.preamble.domain, + "".to_owned(), + "".to_owned(), + dns_record_drop.preamble.ttl.to_string(), + dns_record_drop.preamble.class.to_string() + ], + }); + } + let mut table = builder.build(); + table.with(Style::empty()); + println!("{}", table.to_string()) +} + +pub fn list_records<'a>(settings: DnsSettings, filters: RecordFilters) -> Result<(), Box> { + let database = SimpleDatabase::new(settings.database_file); + let records = database.get_all_records()?; + + // TODO make the filtering happen in the database + let mut filtered_records = Vec::new(); + for record in records { + match &filters.query_type { + Some(query_type) if record.get_query_type() != query_type.clone().into() => break, + _ => {} + }; + match &filters.domain { + Some(domain) if record.get_preamble().domain != *domain => break, + _ => {} + }; + match &filters.class { + Some(class) if record.get_preamble().class != *class => break, + _ => {} + }; + match &filters.ttl { + Some(ttl) if record.get_preamble().ttl != *ttl => break, + _ => {} + }; + match &filters.priority { + Some(priority) => match record { + DnsRecord::MX(mx) if mx.priority != *priority => break, + _ => {} + } + _ => {} + }; + match &filters.ip { + Some(ip) => match record { + DnsRecord::A(a) if a.ip != Ipv4Addr::from_str(ip.as_str())? => break, + DnsRecord::AAAA(aaaa) if aaaa.ip != Ipv4Addr::from_str(ip.as_str())? => break, + _ => {} + } + _ => {} + }; + match &filters.host { + Some(host) => match record { + DnsRecord::CNAME(cname) if cname.host != *host => break, + DnsRecord::MX(mx) if mx.host != *host => break, + DnsRecord::NS(ns) if ns.host != *host => break, + _ => {} + } + _ => {} + } + filtered_records.push(record); + } + + print_table(filtered_records); + Ok(()) +} diff --git a/src/dns_packet.rs b/src/dns_packet.rs index 24e9a27..d115b60 100644 --- a/src/dns_packet.rs +++ b/src/dns_packet.rs @@ -1,6 +1,9 @@ use std::io::{Error, ErrorKind}; use std::net::Ipv4Addr; +#[cfg(feature = "tui")] +use ratatui::widgets::Row; + #[derive(Clone, Debug)] pub struct DnsPacket { pub header: DnsHeader, @@ -433,6 +436,61 @@ impl DnsRecord { DnsRecord::DROP(_) => Vec::new(), } } + + #[cfg(feature = "tui")] + pub fn to_row(&self) -> Row<'_> { + match self { + DnsRecord::Unknown(dns_record_unknown) => todo!(), + DnsRecord::A(dns_record_a) => Row::new(vec![ + dns_record_a.preamble.query_type.into(), + dns_record_a.preamble.domain.to_string(), + dns_record_a.ip.to_string(), + "".to_owned(), + dns_record_a.preamble.ttl.to_string(), + dns_record_a.preamble.class.to_string(), + ]), + DnsRecord::NS(dns_record_ns) => Row::new(vec![ + dns_record_ns.preamble.query_type.into(), + dns_record_ns.preamble.domain.to_string(), + dns_record_ns.host.to_string(), + "".to_owned(), + dns_record_ns.preamble.ttl.to_string(), + dns_record_ns.preamble.class.to_string(), + ]), + DnsRecord::CNAME(dns_record_cname) => Row::new(vec![ + dns_record_cname.preamble.query_type.into(), + dns_record_cname.preamble.domain.to_string(), + dns_record_cname.host.to_string(), + "".to_owned(), + dns_record_cname.preamble.ttl.to_string(), + dns_record_cname.preamble.class.to_string(), + ]), + DnsRecord::MX(dns_record_mx) => Row::new(vec![ + dns_record_mx.preamble.query_type.into(), + dns_record_mx.preamble.domain.to_string(), + dns_record_mx.host.to_string(), + dns_record_mx.priority.to_string(), + dns_record_mx.preamble.ttl.to_string(), + dns_record_mx.preamble.class.to_string(), + ]), + DnsRecord::AAAA(dns_record_aaaa) => Row::new(vec![ + dns_record_aaaa.preamble.query_type.into(), + dns_record_aaaa.preamble.domain.to_string(), + dns_record_aaaa.ip.to_string(), + "".to_owned(), + dns_record_aaaa.preamble.ttl.to_string(), + dns_record_aaaa.preamble.class.to_string(), + ]), + DnsRecord::DROP(dns_record_drop) => Row::new(vec![ + dns_record_drop.preamble.query_type.into(), + dns_record_drop.preamble.domain.to_string(), + "".to_owned(), + "".to_owned(), + dns_record_drop.preamble.ttl.to_string(), + dns_record_drop.preamble.class.to_string(), + ]) + } + } } #[derive(Copy, Clone, Debug, PartialEq)] @@ -484,6 +542,48 @@ impl DnsQueryType { } } +impl From for DnsQueryType { + fn from(value: String) -> Self { + match value.to_uppercase().as_str() { + "A" => DnsQueryType::A, + "NS" => DnsQueryType::NS, + "CNAME" => DnsQueryType::CNAME, + "MX" => DnsQueryType::MX, + "AAAA" => DnsQueryType::AAAA, + "DROP" => DnsQueryType::DROP, + _ => DnsQueryType::Unknown(0), + } + } +} + +impl From<&str> for DnsQueryType { + fn from(value: &str) -> Self { + match value.to_uppercase().as_str() { + "A" => DnsQueryType::A, + "NS" => DnsQueryType::NS, + "CNAME" => DnsQueryType::CNAME, + "MX" => DnsQueryType::MX, + "AAAA" => DnsQueryType::AAAA, + "DROP" => DnsQueryType::DROP, + _ => DnsQueryType::Unknown(0), + } + } +} + +impl From for String { + fn from(value: DnsQueryType) -> Self { + match value { + DnsQueryType::Unknown(x) => format!("?? ({})", x), + DnsQueryType::A => "A".to_string(), + DnsQueryType::NS => "NS".to_string(), + DnsQueryType::CNAME => "CNAME".to_string(), + DnsQueryType::MX => "MX".to_string(), + DnsQueryType::AAAA => "AAAA".to_string(), + DnsQueryType::DROP => "DROP".to_string(), + } + } +} + #[derive(Clone, Debug)] pub struct DnsRecordPreamble { pub domain: String, diff --git a/src/dns_resolver.rs b/src/dns_resolver.rs index 7aa56bf..a3ffadf 100644 --- a/src/dns_resolver.rs +++ b/src/dns_resolver.rs @@ -1,8 +1,7 @@ use crate::dns_packet::{DnsPacket, DnsQueryType, DnsQuestion, DnsRecord, DnsResponseCode}; -use crate::settings::DnsSettings; use crate::simple_database::SimpleDatabase; use crate::{ignore_result_and_log_error, log_debug, log_error, log_info}; -use std::io::Error; +use std::error::Error; use std::net::UdpSocket; pub struct DnsResolver { @@ -18,7 +17,7 @@ impl DnsResolver { } } - pub fn answer_question(&self, request: DnsPacket) -> DnsPacket { + pub fn answer_question(&self, request: DnsPacket) -> Result> { let mut packet = DnsPacket::new(); packet.header.id = request.header.id; packet.header.recurse_desired = true; @@ -31,7 +30,7 @@ impl DnsResolver { match self .database - .get_records(question.name.clone(), question.query_type) + .get_records(question.name.clone()) { Ok(mut records) if !records.is_empty() => { packet.question_section.push(question.clone()); @@ -50,73 +49,10 @@ impl DnsResolver { log_debug!("Found records: {:?}", records); } } - Ok(_) => match self.remote_lookup(&question.name, question.query_type) { - Ok(result) => { - packet.question_section.push(question.clone()); - packet.header.question_count += 1; - packet.header.response_code = result.header.response_code; - - for ans in result.answer_section { - log_debug!("Answer: {:?}", ans); - packet.answer_section.push(ans.clone()); - ignore_result_and_log_error!(self.database.insert_record(ans, true)); - packet.header.answer_count += 1; - } - - for auth in result.authority_section { - log_debug!("Authority: {:?}", auth); - packet.authority_section.push(auth.clone()); - ignore_result_and_log_error!(self.database.insert_record(auth, true)); - packet.header.authority_count += 1; - } - - for add in result.additional_section { - log_debug!("Resource: {:?}", add); - packet.additional_section.push(add.clone()); - ignore_result_and_log_error!(self.database.insert_record(add, true)); - packet.header.additional_count += 1; - } - } - Err(error) => { - log_error!("AW CRAP :( {:#?}", error); - packet.header.response_code = DnsResponseCode::SERVFAIL; - } - }, + Ok(_) => self.do_remote_lookup(question, &mut packet)?, Err(error) => { - log_error!("Database error :( | {}", error); - // TODO fix duplicate code :( - match self.remote_lookup(&question.name, question.query_type) { - Ok(result) => { - packet.question_section.push(question.clone()); - packet.header.question_count += 1; - packet.header.response_code = result.header.response_code; - - for ans in result.answer_section { - log_debug!("Answer: {:?}", ans); - packet.answer_section.push(ans.clone()); - ignore_result_and_log_error!(self.database.insert_record(ans, true)); - packet.header.answer_count += 1; - } - - for auth in result.authority_section { - log_debug!("Authority: {:?}", auth); - packet.authority_section.push(auth.clone()); - ignore_result_and_log_error!(self.database.insert_record(auth, true)); - packet.header.authority_count += 1; - } - - for add in result.additional_section { - log_debug!("Resource: {:?}", add); - packet.additional_section.push(add.clone()); - ignore_result_and_log_error!(self.database.insert_record(add, true)); - packet.header.additional_count += 1; - } - } - Err(error) => { - log_error!("AW CRAP :( {}", error); - packet.header.response_code = DnsResponseCode::SERVFAIL; - } - } + log_error!("Database error :( {}", error); + self.do_remote_lookup(question, &mut packet)?; } } } else { @@ -124,25 +60,57 @@ impl DnsResolver { packet.header.response_code = DnsResponseCode::FORMERR; } - packet + Ok(packet) } - fn remote_lookup(&self, query_name: &str, query_type: DnsQueryType) -> Result { + fn do_remote_lookup(&self, question: &DnsQuestion, packet: &mut DnsPacket) -> Result<(), Box> { let server = (self.database.get_random_remote_lookup_server().unwrap(), 53); let socket = UdpSocket::bind(("0.0.0.0", self.remote_lookup_port))?; - let mut packet = DnsPacket::new(); - packet.header.recurse_desired = true; - packet.add_question(DnsQuestion::new(query_name.to_string(), query_type)); - let packet_bytes = packet.to_bytes(); + let mut remote_packet = DnsPacket::new(); + remote_packet.header.recurse_desired = true; + remote_packet.add_question(DnsQuestion::new(question.name.clone(), question.query_type)); + let remote_packet_bytes = packet.to_bytes(); - socket.send_to(&packet_bytes, server)?; + socket.send_to(&remote_packet_bytes, server)?; let mut res: [u8; 512] = [0; 512]; socket.recv_from(&mut res)?; - DnsPacket::from_bytes(&res) + match DnsPacket::from_bytes(&res) { + Ok(result) => { + packet.question_section.push(question.clone()); + packet.header.question_count += 1; + packet.header.response_code = result.header.response_code; + + for ans in result.answer_section { + log_debug!("Answer: {:?}", ans); + packet.answer_section.push(ans.clone()); + ignore_result_and_log_error!(self.database.insert_record(ans, true)); + packet.header.answer_count += 1; + } + + for auth in result.authority_section { + log_debug!("Authority: {:?}", auth); + packet.authority_section.push(auth.clone()); + ignore_result_and_log_error!(self.database.insert_record(auth, true)); + packet.header.authority_count += 1; + } + + for add in result.additional_section { + log_debug!("Resource: {:?}", add); + packet.additional_section.push(add.clone()); + ignore_result_and_log_error!(self.database.insert_record(add, true)); + packet.header.additional_count += 1; + } + } + Err(error) => { + log_error!("AW CRAP :( {:#?}", error); + packet.header.response_code = DnsResponseCode::SERVFAIL; + } + } + Ok(()) } /* TODO diff --git a/src/dns_server.rs b/src/dns_server.rs index a5f778e..721dfb9 100644 --- a/src/dns_server.rs +++ b/src/dns_server.rs @@ -67,10 +67,15 @@ impl DnsServer for DnsUdpServer { // process request let resolver = DnsResolver::new(settings.database_file.clone(), settings.remote_lookup_port); - let response_packet = resolver.answer_question(request_packet); - - // send result back - ignore_result_and_log_error!(socket_clone.send_to(response_packet.to_bytes().as_slice(), source)); + + match resolver.answer_question(request_packet) { + Ok(result) => { + ignore_result_and_log_error!(socket_clone.send_to(result.to_bytes().as_slice(), source)); + } + Err(error) => { + log_error!("Resolver error {}", error) + } + } } })?; } @@ -152,10 +157,15 @@ impl DnsServer for DnsTcpServer { let request = return_result_or_log_error_continue!(DnsPacket::from_bytes(packet_buffer.as_slice()), "Failed to parse packet from buffer"); let resolver = DnsResolver::new(settings.database_file.clone(), settings.remote_lookup_port); - let result = resolver.answer_question(request); - - ignore_result_or_log_error_continue!(stream.write(result.to_bytes().as_slice()), "Failed writing result back to buffer"); - ignore_result_or_log_error_continue!(stream.shutdown(Shutdown::Both), "Failed shutting down tcp connection"); + match resolver.answer_question(request) { + Ok(result) => { + ignore_result_or_log_error_continue!(stream.write(result.to_bytes().as_slice()), "Failed writing result back to buffer"); + ignore_result_or_log_error_continue!(stream.shutdown(Shutdown::Both), "Failed shutting down tcp connection"); + } + Err(error) => { + log_error!("Resolver error {}", error) + } + } })?; } diff --git a/src/macros.rs b/src/macros.rs index c3e475d..b3342a2 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,28 +1,36 @@ #[macro_export] macro_rules! log_info { ($($args: tt)*) => { - println!("\u{001b}[36m[INFO] {}: {}\u{001b}[0m", Into::>::into(std::time::SystemTime::now()).format("%Y/%m/%d %T"), format!($($args)*)) + if cfg!(feature = "log_info") { + println!("\u{001b}[36m[INFO] {}: {}\u{001b}[0m", Into::>::into(std::time::SystemTime::now()).format("%Y/%m/%d %T"), format!($($args)*)) + } }; } #[macro_export] macro_rules! log_debug { ($($args: tt)*) => { - println!("\u{001b}[35m[DBUG] {}: {}\u{001b}[0m", Into::>::into(std::time::SystemTime::now()).format("%Y/%m/%d %T"), format!($($args)*)) + if cfg!(feature = "log_debug") { + println!("\u{001b}[35m[DBUG] {}: {}\u{001b}[0m", Into::>::into(std::time::SystemTime::now()).format("%Y/%m/%d %T"), format!($($args)*)) + } }; } #[macro_export] macro_rules! log_warn { ($($args: tt)*) => { - println!("\u{001b}[33m[WARN] {}: {}\u{001b}[0m", Into::>::into(std::time::SystemTime::now()).format("%Y/%m/%d %T"), format!($($args)*)) + if cfg!(feature = "log_warn") { + println!("\u{001b}[33m[WARN] {}: {}\u{001b}[0m", Into::>::into(std::time::SystemTime::now()).format("%Y/%m/%d %T"), format!($($args)*)) + } }; } #[macro_export] macro_rules! log_error { ($($args: tt)*) => { - println!("\u{001b}[31m[ERRO] {}: {}\u{001b}[0m", Into::>::into(std::time::SystemTime::now()).format("%Y/%m/%d %T"), format!($($args)*)) + if cfg!(feature = "log_error") { + println!("\u{001b}[31m[ERRO] {}: {}\u{001b}[0m", Into::>::into(std::time::SystemTime::now()).format("%Y/%m/%d %T"), format!($($args)*)) + } }; } diff --git a/src/main.rs b/src/main.rs index fc49565..e9e4cc2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod cli; pub mod dns_packet; mod dns_resolver; pub mod dns_server; @@ -5,30 +6,76 @@ mod macros; mod settings; mod simple_database; +#[cfg(feature = "tui")] +mod tui; + extern crate clap; extern crate yaml_rust; use std::error::Error; use std::fs::{create_dir_all, File}; -use std::io::{stdin, stdout, Write}; -use std::net::Ipv4Addr; use std::path::Path; -use std::str::FromStr; -use clap::{Parser, Subcommand}; -use crate::dns_packet::{DnsQueryType, DnsRecord, DnsRecordA, DnsRecordAAAA, DnsRecordCNAME, DnsRecordDROP, DnsRecordMX, DnsRecordNS, DnsRecordPreamble}; +use clap::{Args, Parser, Subcommand}; +use cli::{add_record, add_record_interactive, list_records}; use crate::dns_server::{DnsServer, DnsTcpServer, DnsUdpServer}; use crate::settings::DnsSettings; use crate::simple_database::SimpleDatabase; +#[cfg(feature = "tui")] +use crate::tui::base::tui_start; + #[derive(Parser, Debug)] #[command(author, version, about = "A simple dns server :)", long_about = None)] -struct Args { +struct Cli { #[command(subcommand)] command: Commands, } +#[derive(Args, Clone, Debug)] +struct RecordFilters { + #[arg(long, value_parser)] + domain: Option, + #[arg(long, value_parser(["A", "NS", "CNAME", "MX", "AAAA", "DROP"]))] + query_type: Option, + #[arg(long, value_parser)] + class: Option, + #[arg(long, value_parser)] + ttl: Option, + #[arg(long, value_parser)] + host: Option, + #[arg(long, value_parser)] + ip: Option, + #[arg(long, value_parser)] + priority: Option, +} + +#[derive(Args, Clone, Debug)] +struct RecordArgs { + #[arg(long, value_parser, required_unless_present("interactive"))] + domain: Option, + #[arg(long, value_parser(["A", "NS", "CNAME", "MX", "AAAA", "DROP"]), required_unless_present("interactive"))] + query_type: Option, + #[arg(long, value_parser, default_value = "1")] + class: u16, + #[arg(long, value_parser, default_value = "300")] + ttl: u32, + #[arg(long, value_parser, required_if_eq_any([ + ("query_type", "NS"), + ("query_type", "CNAME"), + ("query_type", "MX") + ]))] + host: Option, + #[arg(long, value_parser, required_if_eq_any([ + ("query_type", "A"), + ("query_type", "AAAA"), + ]))] + ip: Option, + #[arg(long, value_parser, required_if_eq("query_type", "MX"))] + priority: Option, +} + #[derive(Debug, Subcommand)] enum Commands { Start { @@ -39,38 +86,29 @@ enum Commands { #[arg(short, long, value_parser)] config: Option, }, + Tui { + #[arg(short, long, value_parser)] + config: Option, + }, Add { #[arg(short, long, value_parser)] config: Option, #[arg(short, long, action)] interactive: bool, - #[arg(long, value_parser, required_unless_present("interactive"))] - domain: Option, - #[arg(long, value_parser(["A", "NS", "CNAME", "MX", "AAAA", "DROP"]), required_unless_present("interactive"))] - query_type: Option, - #[arg(long, value_parser, default_value = "1")] - class: u16, - #[arg(long, value_parser, default_value = "300")] - ttl: u32, - #[arg(long, value_parser, required_if_eq_any([ - ("query_type", "NS"), - ("query_type", "CNAME"), - ("query_type", "MX") - ]))] - host: Option, - #[arg(long, value_parser, required_if_eq_any([ - ("query_type", "A"), - ("query_type", "AAAA"), - ]))] - ip: Option, - #[arg(long, value_parser, required_if_eq("query_type", "MX"))] - priority: Option, + #[command(flatten)] + args: RecordArgs, }, + List { + #[arg(short, long, value_parser)] + config: Option, + #[command(flatten)] + filters: RecordFilters, + } } fn main() -> Result<(), Box> { - let args = Args::parse(); - log_info!("Command: {:?}", args.command); + let args = Cli::parse(); + log_debug!("Command: {:?}", args.command); match args.command { Commands::Init { config } => { @@ -80,7 +118,7 @@ fn main() -> Result<(), Box> { }; let settings = settings.expect("Error reading settings!"); - log_info!("Database File Path: {:#?}", settings.database_file); + log_debug!("Database File Path: {:#?}", settings.database_file); let path = Path::new(settings.database_file.as_str()); let parent = path.parent().unwrap(); @@ -90,7 +128,7 @@ fn main() -> Result<(), Box> { let database = SimpleDatabase::new(settings.database_file); match database.initialize() { - Ok(_) => log_info!("Successfully initialized the database :)"), + Ok(_) => log_debug!("Successfully initialized the database :)"), Err(error) => log_error!("There was an error while initializing the database :( | {}", error), } } @@ -100,7 +138,7 @@ fn main() -> Result<(), Box> { None => DnsSettings::load_default(), }; let settings = settings.expect("Error reading settings!"); - log_info!("Settings: {:?}", settings); + log_debug!("Settings: {:?}", settings); let server_udp = DnsUdpServer::new(settings.clone()); let server_tcp = DnsTcpServer::new(settings.clone()); @@ -110,99 +148,62 @@ fn main() -> Result<(), Box> { let _ = server_udp.run(); log_info!("Successfully started UDP server :)"); } else { - log_info!("UDP server was not started due to configuration settings."); + log_debug!("UDP server was not started due to configuration settings."); } if settings.use_tcp { let _ = server_tcp.run(); log_info!("Successfully started TCP server :)"); } else { - log_info!("TCP server was not started due to configuration settings."); + log_debug!("TCP server was not started due to configuration settings."); } }); loop {} // TODO How to deal with this being dead code + // #[allow(unreachable_code)] doesn't work _handle.join().unwrap(); } + #[cfg(feature = "tui")] + Commands::Tui { config } => { + let settings = match config { + Some(filename) => DnsSettings::load_from_file(filename.clone()), + None => DnsSettings::load_default(), + }?; + tui_start(&settings)?; + } + #[cfg(not(feature = "tui"))] + Commands::Tui { config } => { + log_error!("simpledns was not built with the TUI feature :( please rebuild with `cargo build --features \"tui\"`...") + } Commands::Add { config, interactive, .. } if interactive => { let settings = match config { Some(filename) => DnsSettings::load_from_file(filename.clone()), None => DnsSettings::load_default(), }; let settings = settings.expect("Error reading settings!"); - log_info!("Database File Path: {:#?}", settings.database_file); - - let domain = get_input("Domain: ", None, "A domain is required.", |x| !x.is_empty()); // TODO should check for valid domain - let query_type = DnsQueryType::from_string(get_input("Record Type: ", - None, - "A record type is required [A, NS, CNAME, MX, AAAA, DROP]", - |x| ["A", "NS", "CNAME", "MX", "AAAA", "DROP"].contains(&x.to_uppercase().as_str())).as_str()); - let class = get_input("Class [default 1]: ", - Some("1".to_string()), - "A valid u16 must be supplied.", - |x| !x.is_empty() && x.parse::().is_ok()).parse::().unwrap(); - let ttl = get_input("TTL [default 300]: ", - Some("300".to_string()), - "A valid u32 must be supplied.", - |x| !x.is_empty() && x.parse::().is_ok()).parse::().unwrap(); - let preamble = DnsRecordPreamble::build(domain, query_type, class, ttl); - - let record = match query_type { - DnsQueryType::Unknown(_) => panic!("Impossible state"), - DnsQueryType::A => { - let ip = get_input("IP: ", None, "A valid ip address is required.", |x| Ipv4Addr::from_str(x.as_str()).is_ok()); - DnsRecord::A(DnsRecordA::new(preamble, Ipv4Addr::from_str(ip.as_str()).unwrap())) - } - DnsQueryType::NS => { - let host = get_input("Host: ", None, "A host is required.", |x| !x.is_empty()); - DnsRecord::NS(DnsRecordNS::new(preamble, host)) - } - DnsQueryType::CNAME => { - let host = get_input("Host: ", None, "A host is required.", |x| !x.is_empty()); - DnsRecord::CNAME(DnsRecordCNAME::new(preamble, host)) - } - DnsQueryType::MX => { - let host = get_input("Host: ", None, "A host is required.", |x| !x.is_empty()); - let priority = get_input("Priority: ", None, "A valid u16 priority is required.", |x| !x.is_empty() && x.parse::().is_ok()).parse::().unwrap(); - DnsRecord::MX(DnsRecordMX::new(preamble, priority, host)) - } - DnsQueryType::AAAA => { - let ip = get_input("IP: ", None, "A valid ip address is required.", |x| Ipv4Addr::from_str(x.as_str()).is_ok()); - DnsRecord::AAAA(DnsRecordAAAA::new(preamble, Ipv4Addr::from_str(ip.as_str()).unwrap())) - } - DnsQueryType::DROP => DnsRecord::DROP(DnsRecordDROP::new(preamble)) - }; + log_debug!("Database File Path: {:#?}", settings.database_file); - let database = SimpleDatabase::new(settings.database_file); - database.insert_record(record.clone(), false)?; - log_info!("Successfully added record: {:?}", record); + add_record_interactive(settings)?; } - Commands::Add { config, interactive, domain, query_type, class, ttl, host, ip, priority } if !interactive => { + Commands::Add { config, interactive, args } if !interactive => { let settings = match config { Some(filename) => DnsSettings::load_from_file(filename.clone()), None => DnsSettings::load_default(), }; let settings = settings.expect("Error reading settings!"); - log_info!("Database File Path: {:#?}", settings.database_file); - - let domain = domain.unwrap(); - let query_type = DnsQueryType::from_string(query_type.unwrap().as_str()); - let preamble = DnsRecordPreamble::build(domain, query_type, class, ttl); - let record = match query_type { - DnsQueryType::Unknown(_) => panic!("Impossible state"), - DnsQueryType::A => DnsRecord::A(DnsRecordA::new(preamble, Ipv4Addr::from_str(ip.unwrap().as_str()).expect("Couldn't parse ipv4 address"))), - DnsQueryType::NS => DnsRecord::NS(DnsRecordNS::new(preamble, host.unwrap())), - DnsQueryType::CNAME => DnsRecord::CNAME(DnsRecordCNAME::new(preamble, host.unwrap())), - DnsQueryType::MX => DnsRecord::MX(DnsRecordMX::new(preamble, priority.unwrap(), host.unwrap())), - DnsQueryType::AAAA => DnsRecord::AAAA(DnsRecordAAAA::new(preamble, Ipv4Addr::from_str(ip.unwrap().as_str()).expect("Couldn't parse ipv4 address"))), - DnsQueryType::DROP => DnsRecord::DROP(DnsRecordDROP::new(preamble)), - }; + log_debug!("Database File Path: {:#?}", settings.database_file); - let database = SimpleDatabase::new(settings.database_file); - database.insert_record(record.clone(), false)?; - log_info!("Successfully added record: {:?}", record); + add_record(args, settings)?; + } + Commands::List { config, filters} => { + let settings = match config { + Some(filename) => DnsSettings::load_from_file(filename.clone()), + None => DnsSettings::load_default(), + }.expect("Error reading settings :("); + + list_records(settings, filters)?; } _ => log_error!("Unknown command :( \n{:#?}", args), } @@ -210,18 +211,3 @@ fn main() -> Result<(), Box> { Ok(()) } -pub fn get_input(message: &str, default_value: Option, error_message: &str, validate: fn(String) -> bool) -> String { - loop { - let mut s = String::new(); - print!("{}", message); - let _ = stdout().flush(); - let _ = stdin().read_line(&mut s).expect("Did not enter a correct string"); - s = s.trim_matches(&['\r', '\n', ' ', '\t']).to_string(); - if validate(s.clone()) { - return s; - } else if let Some(value) = default_value.clone() { - return value; - } - println!("{}", error_message); - } -} diff --git a/src/settings.rs b/src/settings.rs index 1256452..157d40d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,9 +2,8 @@ use std::error::Error; use std::fs; use std::io::ErrorKind; use yaml_rust::YamlLoader; -use std::path::Path; -use crate::log_info; +use crate::log_debug; extern crate shellexpand; @@ -24,8 +23,8 @@ impl DnsSettings { let error_str = "Aw man, there was an issue while opening the config file '{".to_owned() + filename.as_str() + "}' :("; let contents = fs::read_to_string(shellexpand::full(filename.as_str()).unwrap().to_string()) - .expect(&error_str); - log_info!("Loaded from config file '{}'...", filename.as_str()); + .expect(&error_str); + log_debug!("Loaded from config file '{}'...", filename.as_str()); let yaml_files = &YamlLoader::load_from_str(contents.as_str())?; let config_settings_option = &yaml_files.get(0); @@ -79,8 +78,11 @@ impl DnsSettings { pub fn load_default() -> Result> { let filenames = ["./dns.config.yaml", "~/.config/simpledns/dns.config.yaml", "/etc/simpledns/dns.config.yaml"]; let mut config_file = ""; - for filename in filenames { - if Path::new(filename).exists() { config_file = filename; break; } + for filename in filenames { + if fs::exists(shellexpand::full(filename).unwrap().to_string())? { + config_file = filename; + break; + } } if config_file == "" { panic!("No valid config file given"); } Self::load_from_file(String::from(config_file)) diff --git a/src/simple_database.rs b/src/simple_database.rs index 1acd081..b3f23ab 100644 --- a/src/simple_database.rs +++ b/src/simple_database.rs @@ -2,7 +2,7 @@ use crate::dns_packet::{ DnsQueryType, DnsRecord, DnsRecordA, DnsRecordAAAA, DnsRecordCNAME, DnsRecordDROP, DnsRecordMX, DnsRecordNS, DnsRecordPreamble, DnsRecordUnknown, }; -use rusqlite::{Connection, Result}; +use rusqlite::{params, Connection, Params, Result, Statement}; use std::net::Ipv4Addr; use std::str; use std::str::FromStr; @@ -27,12 +27,8 @@ impl SimpleDatabase { Ok(()) } - pub fn get_records(&self, domain: String, _query_type: DnsQueryType) -> Result> { - self.connection.execute("DELETE FROM records WHERE records.cached AND records.ttl < unixepoch() - records.insert_time;", [])?; - - // TODO what does query type do? - let mut stmt = self.connection.prepare("SELECT domain, query_type, class, ttl, len, hostipbody, priority FROM records WHERE domain = ?1;")?; - let query_results = stmt.query_map(&[&domain], |row| { + fn run_dns_record_query(&self, mut statement: Statement<'_>, params: P) -> Result> { + let query_results = statement.query_map(params, |row| { let mut preamble = DnsRecordPreamble::new(); preamble.domain = row.get(0)?; preamble.query_type = DnsQueryType::from_num(row.get(1)?); @@ -72,6 +68,29 @@ impl SimpleDatabase { Ok(results) } + fn clean_up_cache(&self) -> Result<()> { + self.connection.execute("DELETE FROM records WHERE records.cached AND records.ttl < unixepoch() - records.insert_time;", [])?; + Ok(()) + } + + pub fn get_all_records(&self) -> Result> { + self.clean_up_cache()?; + let stmt = self.connection.prepare("SELECT domain, query_type, class, ttl, len, hostipbody, priority FROM records;")?; + self.run_dns_record_query(stmt, params![]) + } + + /* TODO pub fn get_records_where(&self, where_filter: String, params: P) -> Result> { + self.clean_up_cache()?; + let stmt = self.connection.prepare(format!("SELECT domain, query_type, class, ttl, len, hostipbody, priority FROM records WHERE {};", where_filter).as_str())?; + self.run_dns_record_query(stmt, params) + }*/ + + pub fn get_records(&self, domain: String) -> Result> { + self.clean_up_cache()?; + let stmt = self.connection.prepare("SELECT domain, query_type, class, ttl, len, hostipbody, priority FROM records WHERE domain = ?1;")?; + self.run_dns_record_query(stmt, params![domain]) + } + pub fn insert_record(&self, record: DnsRecord, cached_record: bool) -> Result<()> { let preamble = record.get_preamble(); let domain = preamble.domain; diff --git a/src/tui/base.rs b/src/tui/base.rs new file mode 100644 index 0000000..8797a5d --- /dev/null +++ b/src/tui/base.rs @@ -0,0 +1,152 @@ +use std::borrow::Borrow; +use std::io::Result; +use std::thread::current; + +use ratatui::buffer::Buffer; +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, MouseEvent}; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::prelude::Stylize; +use ratatui::style::Style; +use ratatui::symbols::border; +use ratatui::text::Line; +use ratatui::widgets::{Block, List, ListDirection, ListState, Paragraph, StatefulWidget, Widget}; +use ratatui::{DefaultTerminal, Frame}; + +use crate::settings::DnsSettings; +use crate::log_debug; +use crate::simple_database::SimpleDatabase; + +use super::event::{SimpleEvent, SimpleEventResult}; +use super::record_list_view::RecordListView; +use super::view::View; + +pub fn tui_start(settings: &DnsSettings) -> Result<()> { + log_debug!("Starting TUI...."); + let mut terminal = ratatui::init(); + terminal.clear().expect("Couldn't clear terminal :("); + let mut state = AppState::new(); + App::new(settings).run(&mut terminal, &mut state)?; + ratatui::restore(); + Ok(()) +} + +struct AppState { + selected_view: ListState, +} + +impl AppState { + pub fn new() -> Self { + Self { + selected_view: ListState::default().with_selected(Some(0)), + } + } + + pub fn current_view(&self) -> usize { + match self.selected_view.selected() { + Some(idx) => idx, + None => panic!("idk what to do here") + } + } +} + +struct App { + //simple_connection: SimpleDatabase, + views: Vec>, + exit: bool +} + +impl App { + pub fn new(settings: &DnsSettings) -> Self { + Self { + //simple_connection: SimpleDatabase::new(settings.database_file.clone()), + views: vec![RecordListView::new_boxed(settings)], + exit: false + } + } + + pub fn run(&mut self, terminal: &mut DefaultTerminal, state: &mut AppState) -> Result<()> { + while !self.exit { + terminal.draw(|frame| self.draw(frame, state))?; + self.handle_events(state)?; + } + Ok(()) + } + + pub fn draw(&self, frame: &mut Frame, state: &mut AppState) { + frame.render_stateful_widget(self, frame.area(), state); + } + + pub fn handle_events(&mut self, state: &AppState) -> Result<()> { + let mut current_view = &mut self.views[state.current_view()]; + match event::poll(current_view.poll_rate()) { + Ok(true) => { + let simple_event: SimpleEvent = event::read()?.into(); + match current_view.handle_event(simple_event.clone()) { + SimpleEventResult::Consume => {} + SimpleEventResult::Bubble => match simple_event { + SimpleEvent::Key(key) if key.kind == KeyEventKind::Press && key.code == KeyCode::Esc => { + self.exit = true; + } + _ => {} + } + } + } + Ok(false) => { current_view.handle_event(SimpleEvent::Tick); } + Err(error) => {} // WHAT TO DO??? + } + Ok(()) + } +} + +impl StatefulWidget for &App { + type State = AppState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let current_view = &self.views[state.current_view()]; + + let main_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![ + Constraint::Percentage(80), + Constraint::Percentage(20), + ]).split(area); + let main_area = main_layout[0]; + + let side_layout = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Percentage(50), + Constraint::Percentage(50), + ]).split(main_layout[1]); + let views_area = side_layout[0]; + let help_area = side_layout[1]; + + let title = Line::from("[ SimpleDNS ]".bold()); + + let block = Block::bordered() + .title(title.centered()) + .border_set(border::DOUBLE); + + current_view.draw(block, main_area, buf); + + let views = self.views.iter().map(|x| x.name()).collect::>(); + let view_title = Line::from("[ Views ]".bold()); + let views_block = Block::bordered() + .title(view_title.centered()) + .border_set(border::DOUBLE); + StatefulWidget::render(List::new(views) + .highlight_style(Style::new().bold().italic()) + .highlight_symbol("->") + .direction(ListDirection::TopToBottom) + .block(views_block), views_area, buf, &mut state.selected_view); + + let help = current_view.help(); + let help_title = Line::from("[ Help ]".bold()); + let help_block = Block::bordered() + .title(help_title.centered()) + .border_set(border::DOUBLE); + Paragraph::new(help) + .block(help_block) + .render(help_area, buf); + } +} diff --git a/src/tui/event.rs b/src/tui/event.rs new file mode 100644 index 0000000..159bafb --- /dev/null +++ b/src/tui/event.rs @@ -0,0 +1,29 @@ +use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent}; + +#[derive(Clone)] +pub enum SimpleEvent { + Key(KeyEvent), + Mouse(MouseEvent), + Paste(String), + Focus(bool), + Resize(u16, u16), + Tick, +} + +impl From for SimpleEvent { + fn from(value: Event) -> Self { + match value { + Event::FocusGained =>SimpleEvent::Focus(true), + Event::FocusLost => SimpleEvent::Focus(false), + Event::Key(key_event) => SimpleEvent::Key(key_event), + Event::Mouse(mouse_event) => SimpleEvent::Mouse(mouse_event), + Event::Paste(data) => SimpleEvent::Paste(data), + Event::Resize(x, y) => SimpleEvent::Resize(x, y), + } + } +} + +pub enum SimpleEventResult { + Consume, + Bubble +} \ No newline at end of file diff --git a/src/tui/mod.rs b/src/tui/mod.rs new file mode 100644 index 0000000..f966102 --- /dev/null +++ b/src/tui/mod.rs @@ -0,0 +1,6 @@ +extern crate ratatui; + +pub mod base; +mod view; +mod event; +mod record_list_view; \ No newline at end of file diff --git a/src/tui/record_list_view.rs b/src/tui/record_list_view.rs new file mode 100644 index 0000000..c47dc68 --- /dev/null +++ b/src/tui/record_list_view.rs @@ -0,0 +1,74 @@ +use std::time::Duration; + +use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::{Block, Chart, Dataset, Paragraph, Row, Table, Widget}}; +use ratatui::prelude::Stylize; +use ratatui::prelude::Style; + +use crate::{dns_packet::DnsRecord, settings::DnsSettings, simple_database::SimpleDatabase}; + +use super::{event::{SimpleEvent, SimpleEventResult}, view::View}; + +pub struct RecordListView { + simple_database: SimpleDatabase +} + +impl RecordListView { + pub fn new(settings: &DnsSettings) -> Self { + Self { + simple_database: SimpleDatabase::new(settings.database_file.clone()) + } + } + + pub fn new_boxed(settings: &DnsSettings) -> Box { + Box::new(Self::new(settings)) + } +} + +impl View for RecordListView { + fn draw(&self, block: Block, area: Rect, buf: &mut Buffer) { + match self.simple_database.get_all_records() { + Ok(records) => { + Table::default() + .rows(records.iter().map(|x| x.to_row()).collect::>>()) + .header(Row::new(vec!["Query Type", "Domain", "Host/IP", "Priority", "TTL", "Class"]).underlined().cyan()) + .row_highlight_style(Style::new().underlined()) + .highlight_symbol("->") + .block(block) + .render(area, buf); + } + Err(error) => { + Paragraph::new("ERROR GETTING LIST OF RECORDS FROM DB") + .centered() + .red() + .bold() + .italic() + .block(block) + .render(area, buf); + } + } + + } + + fn handle_event(&mut self, event: SimpleEvent) -> SimpleEventResult { + SimpleEventResult::Bubble + } + + fn name(&self) -> Line { + Line::from(vec![ + " ".into(), + "R".red().bold(), + "ecords".blue(), + " ".into() + ]) + } + + fn help(&self) -> Text { + Text::from(vec![ + "[ESC] - Exit SimpleDNS".into() + ]) + } + + fn poll_rate(&self) -> Duration { + Duration::from_secs(1) + } +} diff --git a/src/tui/view.rs b/src/tui/view.rs new file mode 100644 index 0000000..e2810ec --- /dev/null +++ b/src/tui/view.rs @@ -0,0 +1,13 @@ +use std::time::Duration; + +use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::{Block, Widget}}; + +use super::event::{SimpleEvent, SimpleEventResult}; + +pub trait View { + fn draw(&self, block: Block, area: Rect, buf: &mut Buffer); + fn handle_event(&mut self, event: SimpleEvent) -> SimpleEventResult; + fn name(&self) -> Line; + fn help(&self) -> Text; + fn poll_rate(&self) -> Duration; +} \ No newline at end of file