From 05182e9eaf089e05a6772c06e40effce896f8593 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Fri, 28 Jun 2024 23:13:22 +0800 Subject: [PATCH 01/10] init: initial project structure - Added initial project files and directories - Set up project configuration and initial dependencies - Finish the Servers Selector UI --- .gitignore | 1 + Cargo.lock | 777 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 11 + src/app.rs | 162 +++++++++++ src/main.rs | 59 ++++ 5 files changed, 1010 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/app.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..441749d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,777 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "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 = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[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 = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "object" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ratatui" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "itertools 0.13.0", + "lru", + "paste", + "stability", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +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 = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "ssh-utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "backtrace", + "clap", + "crossterm", + "ratatui", +] + +[[package]] +name = "stability" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools 0.12.1", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9022e7a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ssh-utils" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.86" +backtrace = "0.3.73" +clap = "4.5.7" +crossterm = "0.27.0" +ratatui = "0.27.0" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..dd8296a --- /dev/null +++ b/src/app.rs @@ -0,0 +1,162 @@ +use anyhow::Ok; +use anyhow::Result; +use crossterm::event; +use crossterm::event::Event; +use crossterm::event::KeyCode::*; +use crossterm::event::KeyEventKind; +use ratatui::backend::Backend; +use ratatui::buffer::Buffer; +use ratatui::layout::Constraint; +use ratatui::layout::Layout; +use ratatui::layout::Rect; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::text::Text; +use ratatui::widgets::HighlightSpacing; +use ratatui::widgets::List; +use ratatui::widgets::ListItem; +use ratatui::widgets::ListState; +use ratatui::widgets::StatefulWidget; +use ratatui::widgets::Widget; +use ratatui::Terminal; + +struct ServerItem { + name: String, + address: String, + username: String +} + +struct ServerList { + state: ListState, + items: Vec, + last_selected: Option, +} + +impl ServerList { + fn with_items(items: Vec) -> ServerList { + ServerList { + state: ListState::default(), + items, + last_selected: None, + } + } + + 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)); + } + + 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 struct App { + server_list: ServerList +} + +impl Widget for &mut App { + fn render(self, area: Rect, buf: &mut Buffer){ + let vertical = Layout::vertical([ + Constraint::Length(1), + Constraint::Min(0) + ]); + let [head_area, body_area] = vertical.areas(area); + self.render_header(head_area, buf); + self.render_servers(body_area, buf); + } +} + +impl App { + fn render_header(&self, area: Rect, buf: &mut Buffer) { + let text = Text::styled(format!(" {:<10} {:<15} {:<20}", "user", "ip", "name"), Style::default().add_modifier(Modifier::BOLD)); + Widget::render(text, area, buf); + } + + fn render_servers(&mut self, area: Rect, buf: &mut Buffer) { + let items: Vec = self.server_list.items.iter().map(|item| { + ListItem::new(format!("{:<10} {:<15} {:<20}", item.username, item.address, item.name)) + }).collect(); + + let items = List::new(items) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::REVERSED) + ) + .highlight_symbol("> ") + .highlight_spacing(HighlightSpacing::Always); + + StatefulWidget::render(&items, area, buf, &mut self.server_list.state); + } +} + +impl App { + pub fn new() -> Result { + if cfg!(debug_assertions) { + let app = Self { + server_list: ServerList::with_items(vec![ + ServerItem { + name: "Aliyun ECS".to_string(), + address: "exmaple.com".to_string(), + username: "admin".to_string() + }, + ServerItem { + name: "AWS lightsail".to_string(), + address: "127.0.0.1".to_string(), + username: "root".to_string() + }, + ServerItem { + name: "My Homelab".to_string(), + address: "192.0.0.1".to_string(), + username: "admin".to_string() + } + ]) + }; + Ok(app) + } else { + todo!() + } + } + + + fn draw(&mut self, terminal: &mut Terminal) -> Result<()> { + terminal.draw(|f| f.render_widget(self, f.size()))?; + Ok(()) + } + + pub fn run(&mut self, mut terminal: &mut Terminal) -> Result<()> { + loop{ + self.draw(&mut terminal)?; + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match key.code { + Char('q') | Esc => return Ok(()), + Char('j') | Down => self.server_list.next(), + Char('k') | Up => self.server_list.previous(), + _ => {} + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..72d95fc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,59 @@ +mod app; + +use std::{io::{self, Stdout}, panic}; +use app::App; +use backtrace::Backtrace; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode + }; +use ratatui::{backend::CrosstermBackend, Terminal, TerminalOptions, Viewport}; +use anyhow::{Context, Ok, Result}; + +fn main() -> Result<()> { + set_panic_handlers()?; + let mut terminal = create_terminal()?; + + setup_terminal(&mut terminal)?; + let app = App::new()?; + run_app(app, &mut terminal)?; + restore_terminal()?; + + Ok(()) +} + +fn run_app(mut app: App, terminal: &mut Terminal>) -> Result<(), anyhow::Error> { + app.run(terminal)?; + Ok(()) +} + +fn setup_terminal(terminal: &mut Terminal>) -> Result<(), anyhow::Error> { + terminal.clear()?; + Ok(()) +} + +fn create_terminal() -> Result>> { + let stdout = io::stdout(); + enable_raw_mode()?; + let terminal_option = TerminalOptions { + //TODO: 设置最大行数 + viewport: Viewport::Inline(10) + }; + Terminal::with_options(CrosstermBackend::new(stdout), terminal_option).context("unable to create terminal") +} + +fn restore_terminal() -> Result<()> { + disable_raw_mode()?; + Ok(()) +} + +// handle all panic here +fn set_panic_handlers() -> Result<()> { + panic::set_hook(Box::new(|e| { + if let Err(e) = disable_raw_mode() { + eprintln!("unable to disable raw mode:\n{e}"); + } + let backtrace = Backtrace::new(); + eprintln!("\nssh-utils was close due to an unexpected panic with the following info:\n\n{:?}\ntrace:\n{:?}", e, backtrace); + })); + Ok(()) +} \ No newline at end of file -- Gitee From 6b74bf08344c35805c1301be574a5d899a1ce1c3 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sat, 29 Jun 2024 01:48:19 +0800 Subject: [PATCH 02/10] feat: restore terminal to status that before exec program - clean program's UI in terminal after shutdown program. --- src/main.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 72d95fc..76994c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,11 +3,12 @@ mod app; use std::{io::{self, Stdout}, panic}; use app::App; use backtrace::Backtrace; -use crossterm::terminal::{ - disable_raw_mode, enable_raw_mode - }; +use crossterm::{cursor::{RestorePosition, SavePosition}, execute, terminal::{ + disable_raw_mode, enable_raw_mode, Clear, ClearType + }}; use ratatui::{backend::CrosstermBackend, Terminal, TerminalOptions, Viewport}; -use anyhow::{Context, Ok, Result}; +use anyhow::{Context, Result}; +use std::io::stdout; fn main() -> Result<()> { set_panic_handlers()?; @@ -32,7 +33,11 @@ fn setup_terminal(terminal: &mut Terminal>) -> Result<( } fn create_terminal() -> Result>> { - let stdout = io::stdout(); + let mut stdout = io::stdout(); + execute!( + stdout, + SavePosition + )?; enable_raw_mode()?; let terminal_option = TerminalOptions { //TODO: 设置最大行数 @@ -41,7 +46,13 @@ fn create_terminal() -> Result>> { Terminal::with_options(CrosstermBackend::new(stdout), terminal_option).context("unable to create terminal") } +// restore terminal to status that before exec program fn restore_terminal() -> Result<()> { + execute!( + stdout(), + RestorePosition, + Clear(ClearType::FromCursorDown) + )?; disable_raw_mode()?; Ok(()) } @@ -49,8 +60,8 @@ fn restore_terminal() -> Result<()> { // handle all panic here fn set_panic_handlers() -> Result<()> { panic::set_hook(Box::new(|e| { - if let Err(e) = disable_raw_mode() { - eprintln!("unable to disable raw mode:\n{e}"); + if let Err(e) = restore_terminal() { + eprintln!("unable to restore terminal:\n{e}"); } let backtrace = Backtrace::new(); eprintln!("\nssh-utils was close due to an unexpected panic with the following info:\n\n{:?}\ntrace:\n{:?}", e, backtrace); -- Gitee From e234d4bf8b9d7e08c530a0b57f6d038e2512cb54 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Mon, 1 Jul 2024 11:37:25 +0800 Subject: [PATCH 03/10] feature-wip: exit logical when ctrl + c --- src/app.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app.rs b/src/app.rs index dd8296a..9160c04 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,6 +4,7 @@ use crossterm::event; use crossterm::event::Event; use crossterm::event::KeyCode::*; use crossterm::event::KeyEventKind; +use crossterm::event::KeyModifiers; use ratatui::backend::Backend; use ratatui::buffer::Buffer; use ratatui::layout::Constraint; @@ -153,6 +154,11 @@ impl App { Char('q') | Esc => return Ok(()), Char('j') | Down => self.server_list.next(), Char('k') | Up => self.server_list.previous(), + Char('c') => { + if key.modifiers == KeyModifiers::CONTROL { + return Ok(()) + } + } _ => {} } } -- Gitee From 2f01689c829ee9ac3dc9a68c3763e83aa32f7aa2 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Mon, 1 Jul 2024 12:04:05 +0800 Subject: [PATCH 04/10] improvement: more effective panic hook --- src/main.rs | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index 76994c7..47de51b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ mod app; -use std::{io::{self, Stdout}, panic}; +use std::{io::{self, Stdout}, panic::{self, PanicInfo}}; use app::App; -use backtrace::Backtrace; use crossterm::{cursor::{RestorePosition, SavePosition}, execute, terminal::{ disable_raw_mode, enable_raw_mode, Clear, ClearType }}; @@ -11,7 +10,8 @@ use anyhow::{Context, Result}; use std::io::stdout; fn main() -> Result<()> { - set_panic_handlers()?; + // Setup panic hook + panic::set_hook(Box::new(panic_hook)); let mut terminal = create_terminal()?; setup_terminal(&mut terminal)?; @@ -57,14 +57,32 @@ fn restore_terminal() -> Result<()> { Ok(()) } -// handle all panic here -fn set_panic_handlers() -> Result<()> { - panic::set_hook(Box::new(|e| { - if let Err(e) = restore_terminal() { - eprintln!("unable to restore terminal:\n{e}"); - } - let backtrace = Backtrace::new(); - eprintln!("\nssh-utils was close due to an unexpected panic with the following info:\n\n{:?}\ntrace:\n{:?}", e, backtrace); - })); - Ok(()) +/// A panic hook to properly restore the terminal in the case of a panic. +/// Originally based on [spotify-tui's implementation](https://github.com/Rigellute/spotify-tui/blob/master/src/main.rs). +fn panic_hook(panic_info: &PanicInfo<'_>) { + let mut stdout = stdout(); + + let msg = match panic_info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => match panic_info.payload().downcast_ref::() { + Some(s) => &s[..], + None => "Box", + }, + }; + + let backtrace = format!("{:?}", backtrace::Backtrace::new()); + + if let Err(e) = restore_terminal() { + eprintln!("unable to restore terminal:\n{e}"); + } + + // Print stack trace. Must be done after! + if let Some(panic_info) = panic_info.location() { + let _ = execute!( + stdout, + crossterm::style::Print(format!( + "application panic: '{msg}', {panic_info}\n\r{backtrace}", + )), + ); + } } \ No newline at end of file -- Gitee From 9e29feaded798b5e4686661fdad32188c5af02cf Mon Sep 17 00:00:00 2001 From: Kurisu Date: Fri, 5 Jul 2024 16:24:58 +0800 Subject: [PATCH 05/10] feature-wip: render tip in footer render `Create (C), Delete (D)` in footer. --- src/app.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index 9160c04..0ea4833 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,6 +12,7 @@ use ratatui::layout::Layout; use ratatui::layout::Rect; use ratatui::style::Modifier; use ratatui::style::Style; +use ratatui::style::Stylize; use ratatui::text::Text; use ratatui::widgets::HighlightSpacing; use ratatui::widgets::List; @@ -79,11 +80,13 @@ impl Widget for &mut App { fn render(self, area: Rect, buf: &mut Buffer){ let vertical = Layout::vertical([ Constraint::Length(1), - Constraint::Min(0) + Constraint::Min(0), + Constraint::Length(1) ]); - let [head_area, body_area] = vertical.areas(area); + let [head_area, body_area, foot_area] = vertical.areas(area); self.render_header(head_area, buf); self.render_servers(body_area, buf); + self.render_footer(foot_area, buf); } } @@ -93,6 +96,11 @@ impl App { Widget::render(text, area, buf); } + fn render_footer(&self, area: Rect, buf: &mut Buffer) { + let text = Text::from(" Create (C), Delete (D)").dim(); + Widget::render(text, area, buf); + } + fn render_servers(&mut self, area: Rect, buf: &mut Buffer) { let items: Vec = self.server_list.items.iter().map(|item| { ListItem::new(format!("{:<10} {:<15} {:<20}", item.username, item.address, item.name)) -- Gitee From 4c278481ab02d9b27389755c8e2ed652d8e788a7 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sat, 13 Jul 2024 19:47:44 +0800 Subject: [PATCH 06/10] feat: add the page of adding server. --- src/app.rs | 10 +- src/main.rs | 1 + src/widgets/mod.rs | 1 + src/widgets/server_creator.rs | 294 ++++++++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 src/widgets/mod.rs create mode 100644 src/widgets/server_creator.rs diff --git a/src/app.rs b/src/app.rs index 0ea4833..46da0ff 100644 --- a/src/app.rs +++ b/src/app.rs @@ -22,6 +22,8 @@ use ratatui::widgets::StatefulWidget; use ratatui::widgets::Widget; use ratatui::Terminal; +use crate::widgets::server_creator::ServerCreator; + struct ServerItem { name: String, address: String, @@ -97,7 +99,7 @@ impl App { } fn render_footer(&self, area: Rect, buf: &mut Buffer) { - let text = Text::from(" Create (C), Delete (D)").dim(); + let text = Text::from(" Add (A), Delete (D), Quit (ESC)").dim(); Widget::render(text, area, buf); } @@ -163,9 +165,15 @@ impl App { Char('j') | Down => self.server_list.next(), Char('k') | Up => self.server_list.previous(), Char('c') => { + // Set this hotkey because of man's habit if key.modifiers == KeyModifiers::CONTROL { return Ok(()) } + }, + Char('a') => { + //添加 server + let mut server_creator = ServerCreator::new(); + server_creator.run(&mut terminal)?; } _ => {} } diff --git a/src/main.rs b/src/main.rs index 47de51b..626ad2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod app; +mod widgets; use std::{io::{self, Stdout}, panic::{self, PanicInfo}}; use app::App; diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs new file mode 100644 index 0000000..895223b --- /dev/null +++ b/src/widgets/mod.rs @@ -0,0 +1 @@ +pub mod server_creator; \ No newline at end of file diff --git a/src/widgets/server_creator.rs b/src/widgets/server_creator.rs new file mode 100644 index 0000000..ab078ee --- /dev/null +++ b/src/widgets/server_creator.rs @@ -0,0 +1,294 @@ +use std::ops::{Add, Sub}; + +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use ratatui::{backend::Backend, + buffer::Buffer, + layout::{Constraint, Layout, Rect}, + style::{Style, Stylize}, + text::{Line, Span, Text}, + widgets::{Paragraph, Widget}, + Terminal}; +use anyhow::Result; + +/// current selected item in form +#[derive(Copy, Clone)] +enum CurrentSelect { + User = 0, + Ip, + Password, + Name, +} + +/// impl Add and Sub for CurrentSelect +impl Add for CurrentSelect { + type Output = Self; + + fn add(self, other: Self) -> Self { + let new_value = (self as isize + other as isize) % 4; + match new_value { + 0 => CurrentSelect::User, + 1 => CurrentSelect::Ip, + 2 => CurrentSelect::Password, + 3 => CurrentSelect::Name, + _ => unreachable!(), + } + } +} + +impl Sub for CurrentSelect { + type Output = Self; + + fn sub(self, other: Self) -> Self { + let new_value = (self as isize - other as isize + 4) % 4; + match new_value { + 0 => CurrentSelect::User, + 1 => CurrentSelect::Ip, + 2 => CurrentSelect::Password, + 3 => CurrentSelect::Name, + _ => unreachable!(), + } + } +} + +impl Add for CurrentSelect { + type Output = Self; + + fn add(self, other: isize) -> Self { + let new_value = (self as isize + other).rem_euclid(4); + match new_value { + 0 => CurrentSelect::User, + 1 => CurrentSelect::Ip, + 2 => CurrentSelect::Password, + 3 => CurrentSelect::Name, + _ => unreachable!(), + } + } +} + +impl Sub for CurrentSelect { + type Output = Self; + + fn sub(self, other: isize) -> Self { + let new_value = (self as isize - other).rem_euclid(4); + match new_value { + 0 => CurrentSelect::User, + 1 => CurrentSelect::Ip, + 2 => CurrentSelect::Password, + 3 => CurrentSelect::Name, + _ => unreachable!(), + } + } +} + +/// App holds the state of the application +pub struct ServerCreator { + /// Current values of the input boxes + input: Vec, + /// Position of cursor in the editor area. + character_index: usize, + /// current selected item + current_select: CurrentSelect, + /// form position + /// used to set cursor + form_position: (u16, u16) +} + +impl Widget for &mut ServerCreator { + fn render(self, area: Rect, buf: &mut Buffer) { + let vertical = Layout::vertical([ + Constraint::Length(1), + Constraint::Min(0), + Constraint::Length(1) + ]); + let [head_area, body_area, foot_area] = vertical.areas(area); + self.form_position = (body_area.x, body_area.y); + self.render_header(head_area, buf); + self.render_form(body_area, buf); + self.render_footer(foot_area, buf); + } +} + +impl ServerCreator { + pub fn new() -> Self { + Self { + input: vec![String::new(), String::new(), String::new(), String::new()], + character_index: 0, + current_select: CurrentSelect::User, + form_position: (0,3) + + } + } + + fn render_header(&self, area: Rect, buf: &mut Buffer) { + let text = Text::from("Enter server information below:").yellow(); + Widget::render(text, area, buf); + } + + fn render_footer(&self, area: Rect, buf: &mut Buffer) { + let text = Text::from(" Save (^S), Quit (ESC)").dim(); + Widget::render(text, area, buf); + } + + fn render_form(&self, area: Rect, buf: &mut Buffer) { + // highlight currently selected item + let mut user: Vec = vec![" user:".into(), self.input[CurrentSelect::User as usize].clone().into()]; + let mut ip: Vec = vec![" ip:".into(), self.input[CurrentSelect::Ip as usize].clone().into()]; + let mut password: Vec = vec!["password:".into(), self.input[CurrentSelect::Password as usize].clone().into()]; + let mut name: Vec = vec![" name:".into(), self.input[CurrentSelect::Name as usize].clone().into()]; + + match self.current_select { + CurrentSelect::User => user[0] = Span::styled(" user:", Style::new().bold()), + CurrentSelect::Ip => ip[0] = Span::styled(" ip:", Style::new().bold()), + CurrentSelect::Password => password[0] = Span::styled("password:", Style::new().bold()), + CurrentSelect::Name => name[0] = Span::styled(" name:", Style::new().bold()), + } + + let user_line = Line::from(user); + let ip_line = Line::from(ip); + let password_line = Line::from(password); + let name_line = Line::from(name); + let text = vec![user_line, ip_line, password_line, name_line]; + let form = Paragraph::new(text); + Widget::render(&form, area, buf); + } + + fn move_cursor_left(&mut self) { + let cursor_moved_left = self.character_index.saturating_sub(1); + self.character_index = self.clamp_cursor(cursor_moved_left); + } + + fn move_cursor_right(&mut self) { + let cursor_moved_right = self.character_index.saturating_add(1); + self.character_index = self.clamp_cursor(cursor_moved_right); + } + + fn moveto_current_cursor(&mut self) { + let cursor_position = self.character_index; + self.character_index = self.clamp_cursor(cursor_position); + } + + fn enter_char(&mut self, new_char: char) { + let index = self.byte_index(); + self.input[self.current_select as usize].insert(index, new_char); + self.move_cursor_right(); + } + + /// Returns the byte index based on the character position. + /// + /// Since each character in a string can be contain multiple bytes, it's necessary to calculate + /// the byte index based on the index of the character. + fn byte_index(&mut self) -> usize { + self.input[self.current_select as usize] + .char_indices() + .map(|(i, _)| i) + .nth(self.character_index) + .unwrap_or(self.input[self.current_select as usize].len()) + } + + fn delete_char(&mut self) { + let is_not_cursor_leftmost = self.character_index != 0; + if is_not_cursor_leftmost { + // Method "remove" is not used on the saved text for deleting the selected char. + // Reason: Using remove on String works on bytes instead of the chars. + // Using remove would require special care because of char boundaries. + + let current_index = self.character_index; + let from_left_to_current_index = current_index - 1; + + // Getting all characters before the selected character. + let before_char_to_delete = self.input[self.current_select as usize].chars().take(from_left_to_current_index); + // Getting all characters after selected character. + let after_char_to_delete = self.input[self.current_select as usize].chars().skip(current_index); + + // Put all characters together except the selected one. + // By leaving the selected one out, it is forgotten and therefore deleted. + self.input[self.current_select as usize] = before_char_to_delete.chain(after_char_to_delete).collect(); + self.move_cursor_left(); + } + } + + fn clamp_cursor(&self, new_cursor_pos: usize) -> usize { + new_cursor_pos.clamp(0, self.input[self.current_select as usize].chars().count()) + } + + fn move_next_select_item(&mut self) { + self.current_select = self.current_select + 1; + } + + fn move_pre_select_item(&mut self) { + self.current_select = self.current_select - 1; + } +} + + +impl ServerCreator { + fn draw(&mut self, terminal: &mut Terminal) -> Result<()> { + terminal.draw(|f| { + let character_index = self.character_index as u16; + let form_position = self.form_position; + let cursor_x = form_position.0 + character_index + 9; + let cursor_y = form_position.1 + self.current_select as u16; + + f.render_widget(self, f.size()); + f.set_cursor(cursor_x,cursor_y); + })?; + Ok(()) + } + + pub fn run(&mut self, mut terminal: &mut Terminal) -> Result<()> { + loop{ + self.draw(&mut terminal)?; + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char(to_insert) => { + // Set this hotkey because of man's habit + if to_insert == 'c' { + if key.modifiers == event::KeyModifiers::CONTROL { + return Ok(()) + } + } + self.enter_char(to_insert); + } + KeyCode::Backspace => { + self.delete_char(); + } + KeyCode::Left => { + self.move_cursor_left(); + } + KeyCode::Right => { + self.move_cursor_right(); + } + KeyCode::Esc => { + return Ok(()); + } + KeyCode::Up => { + self.move_pre_select_item(); + self.moveto_current_cursor(); + } + KeyCode::Down | KeyCode::Enter => { + self.move_next_select_item(); + self.moveto_current_cursor(); + } + _ => {} + } + } + } + } + } +} + +#[test] +fn run_widget() -> Result<()> { + crossterm::terminal::enable_raw_mode()?; + let stdout = std::io::stdout(); + crossterm::execute!(std::io::stdout(), crossterm::terminal::EnterAlternateScreen)?; + let backend = ratatui::backend::CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + let mut app = ServerCreator::new(); + + app.run(&mut terminal)?; + crossterm::execute!(std::io::stdout(), crossterm::terminal::LeaveAlternateScreen)?; + crossterm::terminal::disable_raw_mode()?; + Ok(()) +} \ No newline at end of file -- Gitee From 19ded4e687093bcf597ec479656b964bedfa89a4 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sat, 13 Jul 2024 19:50:20 +0800 Subject: [PATCH 07/10] chore: remove unused prop. --- src/app.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app.rs b/src/app.rs index 46da0ff..07eee92 100644 --- a/src/app.rs +++ b/src/app.rs @@ -32,16 +32,14 @@ struct ServerItem { struct ServerList { state: ListState, - items: Vec, - last_selected: Option, + items: Vec } impl ServerList { fn with_items(items: Vec) -> ServerList { ServerList { state: ListState::default(), - items, - last_selected: None, + items } } -- Gitee From 1a568b5559c511dff68accb1c2ce7041a82a680b Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sat, 13 Jul 2024 20:42:23 +0800 Subject: [PATCH 08/10] fix: wrong cursor index in add server page. --- src/widgets/server_creator.rs | 73 ++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/src/widgets/server_creator.rs b/src/widgets/server_creator.rs index ab078ee..eb60a14 100644 --- a/src/widgets/server_creator.rs +++ b/src/widgets/server_creator.rs @@ -1,13 +1,6 @@ use std::ops::{Add, Sub}; - use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use ratatui::{backend::Backend, - buffer::Buffer, - layout::{Constraint, Layout, Rect}, - style::{Style, Stylize}, - text::{Line, Span, Text}, - widgets::{Paragraph, Widget}, - Terminal}; +use ratatui::{backend::Backend, buffer::Buffer, layout::{Constraint, Layout, Rect}, style::{Style, Stylize}, text::{Line, Span, Text}, widgets::{Paragraph, Widget}, Frame, Terminal}; use anyhow::Result; /// current selected item in form @@ -87,34 +80,30 @@ pub struct ServerCreator { /// Position of cursor in the editor area. character_index: usize, /// current selected item - current_select: CurrentSelect, - /// form position - /// used to set cursor - form_position: (u16, u16) + current_select: CurrentSelect } -impl Widget for &mut ServerCreator { - fn render(self, area: Rect, buf: &mut Buffer) { - let vertical = Layout::vertical([ - Constraint::Length(1), - Constraint::Min(0), - Constraint::Length(1) - ]); - let [head_area, body_area, foot_area] = vertical.areas(area); - self.form_position = (body_area.x, body_area.y); - self.render_header(head_area, buf); - self.render_form(body_area, buf); - self.render_footer(foot_area, buf); - } -} +// impl Widget for &mut ServerCreator { +// fn render(self, area: Rect, buf: &mut Buffer) { +// let vertical = Layout::vertical([ +// Constraint::Length(1), +// Constraint::Min(0), +// Constraint::Length(1) +// ]); +// let [head_area, body_area, foot_area] = vertical.areas(area); +// self.form_position = (body_area.x, body_area.y); +// self.render_header(head_area, buf); +// self.render_form(body_area, buf); +// self.render_footer(foot_area, buf); +// } +// } impl ServerCreator { pub fn new() -> Self { Self { input: vec![String::new(), String::new(), String::new(), String::new()], character_index: 0, - current_select: CurrentSelect::User, - form_position: (0,3) + current_select: CurrentSelect::User } } @@ -224,13 +213,7 @@ impl ServerCreator { impl ServerCreator { fn draw(&mut self, terminal: &mut Terminal) -> Result<()> { terminal.draw(|f| { - let character_index = self.character_index as u16; - let form_position = self.form_position; - let cursor_x = form_position.0 + character_index + 9; - let cursor_y = form_position.1 + self.current_select as u16; - - f.render_widget(self, f.size()); - f.set_cursor(cursor_x,cursor_y); + ui(f, &self) })?; Ok(()) } @@ -278,6 +261,26 @@ impl ServerCreator { } } +fn ui(f: &mut Frame, server_creator: &ServerCreator) { + let vertical = Layout::vertical([ + Constraint::Length(1), + Constraint::Min(0), + Constraint::Length(1) + ]); + let [head_area, body_area, foot_area] = vertical.areas(f.size()); + server_creator.render_header(head_area, f.buffer_mut()); + server_creator.render_form(body_area, f.buffer_mut()); + server_creator.render_footer(foot_area, f.buffer_mut()); + + let character_index = server_creator.character_index as u16; + //due to input character index start at 9 + //eg: "password:" + //so here add 9 + let cursor_x = body_area.x + character_index + 9; + let cursor_y = body_area.y + server_creator.current_select as u16; + f.set_cursor(cursor_x,cursor_y); +} + #[test] fn run_widget() -> Result<()> { crossterm::terminal::enable_raw_mode()?; -- Gitee From f11dd07110d88d193d0f0bd99f8acf4c27a0016e Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sun, 14 Jul 2024 02:49:25 +0800 Subject: [PATCH 09/10] improvement: use * to replace the input password. --- src/widgets/server_creator.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/widgets/server_creator.rs b/src/widgets/server_creator.rs index eb60a14..3a81837 100644 --- a/src/widgets/server_creator.rs +++ b/src/widgets/server_creator.rs @@ -122,7 +122,10 @@ impl ServerCreator { // highlight currently selected item let mut user: Vec = vec![" user:".into(), self.input[CurrentSelect::User as usize].clone().into()]; let mut ip: Vec = vec![" ip:".into(), self.input[CurrentSelect::Ip as usize].clone().into()]; - let mut password: Vec = vec!["password:".into(), self.input[CurrentSelect::Password as usize].clone().into()]; + // we use * to replace the password + let password_length = self.input[CurrentSelect::Password as usize].len(); + let masked_password: String = "*".repeat(password_length); + let mut password: Vec = vec!["password:".into(), masked_password.into()]; let mut name: Vec = vec![" name:".into(), self.input[CurrentSelect::Name as usize].clone().into()]; match self.current_select { -- Gitee From fb6fb82c2baa1869fcf9a349d6b4a45256053b23 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sun, 14 Jul 2024 02:50:44 +0800 Subject: [PATCH 10/10] improvement: use tab key to move to next item. --- src/widgets/server_creator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/server_creator.rs b/src/widgets/server_creator.rs index 3a81837..3dc2206 100644 --- a/src/widgets/server_creator.rs +++ b/src/widgets/server_creator.rs @@ -252,7 +252,7 @@ impl ServerCreator { self.move_pre_select_item(); self.moveto_current_cursor(); } - KeyCode::Down | KeyCode::Enter => { + KeyCode::Down | KeyCode::Enter | KeyCode::Tab => { self.move_next_select_item(); self.moveto_current_cursor(); } -- Gitee